feat: Start learn phase

This commit is contained in:
BisratHailu 2026-01-12 09:22:52 +03:00
parent d8a184429b
commit c222b3c67a
172 changed files with 11140 additions and 659 deletions

View File

@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:label="yimaru_app" android:label="Yimaru"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Terms and Condition</title>
<style>
:root {
--primary: #9c2c91;
--text-dark: #2b2b2b;
--text-gray: #6f6f6f;
--background: #ffffff;
--divider: #eaeaea;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body {
background: var(--background);
color: var(--text-dark);
line-height: 1.6;
}
.container {
max-width: 420px;
margin: 0 auto;
padding: 20px;
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 20px;
}
.back-btn {
position: absolute;
left: 0;
font-size: 20px;
cursor: pointer;
}
.title {
color: var(--primary);
font-weight: 600;
font-size: 18px;
}
.updated {
font-size: 13px;
color: var(--primary);
margin-bottom: 20px;
}
/* Sections */
h2 {
font-size: 22px;
font-weight: 700;
margin: 20px 0 10px;
}
p {
font-size: 15px;
color: var(--text-gray);
margin-bottom: 15px;
}
ul {
padding-left: 18px;
margin-top: 10px;
}
li {
font-size: 15px;
color: var(--text-gray);
margin-bottom: 8px;
}
a {
color: #7b2cff;
text-decoration: none;
font-weight: 500;
}
/* Button */
.download-btn {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 40px);
max-width: 420px;
background: var(--primary);
color: white;
border: none;
padding: 15px;
border-radius: 30px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
}
.spacer {
height: 100px;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="back-btn">←</div>
<div class="title">Terms and Condition</div>
</div>
<div class="updated">Last updated: October 26, 2025</div>
<!-- Introduction -->
<h2>Introduction</h2>
<p>
Welcome to Yimaru! These terms and conditions outline the rules and
regulations for the use of our application. By accessing this app,
we assume you accept these terms and conditions.
</p>
<!-- User Accounts -->
<h2>User Accounts</h2>
<p>
When you create an account with us, you must provide us with information
that is accurate, complete, and current at all times. Failure to do so
constitutes a breach of the Terms, which may result in immediate
termination of your account on our Service.
</p>
<!-- Content & Services -->
<h2>Content & Services</h2>
<p>
Our Service allows you to access learning materials. You are granted a
limited license to access and use the app content for personal,
non-commercial purposes. You agree not to:
</p>
<ul>
<li>Reproduce, duplicate, copy, or sell any material from the app.</li>
<li>Redistribute content from Yimaru.</li>
<li>Use the app in any way that is damaging or harmful.</li>
</ul>
<!-- Privacy Policy -->
<h2>Privacy Policy</h2>
<p>
Your privacy is important to us. Please read our
<a href="#">Privacy Policy</a> to understand how we collect, use,
and share information about you.
</p>
<!-- Contact -->
<h2>Contact Us</h2>
<p>
If you have any questions about these Terms, please contact us at
<strong>support@yimaru.et</strong>.
</p>
<div class="spacer"></div>
</div>
<!-- Download Button -->
<button class="download-btn">
Download as PDF ⬇
</button>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

View File

@ -1,15 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?> <svg width="524" height="173" viewBox="0 0 524 173" fill="none" xmlns="http://www.w3.org/2000/svg">
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <g clip-path="url(#clip0_23_9014)">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="137px" height="45px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink"> <path d="M194.31 28.74L206.48 51L219.27 28.76H232.21L212.29 62.86V83H200.67V62.84L180.75 28.74H194.31Z" fill="white"/>
<g><path style="opacity:0.855" fill="#fefffe" d="M 17.5,-0.5 C 18.8333,-0.5 20.1667,-0.5 21.5,-0.5C 24.7669,1.81111 28.1002,4.14444 31.5,6.5C 27.8282,9.33799 23.8282,11.6713 19.5,13.5C 15.1718,11.6713 11.1718,9.33799 7.5,6.5C 10.8514,4.14144 14.1847,1.80811 17.5,-0.5 Z"/></g> <path d="M247.79 32.69C247.809 33.51 247.661 34.3253 247.353 35.0855C247.045 35.8457 246.584 36.5347 246 37.11C245.412 37.6963 244.71 38.1565 243.938 38.4625C243.166 38.7686 242.34 38.914 241.51 38.89C240.677 38.907 239.85 38.7584 239.076 38.4528C238.302 38.1472 237.596 37.6908 237 37.11C236.41 36.5373 235.944 35.8494 235.631 35.0891C235.317 34.3287 235.164 33.5122 235.18 32.69C235.159 31.8731 235.31 31.0609 235.623 30.3062C235.937 29.5516 236.406 28.8714 237 28.31C237.604 27.7351 238.317 27.2854 239.096 26.9867C239.875 26.6881 240.706 26.5465 241.54 26.57C242.362 26.5477 243.18 26.6902 243.946 26.989C244.712 27.2879 245.41 27.737 246 28.31C246.586 28.8756 247.048 29.5572 247.356 30.3113C247.665 31.0655 247.812 31.8756 247.79 32.69ZM246.93 43.15V83H236V43.15H246.93Z" fill="white"/>
<g><path style="opacity:0.694" fill="#fefffe" d="M 62.5,5.5 C 63.6946,5.86562 64.3613,6.69895 64.5,8C 64.3178,12.3314 64.3178,16.8314 64.5,21.5C 63.1667,21.5 61.8333,21.5 60.5,21.5C 60.2065,17.5633 60.5399,13.73 61.5,10C 61,9.5 60.5,9 60,8.5C 56.7899,12.2511 55.2899,16.5844 55.5,21.5C 54.1667,21.5 52.8333,21.5 51.5,21.5C 51.5405,16.243 49.8738,11.5763 46.5,7.5C 47.723,6.38615 49.0563,6.21948 50.5,7C 51.7875,8.40952 52.9542,9.90952 54,11.5C 55.3906,7.36888 58.224,5.36888 62.5,5.5 Z"/></g> <path d="M316.92 61.13V83.07H306V60.74C306 54.7 303.7 51.68 299.1 51.68C298.017 51.641 296.939 51.8473 295.947 52.2837C294.954 52.7201 294.074 53.3752 293.37 54.2C291.819 56.1915 291.048 58.6803 291.2 61.2V83.07H280.26V60.74C280.26 54.7 277.927 51.68 273.26 51.68C272.176 51.6412 271.098 51.855 270.11 52.3045C269.123 52.754 268.253 53.4269 267.57 54.27C266.061 56.2816 265.304 58.7583 265.43 61.27V83.07H254.53V43.15H264.06L265.06 48.15C266.341 46.4745 267.981 45.1076 269.86 44.15C272.055 43.1452 274.447 42.643 276.86 42.68C282.54 42.68 286.494 44.9267 288.72 49.42C290.134 47.2728 292.099 45.5458 294.41 44.42C296.907 43.2298 299.645 42.6342 302.41 42.68C304.364 42.6151 306.308 42.9668 308.115 43.7116C309.922 44.4565 311.55 45.5774 312.89 47C315.57 49.8067 316.914 54.5167 316.92 61.13Z" fill="white"/>
<g><path style="opacity:0.93" fill="#fefffe" d="M 25.5,44.5 C 21.5,44.5 17.5,44.5 13.5,44.5C 13.8282,39.1254 13.4948,33.7921 12.5,28.5C 8.27197,25.8873 3.93863,23.554 -0.5,21.5C -0.5,20.5 -0.5,19.5 -0.5,18.5C 1.72466,15.7376 3.72466,12.7376 5.5,9.5C 10.1667,12.1667 14.8333,14.8333 19.5,17.5C 24.0825,14.9593 28.5825,12.2926 33,9.5C 35.549,12.765 37.7157,16.265 39.5,20C 35.538,23.32 31.2047,26.1534 26.5,28.5C 25.5052,33.7921 25.1718,39.1254 25.5,44.5 Z"/></g> <path d="M363 83H357.42C352.4 83 349.944 80.83 350.05 76.49C348.731 78.6384 346.895 80.4226 344.71 81.68C342.347 82.9271 339.701 83.5403 337.03 83.46C332.59 83.46 329.014 82.44 326.3 80.4C324.968 79.4019 323.9 78.0925 323.19 76.5864C322.481 75.0802 322.151 73.4231 322.23 71.76C322.153 69.8652 322.549 67.9809 323.382 66.2771C324.214 64.5733 325.458 63.1034 327 62C330.12 59.7 334.654 58.55 340.6 58.55H348.51V56.56C348.542 55.736 348.377 54.9164 348.029 54.1687C347.681 53.4211 347.161 52.7668 346.51 52.26C344.913 51.1085 342.966 50.5465 341 50.67C339.238 50.5873 337.492 51.0481 336 51.99C335.383 52.3861 334.857 52.9079 334.455 53.5213C334.054 54.1348 333.786 54.8261 333.67 55.55H323.19C323.3 53.6561 323.848 51.8134 324.79 50.1668C325.732 48.5201 327.043 47.1144 328.62 46.06C331.874 43.8133 336.187 42.69 341.56 42.69C347.187 42.69 351.54 43.93 354.62 46.41C357.7 48.89 359.237 52.48 359.23 57.18V71.18C359.19 71.5381 359.224 71.9007 359.331 72.2449C359.438 72.5891 359.614 72.9075 359.85 73.18C360.408 73.5936 361.098 73.7892 361.79 73.73H363V83ZM340.47 65.78C338.602 65.6742 336.747 66.1493 335.16 67.14C334.544 67.5708 334.048 68.1509 333.717 68.8261C333.386 69.5012 333.233 70.2492 333.27 71C333.247 71.6411 333.379 72.2783 333.654 72.8578C333.929 73.4374 334.339 73.9423 334.85 74.33C336.106 75.2174 337.625 75.6544 339.16 75.57C340.39 75.6344 341.62 75.4517 342.777 75.0326C343.935 74.6135 344.997 73.9666 345.9 73.13C346.741 72.2633 347.401 71.2376 347.84 70.1129C348.28 68.9882 348.49 67.7872 348.46 66.58V65.8L340.47 65.78Z" fill="white"/>
<g><path style="opacity:0.752" fill="#fefffe" d="M 65.5,10.5 C 70.9897,10.8823 76.323,11.0489 81.5,11C 82.4558,14.3936 82.7892,17.8936 82.5,21.5C 81.5,21.5 80.5,21.5 79.5,21.5C 79.6633,19.1432 79.4966,16.8098 79,14.5C 78.586,14.0426 78.086,13.7093 77.5,13.5C 76.52,16.0865 76.1866,18.7531 76.5,21.5C 75.1667,21.5 73.8333,21.5 72.5,21.5C 72.5,18.8333 72.5,16.1667 72.5,13.5C 71.5,13.5 70.5,13.5 69.5,13.5C 69.5,16.1667 69.5,18.8333 69.5,21.5C 68.1667,21.5 66.8333,21.5 65.5,21.5C 65.5,17.8333 65.5,14.1667 65.5,10.5 Z"/></g> <path d="M391.54 53.07H387.2C384.1 53.07 381.827 54 380.38 55.86C378.827 58.0794 378.063 60.7553 378.21 63.46V83H367.29V43.15H377.21L378.21 49.15C379.277 47.3096 380.799 45.7739 382.63 44.69C384.812 43.5903 387.239 43.0703 389.68 43.18H391.54V53.07Z" fill="white"/>
<g><path style="opacity:0.731" fill="#fefffe" d="M 85.5,10.5 C 88.1194,10.2317 90.6194,10.565 93,11.5C 93.4552,14.8536 93.9552,18.1869 94.5,21.5C 91.0678,21.4096 87.5678,21.0763 84,20.5C 83.1751,18.2992 83.6751,16.4659 85.5,15C 84.6919,14.6924 84.0253,14.1924 83.5,13.5C 84.1925,12.4822 84.8592,11.4822 85.5,10.5 Z"/></g> <path d="M433.94 83H424.25L423.25 78.19C421.853 79.9283 420.065 81.3117 418.032 82.2275C415.999 83.1434 413.778 83.5657 411.55 83.46C409.459 83.5413 407.373 83.2027 405.415 82.4642C403.457 81.7256 401.667 80.6021 400.15 79.16C397.21 76.2867 395.74 71.4967 395.74 64.79V43.15H406.66V63.46C406.66 67.08 407.3 69.8 408.56 71.63C409.218 72.552 410.101 73.2899 411.125 73.7732C412.149 74.2564 413.28 74.4688 414.41 74.39C415.642 74.4479 416.869 74.1975 417.979 73.6614C419.09 73.1253 420.049 72.3206 420.77 71.32C422.257 69.28 423 66.4267 423 62.76V43.15H433.93L433.94 83Z" fill="white"/>
<g><path style="opacity:0.753" fill="#fefffe" d="M 95.5,10.5 C 99.1667,10.5 102.833,10.5 106.5,10.5C 106.5,13.1667 106.5,15.8333 106.5,18.5C 107.5,18.5 108.5,18.5 109.5,18.5C 109.5,15.8333 109.5,13.1667 109.5,10.5C 110.833,10.5 112.167,10.5 113.5,10.5C 113.5,14.1667 113.5,17.8333 113.5,21.5C 110.644,21.0717 107.644,20.905 104.5,21C 103.029,18.7572 102.029,16.2572 101.5,13.5C 98.828,15.4255 97.828,18.0921 98.5,21.5C 97.5,21.5 96.5,21.5 95.5,21.5C 95.5,17.8333 95.5,14.1667 95.5,10.5 Z"/></g> <path d="M200.9 89.43H214.85L234.54 143.68H222.06L217.87 131.82H197.26L193.07 143.68H181.07L200.9 89.43ZM214.7 122.43L207.49 101.82L200.36 122.43H214.7Z" fill="white"/>
<g><path style="opacity:0.736" fill="#fefffe" d="M 51.5,22.5 C 53.288,22.2148 54.9547,22.5481 56.5,23.5C 57.7653,26.7957 58.9319,30.129 60,33.5C 60.5617,26.874 64.0617,24.7073 70.5,27C 71.3366,28.0113 71.67,29.1779 71.5,30.5C 69.7376,30.6427 68.0709,30.3094 66.5,29.5C 63.8333,31.1667 63.8333,32.8333 66.5,34.5C 67.8498,33.0499 69.5165,32.3832 71.5,32.5C 70.8258,37.1514 68.1592,38.6514 63.5,37C 62.2823,35.5839 60.9489,35.7506 59.5,37.5C 57.6711,36.5354 55.8377,35.5354 54,34.5C 51.8157,34.8852 49.9823,35.8852 48.5,37.5C 47.8333,37.1667 47.1667,36.8333 46.5,36.5C 48.1805,31.8123 49.8471,27.1457 51.5,22.5 Z M 53.5,28.5 C 54.7759,29.3864 54.7759,30.3864 53.5,31.5C 52.4376,30.5754 52.4376,29.5754 53.5,28.5 Z"/></g> <path d="M255.77 144.15C251.999 144.235 248.268 143.369 244.92 141.63C241.873 140.003 239.377 137.507 237.75 134.46C236.09 131.132 235.226 127.464 235.226 123.745C235.226 120.026 236.09 116.358 237.75 113.03C239.403 109.984 241.927 107.501 245 105.9C248.362 104.162 252.107 103.296 255.89 103.38C261.224 103.38 265.557 104.737 268.89 107.45C272.315 110.281 274.532 114.312 275.09 118.72H263.75C263.384 116.884 262.417 115.223 261 114C259.536 112.814 257.693 112.197 255.81 112.26C254.504 112.217 253.205 112.482 252.02 113.035C250.835 113.587 249.797 114.411 248.99 115.44C247.323 117.885 246.431 120.776 246.431 123.735C246.431 126.694 247.323 129.585 248.99 132.03C249.799 133.056 250.838 133.877 252.023 134.427C253.207 134.978 254.505 135.243 255.81 135.2C257.737 135.266 259.623 134.634 261.12 133.42C262.579 132.164 263.552 130.438 263.87 128.54H275.11C274.597 133.016 272.376 137.121 268.91 140C265.517 142.76 261.137 144.143 255.77 144.15Z" fill="white"/>
<g><path style="opacity:0.718" fill="#fefffe" d="M 83.5,34.5 C 82.641,28.1855 85.3077,25.5189 91.5,26.5C 91.5,25.1667 91.5,23.8333 91.5,22.5C 92.8333,22.5 94.1667,22.5 95.5,22.5C 95.5,27.5 95.5,32.5 95.5,37.5C 92.4816,37.6646 89.4816,37.498 86.5,37C 85.3094,36.3021 84.3094,35.4687 83.5,34.5 Z M 88.5,29.5 C 91.9563,30.5362 92.2896,32.2029 89.5,34.5C 87.2957,33.218 86.9624,31.5514 88.5,29.5 Z"/></g> <path d="M320.17 143.68H314.59C309.583 143.68 307.13 141.51 307.23 137.17C305.907 139.319 304.068 141.103 301.88 142.36C299.521 143.609 296.878 144.226 294.21 144.15C289.76 144.15 286.21 143.15 283.47 141.08C282.137 140.082 281.069 138.773 280.36 137.266C279.65 135.76 279.321 134.103 279.4 132.44C279.31 130.546 279.691 128.659 280.51 126.949C281.328 125.239 282.558 123.758 284.09 122.64C287.223 120.34 291.76 119.19 297.7 119.19H305.6V117.25C305.632 116.426 305.467 115.606 305.119 114.858C304.772 114.11 304.251 113.456 303.6 112.95C302.059 111.957 300.275 111.407 298.442 111.359C296.61 111.312 294.8 111.768 293.21 112.68C292.595 113.078 292.071 113.6 291.671 114.213C291.271 114.827 291.005 115.517 290.89 116.24H280.33C280.442 114.347 280.99 112.505 281.932 110.858C282.874 109.212 284.184 107.806 285.76 106.75C289.013 104.503 293.326 103.38 298.7 103.38C304.34 103.38 308.693 104.62 311.76 107.1C314.826 109.58 316.363 113.17 316.37 117.87V131.87C316.332 132.228 316.367 132.59 316.474 132.934C316.58 133.278 316.756 133.596 316.99 133.87C317.552 134.274 318.239 134.466 318.93 134.41H320.17V143.68ZM297.62 126.47C295.751 126.36 293.895 126.836 292.31 127.83C291.694 128.257 291.197 128.833 290.865 129.505C290.532 130.177 290.376 130.921 290.41 131.67C290.388 132.312 290.521 132.95 290.798 133.529C291.074 134.109 291.487 134.613 292 135C293.252 135.887 294.767 136.324 296.3 136.24C297.523 136.301 298.746 136.117 299.897 135.698C301.047 135.279 302.102 134.634 303 133.8C303.844 132.936 304.506 131.911 304.946 130.785C305.386 129.66 305.594 128.458 305.56 127.25V126.47H297.62Z" fill="white"/>
<g><path style="opacity:0.737" fill="#fefffe" d="M 83.5,34.5 C 83.5,35.5 83.5,36.5 83.5,37.5C 80.242,36.9322 76.9087,36.7655 73.5,37C 72.9186,36.1074 72.5852,35.1074 72.5,34C 73.3774,31.8828 73.5441,29.7161 73,27.5C 76,26.1667 79,26.1667 82,27.5C 82.1862,30.0356 82.6862,32.369 83.5,34.5 Z M 77.5,32.5 C 79.9752,32.7467 79.9752,33.4134 77.5,34.5C 76.537,34.0302 76.537,33.3635 77.5,32.5 Z"/></g> <path d="M364.27 143.68H354.73L353.65 138.18C352.225 140.111 350.352 141.666 348.191 142.712C346.031 143.757 343.649 144.261 341.25 144.18C337.925 144.23 334.65 143.358 331.79 141.66C328.968 139.944 326.695 137.456 325.24 134.49C323.61 131.134 322.802 127.44 322.88 123.71C322.795 120.002 323.604 116.329 325.24 113C326.711 110.043 328.998 107.569 331.83 105.87C334.706 104.174 337.992 103.302 341.33 103.35C346.49 103.35 350.49 105.157 353.33 108.77V89.43H364.26L364.27 143.68ZM353.5 123.84C353.645 120.825 352.708 117.857 350.86 115.47C350.01 114.432 348.931 113.605 347.708 113.054C346.485 112.502 345.151 112.241 343.81 112.29C342.464 112.241 341.124 112.503 339.895 113.054C338.665 113.605 337.579 114.432 336.72 115.47C334.978 117.885 334.041 120.787 334.041 123.765C334.041 126.743 334.978 129.645 336.72 132.06C337.581 133.095 338.668 133.919 339.897 134.468C341.126 135.018 342.465 135.278 343.81 135.23C345.147 135.283 346.479 135.027 347.701 134.482C348.924 133.938 350.005 133.119 350.86 132.09C352.705 129.748 353.642 126.818 353.5 123.84Z" fill="white"/>
<g><path style="opacity:0.78" fill="#fefffe" d="M 98.5,26.5 C 100.857,26.3367 103.19,26.5034 105.5,27C 107.155,28.4443 107.822,30.2776 107.5,32.5C 104.813,32.3359 102.146,32.5026 99.5,33C 100.108,33.8699 100.941,34.3699 102,34.5C 107.553,32.6843 108.386,33.5176 104.5,37C 99.7658,38.3118 97.0991,36.6451 96.5,32C 96.3948,29.8218 97.0615,27.9885 98.5,26.5 Z"/></g> <path d="M379.88 105.9C383.105 104.17 386.722 103.301 390.38 103.38C394.058 103.311 397.698 104.12 401 105.74C403.986 107.237 406.486 109.55 408.21 112.41C410.559 116.613 411.392 121.496 410.57 126.24H381.7V126.55C381.805 129.079 382.792 131.492 384.49 133.37C385.356 134.219 386.389 134.878 387.524 135.305C388.658 135.732 389.87 135.917 391.08 135.85C393.027 135.923 394.952 135.426 396.62 134.42C398.103 133.446 399.147 131.932 399.53 130.2H410.3C409.913 132.787 408.881 135.236 407.3 137.32C405.634 139.494 403.446 141.214 400.94 142.32C398.09 143.586 394.998 144.21 391.88 144.15C387.901 144.251 383.955 143.401 380.37 141.67C377.234 140.103 374.642 137.629 372.93 134.57C371.14 131.282 370.243 127.583 370.33 123.84C370.23 120.073 371.09 116.342 372.83 113C374.447 110.003 376.895 107.538 379.88 105.9ZM397 113.49C395.244 112.124 393.064 111.416 390.84 111.49C388.664 111.409 386.532 112.119 384.84 113.49C383.196 114.894 382.157 116.879 381.94 119.03H400C399.746 116.868 398.672 114.885 397 113.49Z" fill="white"/>
<g><path style="opacity:0.743" fill="#fefffe" d="M 136.5,26.5 C 136.5,27.5 136.5,28.5 136.5,29.5C 134.732,32.6057 133.232,35.9391 132,39.5C 130.211,41.229 128.044,41.8957 125.5,41.5C 125.5,40.5 125.5,39.5 125.5,38.5C 126.5,38.5 127.5,38.5 128.5,38.5C 128.263,35.3527 127.43,32.3527 126,29.5C 125.503,32.146 125.336,34.8127 125.5,37.5C 124.167,37.5 122.833,37.5 121.5,37.5C 121.813,34.7531 121.48,32.0865 120.5,29.5C 119.914,29.7093 119.414,30.0426 119,30.5C 118.503,32.8098 118.337,35.1432 118.5,37.5C 117.167,37.5 115.833,37.5 114.5,37.5C 114.813,34.7531 114.48,32.0865 113.5,29.5C 112.914,29.7093 112.414,30.0426 112,30.5C 111.503,32.8098 111.337,35.1432 111.5,37.5C 110.5,37.5 109.5,37.5 108.5,37.5C 108.5,33.8333 108.5,30.1667 108.5,26.5C 115.5,26.5 122.5,26.5 129.5,26.5C 130.5,34.5 131.5,34.5 132.5,26.5C 133.833,26.5 135.167,26.5 136.5,26.5 Z"/></g> <path d="M479.28 121.82V143.76H468.35V121.44C468.35 115.393 466.05 112.37 461.45 112.37C460.366 112.329 459.288 112.535 458.295 112.972C457.303 113.408 456.422 114.064 455.72 114.89C454.167 116.881 453.392 119.369 453.54 121.89V143.75H442.62V121.44C442.62 115.393 440.286 112.37 435.62 112.37C434.534 112.33 433.453 112.543 432.464 112.992C431.475 113.442 430.604 114.115 429.92 114.96C428.413 116.973 427.659 119.449 427.79 121.96V143.74H416.89V103.84H426.42L427.42 108.84C428.7 107.164 430.341 105.798 432.22 104.84C434.415 103.835 436.806 103.333 439.22 103.37C444.9 103.37 448.85 105.617 451.07 110.11C452.49 107.965 454.458 106.238 456.77 105.11C459.266 103.92 462.004 103.324 464.77 103.37C466.717 103.299 468.657 103.642 470.462 104.376C472.267 105.111 473.895 106.22 475.24 107.63C477.926 110.477 479.273 115.207 479.28 121.82Z" fill="white"/>
<path d="M502.21 132.44L511.67 103.84H523.37L505.7 149.34C504.985 151.312 504.102 153.219 503.06 155.04C502.312 156.331 501.231 157.398 499.93 158.13C498.401 158.889 496.706 159.25 495 159.18H484.39V150H490.39C491.439 150.081 492.486 149.836 493.39 149.3C494.197 148.556 494.808 147.625 495.17 146.59L496.17 143.95L480.9 103.84H492.52L502.21 132.44Z" fill="white"/>
<path d="M121.07 26.53L98.76 39.42L76.03 52.54L53.26 39.39L53.24 39.38L30.96 26.52L31.41 25.76L53.24 13.15L53.26 13.13L76.02 0L98.76 13.12L120.6 25.74L121.07 26.53Z" fill="white"/>
<path d="M53.26 172.41H98.76V110.9L121.51 97.78L137.93 88.29L152.02 80.15L129.27 40.75L98.76 58.36L92.43 62.02L76 71.49L59.6 62.03L53.26 58.37L53.24 58.36L22.75 40.75L0 80.16L14.09 88.3L30.5 97.78L53.24 110.9L53.26 110.91V137.18V172.41Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_23_9014">
<rect width="523.37" height="172.42" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -5,7 +5,27 @@ import 'package:stacked/stacked_annotations.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart';
import 'package:yimaru_app/ui/views/startup/startup_view.dart'; import 'package:yimaru_app/ui/views/startup/startup_view.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart'; import 'package:yimaru_app/ui/views/profile/profile_view.dart';
import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart';
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart';
import 'package:yimaru_app/ui/views/progress/progress_view.dart';
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart';
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart';
import 'package:yimaru_app/ui/views/support/support_view.dart';
import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart';
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart';
import 'package:yimaru_app/ui/views/language/language_view.dart';
import 'package:yimaru_app/ui/views/privacy_policy/privacy_policy_view.dart';
import 'package:yimaru_app/ui/views/terms_and_conditions/terms_and_conditions_view.dart';
import 'package:yimaru_app/ui/views/register/register_view.dart';
import 'package:yimaru_app/ui/views/login/login_view.dart';
import 'package:yimaru_app/ui/views/learn/learn_view.dart';
import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart';
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/services/api_service.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
import 'package:yimaru_app/services/dio_service.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -13,13 +33,34 @@ import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart';
MaterialRoute(page: HomeView), MaterialRoute(page: HomeView),
MaterialRoute(page: OnboardingView), MaterialRoute(page: OnboardingView),
MaterialRoute(page: StartupView), MaterialRoute(page: StartupView),
MaterialRoute(page: ProfileView),
MaterialRoute(page: ProfileDetailView),
MaterialRoute(page: DownloadsView),
MaterialRoute(page: ProgressView),
MaterialRoute(page: OngoingProgressView),
MaterialRoute(page: AccountPrivacyView),
MaterialRoute(page: SupportView),
MaterialRoute(page: TelegramSupportView),
MaterialRoute(page: CallSupportView),
MaterialRoute(page: LanguageView),
MaterialRoute(page: PrivacyPolicyView),
MaterialRoute(page: TermsAndConditionsView),
MaterialRoute(page: RegisterView),
MaterialRoute(page: LoginView),
MaterialRoute(page: LearnView),
MaterialRoute(page: LearnLevelView),
MaterialRoute(page: LearnModuleView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [
LazySingleton(classType: BottomSheetService), LazySingleton(classType: BottomSheetService),
LazySingleton(classType: DialogService), LazySingleton(classType: DialogService),
LazySingleton(classType: NavigationService), LazySingleton(classType: NavigationService),
// @stacked-service LazySingleton(classType: AuthenticationService),
LazySingleton(classType: ApiService),
LazySingleton(classType: SecureStorageService),
LazySingleton(classType: DioService),
// @stacked-service
], ],
bottomsheets: [ bottomsheets: [
StackedBottomsheet(classType: NoticeSheet), StackedBottomsheet(classType: NoticeSheet),

View File

@ -11,6 +11,11 @@ import 'package:stacked_services/src/dialog/dialog_service.dart';
import 'package:stacked_services/src/navigation/navigation_service.dart'; import 'package:stacked_services/src/navigation/navigation_service.dart';
import 'package:stacked_shared/stacked_shared.dart'; import 'package:stacked_shared/stacked_shared.dart';
import '../services/api_service.dart';
import '../services/authentication_service.dart';
import '../services/dio_service.dart';
import '../services/secure_storage_service.dart';
final locator = StackedLocator.instance; final locator = StackedLocator.instance;
Future<void> setupLocator({ Future<void> setupLocator({
@ -25,4 +30,8 @@ Future<void> setupLocator({
locator.registerLazySingleton(() => BottomSheetService()); locator.registerLazySingleton(() => BottomSheetService());
locator.registerLazySingleton(() => DialogService()); locator.registerLazySingleton(() => DialogService());
locator.registerLazySingleton(() => NavigationService()); locator.registerLazySingleton(() => NavigationService());
locator.registerLazySingleton(() => AuthenticationService());
locator.registerLazySingleton(() => ApiService());
locator.registerLazySingleton(() => SecureStorageService());
locator.registerLazySingleton(() => DioService());
} }

View File

@ -5,14 +5,38 @@
// ************************************************************************** // **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i6; import 'package:flutter/material.dart' as _i22;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1; import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i7; import 'package:stacked_services/stacked_services.dart' as _i23;
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i10;
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
as _i13;
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2; import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2;
import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart' as _i5; import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19;
import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20;
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
as _i21;
import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18;
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'
as _i9;
import 'package:yimaru_app/ui/views/privacy_policy/privacy_policy_view.dart'
as _i15;
import 'package:yimaru_app/ui/views/profile/profile_view.dart' as _i5;
import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart'
as _i6;
import 'package:yimaru_app/ui/views/progress/progress_view.dart' as _i8;
import 'package:yimaru_app/ui/views/register/register_view.dart' as _i17;
import 'package:yimaru_app/ui/views/startup/startup_view.dart' as _i4; import 'package:yimaru_app/ui/views/startup/startup_view.dart' as _i4;
import 'package:yimaru_app/ui/views/support/support_view.dart' as _i11;
import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart'
as _i12;
import 'package:yimaru_app/ui/views/terms_and_conditions/terms_and_conditions_view.dart'
as _i16;
class Routes { class Routes {
static const homeView = '/home-view'; static const homeView = '/home-view';
@ -21,12 +45,61 @@ class Routes {
static const startupView = '/startup-view'; static const startupView = '/startup-view';
static const profileView = '/profile-view';
static const profileDetailView = '/profile-detail-view';
static const downloadsView = '/downloads-view';
static const progressView = '/progress-view';
static const ongoingProgressView = '/ongoing-progress-view';
static const accountPrivacyView = '/account-privacy-view';
static const supportView = '/support-view';
static const telegramSupportView = '/telegram-support-view';
static const callSupportView = '/call-support-view';
static const languageView = '/language-view';
static const privacyPolicyView = '/privacy-policy-view';
static const termsAndConditionsView = '/terms-and-conditions-view';
static const registerView = '/register-view';
static const loginView = '/login-view';
static const learnView = '/learn-view';
static const learnLevelView = '/learn-level-view';
static const learnModuleView = '/learn-module-view';
static const all = <String>{ static const all = <String>{
homeView, homeView,
onboardingView, onboardingView,
startupView, startupView,
profileView,
profileDetailView,
downloadsView,
progressView,
ongoingProgressView,
accountPrivacyView,
supportView,
telegramSupportView,
callSupportView,
languageView,
privacyPolicyView,
termsAndConditionsView,
registerView,
loginView,
learnView,
learnLevelView,
learnModuleView,
}; };
} }
@ -44,31 +117,194 @@ class StackedRouter extends _i1.RouterBase {
Routes.startupView, Routes.startupView,
page: _i4.StartupView, page: _i4.StartupView,
), ),
_i1.RouteDef(
Routes.profileView,
page: _i5.ProfileView,
),
_i1.RouteDef(
Routes.profileDetailView,
page: _i6.ProfileDetailView,
),
_i1.RouteDef(
Routes.downloadsView,
page: _i7.DownloadsView,
),
_i1.RouteDef(
Routes.progressView,
page: _i8.ProgressView,
),
_i1.RouteDef(
Routes.ongoingProgressView,
page: _i9.OngoingProgressView,
),
_i1.RouteDef(
Routes.accountPrivacyView,
page: _i10.AccountPrivacyView,
),
_i1.RouteDef(
Routes.supportView,
page: _i11.SupportView,
),
_i1.RouteDef(
Routes.telegramSupportView,
page: _i12.TelegramSupportView,
),
_i1.RouteDef(
Routes.callSupportView,
page: _i13.CallSupportView,
),
_i1.RouteDef(
Routes.languageView,
page: _i14.LanguageView,
),
_i1.RouteDef(
Routes.privacyPolicyView,
page: _i15.PrivacyPolicyView,
),
_i1.RouteDef(
Routes.termsAndConditionsView,
page: _i16.TermsAndConditionsView,
),
_i1.RouteDef(
Routes.registerView,
page: _i17.RegisterView,
),
_i1.RouteDef(
Routes.loginView,
page: _i18.LoginView,
),
_i1.RouteDef(
Routes.learnView,
page: _i19.LearnView,
),
_i1.RouteDef(
Routes.learnLevelView,
page: _i20.LearnLevelView,
),
_i1.RouteDef(
Routes.learnModuleView,
page: _i21.LearnModuleView,
),
]; ];
final _pagesMap = <Type, _i1.StackedRouteFactory>{ final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) { _i2.HomeView: (data) {
return _i6.MaterialPageRoute<dynamic>( return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(), builder: (context) => const _i2.HomeView(),
settings: data, settings: data,
); );
}, },
_i3.OnboardingView: (data) { _i3.OnboardingView: (data) {
return _i6.MaterialPageRoute<dynamic>( return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(), builder: (context) => const _i3.OnboardingView(),
settings: data, settings: data,
); );
}, },
_i4.StartupView: (data) { _i4.StartupView: (data) {
return _i6.MaterialPageRoute<dynamic>( return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i4.StartupView(), builder: (context) => const _i4.StartupView(),
settings: data, settings: data,
); );
}, },
_i5.LanguageSelector: (data) { _i5.ProfileView: (data) {
return _i6.MaterialPageRoute<dynamic>( return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.LanguageSelector(), builder: (context) => const _i5.ProfileView(),
settings: data,
);
},
_i6.ProfileDetailView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(),
settings: data,
);
},
_i7.DownloadsView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(),
settings: data,
);
},
_i8.ProgressView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(),
settings: data,
);
},
_i9.OngoingProgressView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(),
settings: data,
);
},
_i10.AccountPrivacyView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(),
settings: data,
);
},
_i11.SupportView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(),
settings: data,
);
},
_i12.TelegramSupportView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(),
settings: data,
);
},
_i13.CallSupportView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(),
settings: data,
);
},
_i14.LanguageView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(),
settings: data,
);
},
_i15.PrivacyPolicyView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(),
settings: data,
);
},
_i16.TermsAndConditionsView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(),
settings: data,
);
},
_i17.RegisterView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(),
settings: data,
);
},
_i18.LoginView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(),
settings: data,
);
},
_i19.LearnView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(),
settings: data,
);
},
_i20.LearnLevelView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(),
settings: data,
);
},
_i21.LearnModuleView: (data) {
return _i22.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(),
settings: data, settings: data,
); );
}, },
@ -81,7 +317,7 @@ class StackedRouter extends _i1.RouterBase {
Map<Type, _i1.StackedRouteFactory> get pagesMap => _pagesMap; Map<Type, _i1.StackedRouteFactory> get pagesMap => _pagesMap;
} }
extension NavigatorStateExtension on _i7.NavigationService { extension NavigatorStateExtension on _i23.NavigationService {
Future<dynamic> navigateToHomeView([ Future<dynamic> navigateToHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -124,7 +360,243 @@ extension NavigatorStateExtension on _i7.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToProfileView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.profileView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToProfileDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.profileDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToDownloadsView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.downloadsView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToProgressView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.progressView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToOngoingProgressView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.ongoingProgressView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToAccountPrivacyView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.accountPrivacyView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToSupportView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.supportView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToTelegramSupportView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.telegramSupportView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToCallSupportView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.callSupportView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLanguageView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.languageView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToPrivacyPolicyView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.privacyPolicyView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToTermsAndConditionsView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.termsAndConditionsView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToRegisterView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.registerView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLoginView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.loginView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnLevelView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnLevelView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnModuleView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnModuleView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([ Future<dynamic> replaceWithHomeView([
int? routerId, int? routerId,
@ -168,5 +640,241 @@ extension NavigatorStateExtension on _i7.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithProfileView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.profileView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithProfileDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.profileDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithDownloadsView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.downloadsView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithProgressView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.progressView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithOngoingProgressView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.ongoingProgressView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithAccountPrivacyView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.accountPrivacyView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithSupportView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.supportView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithTelegramSupportView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.telegramSupportView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithCallSupportView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.callSupportView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLanguageView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.languageView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithPrivacyPolicyView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.privacyPolicyView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithTermsAndConditionsView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.termsAndConditionsView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithRegisterView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.registerView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLoginView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.loginView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnLevelView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnLevelView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnModuleView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnModuleView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toastification/toastification.dart';
import 'package:yimaru_app/app/app.bottomsheets.dart'; import 'package:yimaru_app/app/app.bottomsheets.dart';
import 'package:yimaru_app/app/app.dialogs.dart'; import 'package:yimaru_app/app/app.dialogs.dart';
import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.locator.dart';
@ -17,13 +18,17 @@ class MainApp extends StatelessWidget {
const MainApp({super.key}); const MainApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => _buildMaterialWrapper();
return MaterialApp(
Widget _buildMaterialWrapper() => ToastificationWrapper(
child: _buildMaterialApp(),
);
Widget _buildMaterialApp() => MaterialApp(
initialRoute: Routes.startupView, initialRoute: Routes.startupView,
theme: ThemeData(fontFamily: 'Aeonik'), theme: ThemeData(fontFamily: 'Aeonik'),
onGenerateRoute: StackedRouter().onGenerateRoute, onGenerateRoute: StackedRouter().onGenerateRoute,
navigatorKey: StackedService.navigatorKey, navigatorKey: StackedService.navigatorKey,
navigatorObservers: [StackedService.routeObserver], navigatorObservers: [StackedService.routeObserver],
); );
}
} }

View File

@ -0,0 +1,22 @@
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';
@JsonSerializable()
class UserModel {
@JsonKey(name: 'user_id')
final int? userId;
@JsonKey(name: 'access_token')
final String? accessToken;
@JsonKey(name: 'refresh_token')
final String? refreshToken;
UserModel({this.userId, this.accessToken, this.refreshToken});
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
}

View File

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
userId: (json['user_id'] as num?)?.toInt(),
accessToken: json['access_token'] as String?,
refreshToken: json['refresh_token'] as String?,
);
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'user_id': instance.userId,
'access_token': instance.accessToken,
'refresh_token': instance.refreshToken,
};

View File

@ -0,0 +1,171 @@
import 'package:dio/dio.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import '../app/app.locator.dart';
import '../ui/common/enmus.dart';
class ApiService {
final _service = locator<DioService>();
// Http headers
Map<String, dynamic> _getHeaders({String? token}) => {
// if (token != null) 'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8',
if (token != null) 'Authorization': 'Bearer $token'
};
// Dio options
Options? _getOptions({String? token}) {
return Options(
// followRedirects: false,
// validateStatus: (status) => true,
headers: _getHeaders(token: token),
);
}
// Register
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$baseUrl/$userUrl/$kRegisterUrl',
data: data,
options: _getOptions(),
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Otp sent successfully'
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
};
}
}
// Login
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$baseUrl/$kLoginUrl',
data: data,
options: _getOptions(),
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Logged in successfully',
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
};
}
}
// Verify otp
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$baseUrl/$userUrl/$kVerifyOtpUrl',
data: data,
options: _getOptions(),
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Otp verified successfully',
//'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
};
}
}
// Resend otp
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$baseUrl/$userUrl/$kResendOtpUrl',
data: data,
options: _getOptions(),
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Otp resend successfully'
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
};
}
}
// Profile completed
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
try {
Response response = await _service.dio.get(
'$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl',
options: _getOptions(token: user?.accessToken),
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Profile completion status fetched successfully',
'data': response.data['data']['is_profile_completed'] as bool,
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
};
}
}
}

View File

@ -0,0 +1,32 @@
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
class AuthenticationService {
final _secureService = locator<SecureStorageService>();
Future<bool> userLoggedIn() async {
if (await _secureService.getString('userId') != null) {
return true;
}
return false;
}
Future<void> saveUserData(Map<String, dynamic> data) async {
await _secureService.setInt('userId', data['userId']);
await _secureService.setString('accessToken', data['accessToken']);
await _secureService.setString('refreshToken', data['refreshToken']);
}
Future<UserModel> getUser() async {
UserModel user = UserModel(
userId: await _secureService.getInt('userId'),
accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'));
return user;
}
Future<void> logOut() async {
await _secureService.clear();
}
}

View File

@ -0,0 +1,41 @@
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import '../ui/common/app_constants.dart';
class DioService {
final Dio _dio = Dio();
DioService() {
_dio.options.baseUrl = baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 30);
_dio.options.receiveTimeout = const Duration(seconds: 30);
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
debugPrint('➡️➡️➡️ REQUEST ➡️➡️➡️');
debugPrint('➡️ Data: ${options.data}');
debugPrint('➡️ Headers: ${options.headers}');
debugPrint('➡️ ${options.method} ${options.uri}');
handler.next(options);
},
onResponse: (response, handler) {
debugPrint('✅✅✅ RESPONSE ✅✅✅');
debugPrint('✅ Data : ${response.data}');
debugPrint('✅ Status Code : ${response.statusCode}');
handler.next(response);
},
onError: (error, handler) {
debugPrint('❌❌❌ ERROR ❌❌❌');
debugPrint('${error.message}');
debugPrint('❌ URI: ${error.requestOptions.uri}');
debugPrint('❌ Headers sent: ${error.requestOptions.headers}');
handler.next(error);
},
),
);
}
Dio get dio => _dio;
}

View File

@ -0,0 +1,64 @@
import 'dart:io';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
extension BoolParsing on String {
bool parseBool() {
if (toLowerCase() == 'true') {
return true;
} else if (toLowerCase() == 'false') {
return false;
}
throw '"$this" can not be parsed to boolean.';
}
}
class SecureStorageService {
// Create storage
late final FlutterSecureStorage _storage;
SecureStorageService() {
_storage = Platform.isAndroid
? const FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
)
: const FlutterSecureStorage(
iOptions: IOSOptions(),
);
}
Future<void> clear() async {
_storage.deleteAll();
}
Future<bool?> getBool(String key) async {
String? result = await _storage.read(key: key);
return result?.parseBool();
}
Future<String?> getString(String key) async {
return await _storage.read(key: key);
}
Future<int?> getInt(String key) async {
return await _storage.read(key: key) == null
? null
: int.parse(await _storage.read(key: key) ?? '0');
}
Future<void> setString(String key, String value) async {
await _storage.write(key: key, value: value);
}
Future<void> setInt(String key, int value) async {
await _storage.write(key: key, value: value.toString());
}
Future<void> setBool(String key, bool value) async {
await _storage.write(key: key, value: value.toString());
}
}

View File

@ -1,10 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const Color kcBackgroundColor = kcWhiteColor; const Color kcBlack = Colors.black;
const Color kcWhiteColor = Color(0xFFFFFFFF); const Color kcRed = Color(0xffFF4C4C);
const Color kcGreen = Color(0xFF1DE964);
const Color kcBackgroundColor = kcWhite;
const Color kcWhite = Color(0xFFFFFFFF);
const Color kcIndigo = Color(0xff6A1B9A);
const Color kcOrange = Color(0xFFF79400);
const Color kcSkyBlue = Color(0xFF28B4CD);
const Color kcDarkGrey = Color(0xFF1A1B1E);
const Color kcMediumGrey = Color(0xFF474A54); const Color kcMediumGrey = Color(0xFF474A54);
const Color kcAquamarine = Color(0xFF1DE9B6);
const Color kcTransparent = Colors.transparent;
const Color kcPrimaryColor = Color(0xFF9E2891); const Color kcPrimaryColor = Color(0xFF9E2891);
const Color kcDarkGreyColor = Color(0xFF1A1B1E); const Color kcPrimaryAccent = Color(0xFF6A1B9A);
const Color kcVeryLightGrey = Color(0xFFE3E3E3); const Color kcVeryLightGrey = Color(0xFFE3E3E3);
const Color kcPrimaryColorDark = Color(0xFF300151); const Color kcPrimaryColorDark = Color(0xFF300151);
const Color kcPrimaryColorLight = Color(0x149E2891); const Color kcPrimaryColorLight = Color(0x149E2891);

View File

@ -0,0 +1,14 @@
//String baseUrl = 'http://195.35.29.82:8080';
String baseUrl = 'https://api.yimaru.yaltopia.com';
String userUrl = 'api/v1/user';
String kRegisterUrl = 'register';
String kVerifyOtpUrl = 'verify-otp';
String kResendOtpUrl = 'resend-otp';
String kLoginUrl = 'api/v1/auth/customer-login';
String kProfileStatusUrl = 'is-profile-completed';

View File

@ -1,3 +1,54 @@
const String ksSuggestion =
"15 minutes a day can make you 3x more fluent in 3 month";
const String ksHomeBottomSheetTitle = 'Build Great Apps!'; const String ksHomeBottomSheetTitle = 'Build Great Apps!';
const String ksPrivacyPolicy =
'A brief, simple overview of Yimarus commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.';
const String ksHomeBottomSheetDescription = const String ksHomeBottomSheetDescription =
'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more'; 'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more';
const String ksTerms = """
<p style="color:#9C2C91;font-size:13px;">
Last updated: October 26, 2025
</p>
<h2>Introduction</h2>
<p>
Welcome to Yimaru! These terms and conditions outline the rules and regulations
for the use of our application. By accessing this app, we assume you accept
these terms and conditions.
</p>
<h2>User Accounts</h2>
<p>
When you create an account with us, you must provide us with information that is
accurate, complete, and current at all times. Failure to do so constitutes a
breach of the Terms, which may result in immediate termination of your account
on our Service.
</p>
<h2>Content & Services</h2>
<p>
Our Service allows you to access learning materials. You are granted a limited
license to access and use the app content for personal, non-commercial purposes.
You agree not to:
</p>
<ul>
<li>Reproduce, duplicate, copy, or sell any material from the app.</li>
<li>Redistribute content from Yimaru.</li>
<li>Use the app in any way that is damaging or harmful.</li>
</ul>
<h2>Privacy Policy</h2>
<p>
Your privacy is important to us. Please read our
<a href="#">Privacy Policy</a>
to understand how we collect, use, and share information about you.
</p>
<h2>Contact Us</h2>
<p>
If you have any questions about these Terms, please contact us at
<strong>support@yimaru.et</strong>.
</p>
""";

View File

@ -0,0 +1,7 @@
// Registration type
enum RegistrationType { phone, email }
// Report status
enum ResponseStatus { success, failure }
enum LearnLevelStatus { pending, started, completed }

View File

@ -1,6 +1,9 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter_html/flutter_html.dart';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinput/pinput.dart';
import 'package:toastification/toastification.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
const double _tinySize = 5.0; const double _tinySize = 5.0;
@ -34,6 +37,9 @@ double screenWidth(BuildContext context) => MediaQuery.of(context).size.width;
double screenHeight(BuildContext context) => MediaQuery.of(context).size.height; double screenHeight(BuildContext context) => MediaQuery.of(context).size.height;
double buttonAlignment(BuildContext context) =>
MediaQuery.of(context).size.height * 0.9;
double screenHeightFraction( double screenHeightFraction(
BuildContext context, { BuildContext context, {
int dividedBy = 1, int dividedBy = 1,
@ -92,18 +98,20 @@ double getResponsiveFontSize(
return responsiveSize; return responsiveSize;
} }
InputDecoration inputDecoration({ InputDecoration inputDecoration(
String? hint, {String? hint,
Widget? suffix,
required bool focus, required bool focus,
}) => required bool filled}) =>
InputDecoration( InputDecoration(
hintText: hint,
filled: true, filled: true,
hintText: hint,
border: border, border: border,
suffixIcon: suffix,
errorBorder: errorBorder, errorBorder: errorBorder,
enabledBorder: enabledBorder, enabledBorder: enabledBorder,
focusedBorder: focusedBorder, focusedBorder: focusedBorder,
fillColor: focus ? kcPrimaryColor.withOpacity(0.2) : kcWhiteColor, fillColor: focus || filled ? kcPrimaryColor.withOpacity(0.1) : kcWhite,
); );
Border rightBorder = Border( Border rightBorder = Border(
@ -113,6 +121,8 @@ Border rightBorder = Border(
), ),
); );
DateFormat format = DateFormat("d MMM, yyyy");
OutlineInputBorder border = OutlineInputBorder border =
const OutlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor)); const OutlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor));
OutlineInputBorder errorBorder = OutlineInputBorder errorBorder =
@ -124,3 +134,142 @@ OutlineInputBorder focusedBorder =
UnderlineInputBorder searchBorder = UnderlineInputBorder searchBorder =
const UnderlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor)); const UnderlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor));
TextStyle defaultPinTextStyle = const TextStyle(
fontSize: 22,
color: kcDarkGrey,
);
BoxDecoration defaultPinDecoration = BoxDecoration(
borderRadius: BorderRadius.circular(19),
border: Border.all(
color: kcPrimaryColor.withOpacity(0.5),
),
);
PinTheme defaultPin = PinTheme(
width: 56,
height: 56,
textStyle: defaultPinTextStyle,
decoration: defaultPinDecoration,
);
PinTheme focusedThemePin = defaultPin.copyWith(
decoration: defaultPin.decoration?.copyWith(
border: Border.all(color: kcPrimaryColor, width: 3),
),
);
PinTheme submittedThemePin = defaultPin.copyWith(
decoration: defaultPin.decoration?.copyWith(
borderRadius: BorderRadius.circular(19),
border: Border.all(color: kcPrimaryColor),
),
);
PinTheme errorPinTheme = defaultPin.copyBorderWith(
border: Border.all(color: Colors.red),
);
TextStyle validationStyle = const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
);
TextStyle style25DG600 = const TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
);
TextStyle style16DG600 = const TextStyle(
fontSize: 16,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
);
TextStyle style16DG400 = const TextStyle(
fontSize: 16,
color: kcDarkGrey,
);
TextStyle style14DG400 = const TextStyle(
color: kcDarkGrey,
);
TextStyle style14P400 = const TextStyle(
color: kcPrimaryColor,
);
TextStyle style14P600 = const TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
Map<String, Style> htmlStyle = {
"p": htmlDefaultStyle,
"h1": htmlDefaultStyle,
"h2": htmlDefaultStyle,
"h3": htmlDefaultStyle,
"h4": htmlDefaultStyle,
"h5": htmlDefaultStyle,
"h6": htmlDefaultStyle,
"li": Style(
color: kcDarkGrey,
margin: Margins.zero,
fontSize: FontSize(16),
padding: HtmlPaddings.zero,
fontWeight: FontWeight.w400,
listStyleType: ListStyleType.circle,
verticalAlign: VerticalAlign.baseline,
),
};
Widget buildToastDescription(String message) => Text(
message,
maxLines: 4,
style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500),
);
void showErrorToast(String message) {
toastification.show(
showIcon: true,
dragToClose: true,
primaryColor: kcRed,
showProgressBar: false,
applyBlurEffect: false,
icon: const Icon(Icons.check),
type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored,
description: buildToastDescription(message),
autoCloseDuration: const Duration(seconds: 10),
margin: const EdgeInsets.symmetric(horizontal: 15),
);
}
void showSuccessToast(String message) {
toastification.show(
showIcon: true,
dragToClose: true,
showProgressBar: false,
applyBlurEffect: false,
icon: const Icon(Icons.check),
primaryColor: kcPrimaryColor,
type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored,
description: buildToastDescription(message),
autoCloseDuration: const Duration(seconds: 10),
margin: const EdgeInsets.symmetric(horizontal: 15),
);
}

View File

@ -0,0 +1,64 @@
import 'package:email_validator/email_validator.dart';
class FormValidator {
static String? validateForm(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
return null;
}
static String? validatePhoneNumber(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
// Regex validation
final regex = RegExp(r'^251');
if (!regex.hasMatch(value)) {
return 'Phone number must start with 251';
}
// Length check first (optional but recommended)
if (value.length != 12) {
return 'Phone number must be 12 digits';
}
return null;
}
static String? validateEmail(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
if (!EmailValidator.validate(value)) {
return 'Invalid email format';
}
return null;
}
static String? validatePassword(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
return null;
}
}

View File

@ -1,12 +0,0 @@
class OnboardingFormValidator {
static String? validateForm(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
return null;
}
}

View File

@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/custom_list_tile.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'account_privacy_viewmodel.dart';
class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
const AccountPrivacyView({Key? key}) : super(key: key);
@override
AccountPrivacyViewModel viewModelBuilder(
BuildContext context,
) =>
AccountPrivacyViewModel();
@override
Widget builder(
BuildContext context,
AccountPrivacyViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(AccountPrivacyViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(AccountPrivacyViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(AccountPrivacyViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(AccountPrivacyViewModel viewModel) =>
_buildColumn(viewModel);
Widget _buildColumn(AccountPrivacyViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(AccountPrivacyViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
verticalSpaceSmall,
_buildContentWrapper(viewModel)
];
Widget _buildAppBarWrapper(AccountPrivacyViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
title: 'Account Privacy',
onTap: viewModel.pop,
);
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
Expanded(child: _buildContentColumnWrapper(viewModel));
Widget _buildContentColumnWrapper(AccountPrivacyViewModel viewModel) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildContentColumn(viewModel),
);
Widget _buildContentColumn(AccountPrivacyViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildContentChildren(viewModel),
);
List<Widget> _buildContentChildren(AccountPrivacyViewModel viewModel) =>
[_buildMenuColumnScrollView(viewModel), _buildDeleteButtonWrapper()];
Widget _buildMenuColumnScrollView(AccountPrivacyViewModel viewModel) =>
SingleChildScrollView(
child: _buildMenuColumn(viewModel),
);
Widget _buildMenuColumn(AccountPrivacyViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildMenuColumnChildren(viewModel),
);
List<Widget> _buildMenuColumnChildren(AccountPrivacyViewModel viewModel) => [
verticalSpaceLarge,
_buildHeader('App Settings'),
verticalSpaceSmall,
_buildLanguageMenu(viewModel),
_buildDividerWrapper(),
verticalSpaceMedium,
_buildHeader('Legal & Information'),
verticalSpaceSmall,
_buildTermsAndConditionsMenu(viewModel),
_buildPrivacyPolicy(viewModel),
_buildDividerWrapper(),
];
Widget _buildHeader(String title) => Text(
title,
style: const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
CustomListTile(
isLanguage: true,
language: 'English',
icon: Icons.language,
title: 'Change Language',
onTap: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTermsAndConditionsMenu(AccountPrivacyViewModel viewModel) =>
CustomListTile(
icon: Icons.handshake,
title: 'Terms & Conditions',
onTap: () async => await viewModel.navigateToTerms(),
);
Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) =>
CustomListTile(
icon: Icons.shield_moon_outlined,
title: 'Privacy Policy',
onTap: () async => await viewModel.navigateToPrivacyPolicy(),
);
Widget _buildDividerWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: _buildDivider(),
);
Widget _buildDivider() => const Divider(color: kcVeryLightGrey);
Widget _buildDeleteButtonWrapper() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildDeleteButton(),
);
Widget _buildDeleteButton() => CustomElevatedButton(
height: 55,
text: 'Delete Account',
borderRadius: 12,
foregroundColor: kcRed,
backgroundColor: kcRed.withOpacity(0.25),
);
}

View File

@ -0,0 +1,21 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
class AccountPrivacyViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToPrivacyPolicy() async =>
await _navigationService.navigateToPrivacyPolicyView();
Future<void> navigateToTerms() async =>
await _navigationService.navigateToTermsAndConditionsView();
}

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/circular_icon.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'call_support_viewmodel.dart';
class CallSupportView extends StackedView<CallSupportViewModel> {
const CallSupportView({Key? key}) : super(key: key);
@override
CallSupportViewModel viewModelBuilder(
BuildContext context,
) =>
CallSupportViewModel();
@override
Widget builder(
BuildContext context,
CallSupportViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CallSupportViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CallSupportViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(CallSupportViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(CallSupportViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
_buildExpandedColumn(viewModel)
];
Widget _buildAppBarWrapper(CallSupportViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar(
title: 'Call Support',
onTap: viewModel.pop,
);
Widget _buildExpandedColumn(CallSupportViewModel viewModel) =>
Expanded(child: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper(CallSupportViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CallSupportViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(CallSupportViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(CallSupportViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(CallSupportViewModel viewModel) => [
verticalSpaceLarge,
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubTitle('+2519012345678'),
verticalSpaceSmall,
_buildSubTitle('+2519012345678'),
];
Widget _buildIcon() =>
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
Widget _buildTitle() => const Text(
'Call our support team between 9 AM - 6 PM',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitle(String title) => Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(color: kcPrimaryColor),
);
Widget _buildContinueButtonWrapper(CallSupportViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(CallSupportViewModel viewModel) =>
const CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Tap to Call',
leadingIcon: Icons.call,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,9 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class CallSupportViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/download_card.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'downloads_viewmodel.dart';
class DownloadsView extends StackedView<DownloadsViewModel> {
const DownloadsView({Key? key}) : super(key: key);
@override
DownloadsViewModel viewModelBuilder(
BuildContext context,
) =>
DownloadsViewModel();
@override
Widget builder(
BuildContext context,
DownloadsViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(DownloadsViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(DownloadsViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(DownloadsViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(DownloadsViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(DownloadsViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(DownloadsViewModel viewModel) => [
verticalSpaceMedium,
_buildAppbar(viewModel),
_buildContentWrapper(viewModel)
];
Widget _buildAppbar(DownloadsViewModel viewModel) => SmallAppBar(
title: 'Offline Downloads',
onTap: viewModel.pop,
);
Widget _buildContentWrapper(DownloadsViewModel viewModel) =>
viewModel.showDownload
? _buildEmptyContent(viewModel)
: _buildContent(viewModel);
Widget _buildContent(DownloadsViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildContentChildren(viewModel),
);
List<Widget> _buildContentChildren(DownloadsViewModel viewModel) => [
verticalSpaceMedium,
_buildStorageSection(viewModel),
verticalSpaceLarge,
_buildDownloads(viewModel)
];
Widget _buildStorageSection(DownloadsViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildStorageSectionChildren(viewModel),
);
List<Widget> _buildStorageSectionChildren(DownloadsViewModel viewModel) => [
_buildStorageInfoWrapper(viewModel),
_buildStorageIndicator(),
];
Widget _buildStorageInfoWrapper(DownloadsViewModel viewModel) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildStorageInfoChildren(viewModel),
);
List<Widget> _buildStorageInfoChildren(DownloadsViewModel viewModel) =>
[_buildStorageInfo(), _buildManageButton(viewModel)];
Widget _buildStorageInfo() => const Text.rich(
TextSpan(
text: '1.2GB',
style: TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
children: [
TextSpan(
text: ' used of 2GB',
style: TextStyle(
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
)
]),
);
Widget _buildManageButton(DownloadsViewModel viewModel) => TextButton(
onPressed: viewModel.setShowDownload, child: _buildManageText());
Widget _buildManageText() => const Text(
'Manage Storage',
style: TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
);
Widget _buildStorageIndicator() => const CustomLinearProgressIndicator(
progress: 0.75,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
);
Widget _buildDownloads(DownloadsViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.downloads.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildDownload(
size: viewModel.downloads[index]['size'],
title: viewModel.downloads[index]['title'],
duration: viewModel.downloads[index]['duration'],
thumbnail: viewModel.downloads[index]['thumbnail']),
);
Widget _buildDownload(
{required String title,
required String size,
required String duration,
required String thumbnail}) =>
DownloadCard(
size: size,
title: title,
duration: duration,
thumbnail: thumbnail,
);
Widget _buildEmptyContent(DownloadsViewModel viewModel) => Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildEmptyContentChildren(viewModel),
),
);
List<Widget> _buildEmptyContentChildren(DownloadsViewModel viewModel) =>
[_buildUpperEmptyContent(viewModel), _buildGoButtonWrapper(viewModel)];
Widget _buildUpperEmptyContent(DownloadsViewModel viewModel) => Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildUpperEmptyContentChildren(viewModel),
),
);
List<Widget> _buildUpperEmptyContentChildren(DownloadsViewModel viewModel) =>
[
verticalSpaceMassive,
_buildEmptyIcon(),
verticalSpaceMedium,
_buildEmptyTitle(),
verticalSpaceSmall,
_buildEmptySubTitle(),
];
Widget _buildEmptyIcon() => const Icon(
Icons.hourglass_empty,
size: 100,
color: kcPrimaryColor,
);
Widget _buildEmptyTitle() => const Text(
'Looking for something to download?',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildEmptySubTitle() => const Text(
'Start by exploring your learning materials and save them for offline access.',
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildGoButtonWrapper(DownloadsViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildGoButton(viewModel),
);
Widget _buildGoButton(DownloadsViewModel viewModel) => CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Go to Learn Section',
onTap: viewModel.setShowDownload,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,42 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class DownloadsViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
bool _showDownload = false;
bool get showDownload => _showDownload;
// Downloads
final List<Map<String, dynamic>> _downloads = [
{
'size': '120 MB',
'duration': '3h 46 m',
'title': 'Duolingo English',
'thumbnail': 'assets/images/image_1.png',
},
{
'size': '79 MB',
'duration': '1h 34 m',
'title': 'IELTS Listening',
'thumbnail': 'assets/images/image_1.png',
},
{
'size': '120 MB',
'duration': '3h 46 m',
'title': 'Customer Service',
'thumbnail': 'assets/images/image_1.png',
},
];
List<Map<String, dynamic>> get downloads => _downloads;
void setShowDownload() {
_showDownload = !_showDownload;
rebuildUi();
}
void pop() => _navigationService.back();
}

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/views/learn/learn_view.dart';
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
import 'package:yimaru_app/ui/widgets/coming_soon.dart';
import 'home_viewmodel.dart'; import 'home_viewmodel.dart';
@ -9,66 +11,61 @@ class HomeView extends StackedView<HomeViewModel> {
const HomeView({Key? key}) : super(key: key); const HomeView({Key? key}) : super(key: key);
@override @override
Widget builder(BuildContext context, HomeViewModel viewModel, Widget? child) { HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel();
return Scaffold(
body: SafeArea( @override
child: Padding( void onViewModelReady(HomeViewModel viewModel) {
padding: const EdgeInsets.symmetric(horizontal: 25.0), viewModel.getProfileStatus();
child: Center( super.onViewModelReady(viewModel);
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
verticalSpaceLarge,
Column(
children: [
const Text(
'Hello, STACKED!',
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.w900,
),
),
verticalSpaceMedium,
MaterialButton(
color: Colors.black,
onPressed: viewModel.incrementCounter,
child: Text(
viewModel.counterLabel,
style: const TextStyle(color: Colors.white),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MaterialButton(
color: kcDarkGreyColor,
onPressed: viewModel.showDialog,
child: const Text(
'Show Dialog',
style: TextStyle(color: Colors.white),
),
),
MaterialButton(
color: kcDarkGreyColor,
onPressed: viewModel.showBottomSheet,
child: const Text(
'Show Bottom Sheet',
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
),
),
);
} }
@override @override
HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel(); Widget builder(
BuildContext context, HomeViewModel viewModel, Widget? child) =>
_buildScaffold(viewModel);
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(
body: getViewForIndex(viewModel.currentIndex),
bottomNavigationBar: BottomNavigationBar(
onTap: viewModel.setCurrentIndex,
items: _buildNavBarItems(),
selectedItemColor: kcPrimaryColor,
backgroundColor: kcBackgroundColor,
type: BottomNavigationBarType.fixed,
currentIndex: viewModel.currentIndex,
),
);
List<BottomNavigationBarItem> _buildNavBarItems() => [
_buildLearnItem(),
_buildCourseItem(),
_buildProfileItem(),
];
BottomNavigationBarItem _buildLearnItem() => const BottomNavigationBarItem(
label: 'Learn',
icon: Icon(Icons.school),
);
BottomNavigationBarItem _buildCourseItem() => const BottomNavigationBarItem(
label: 'Course',
icon: Icon(Icons.book),
);
BottomNavigationBarItem _buildProfileItem() => const BottomNavigationBarItem(
label: 'Profile',
icon: Icon(Icons.person),
);
}
Widget getViewForIndex(int index) {
switch (index) {
case 0:
return const LearnView();
case 1:
return const ComingSoon();
default:
return const ProfileView();
}
} }

View File

@ -1,20 +1,30 @@
import 'package:yimaru_app/app/app.bottomsheets.dart'; import 'package:yimaru_app/app/app.bottomsheets.dart';
import 'package:yimaru_app/app/app.dialogs.dart'; import 'package:yimaru_app/app/app.dialogs.dart';
import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../common/enmus.dart';
class HomeViewModel extends BaseViewModel { class HomeViewModel extends BaseViewModel {
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>(); final _dialogService = locator<DialogService>();
final _navigationService = locator<NavigationService>();
final _bottomSheetService = locator<BottomSheetService>(); final _bottomSheetService = locator<BottomSheetService>();
final _authenticationService = locator<AuthenticationService>();
String get counterLabel => 'Counter is: $_counter'; // Bottom navigation
int _currentIndex = 0;
int _counter = 0; int get currentIndex => _currentIndex;
void incrementCounter() { void setCurrentIndex(int index) {
_counter++; _currentIndex = index;
rebuildUi(); rebuildUi();
} }
@ -22,7 +32,7 @@ class HomeViewModel extends BaseViewModel {
_dialogService.showCustomDialog( _dialogService.showCustomDialog(
variant: DialogType.infoAlert, variant: DialogType.infoAlert,
title: 'Stacked Rocks!', title: 'Stacked Rocks!',
description: 'Give stacked $_counter stars on Github', description: 'Give stacked stars on Github',
); );
} }
@ -33,4 +43,20 @@ class HomeViewModel extends BaseViewModel {
description: ksHomeBottomSheetDescription, description: ksHomeBottomSheetDescription,
); );
} }
// Navigation
Future<void> replaceWithOnboarding() async =>
await _navigationService.replaceWithOnboardingView();
// Remote api calls
Future<void> getProfileStatus() async {
UserModel user = await _authenticationService.getUser();
Map<String, dynamic> response = await runBusyFuture<Map<String, dynamic>>(
_apiService.getProfileStatus(user));
if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding();
}
}
} }

View File

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_small_radio_button.dart';
import '../../widgets/small_app_bar.dart';
import 'language_viewmodel.dart';
class LanguageView extends StackedView<LanguageViewModel> {
const LanguageView({Key? key}) : super(key: key);
@override
LanguageViewModel viewModelBuilder(
BuildContext context,
) =>
LanguageViewModel();
@override
Widget builder(
BuildContext context,
LanguageViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LanguageViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LanguageViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(LanguageViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LanguageViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
_buildExpandedBody(viewModel)
];
Widget _buildExpandedBody(LanguageViewModel viewModel) =>
Expanded(child: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper(LanguageViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LanguageViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(LanguageViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
verticalSpaceMedium,
_buildLanguages(viewModel)
];
Widget _buildAppBarWrapper(LanguageViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
title: 'Language Preference',
onTap: viewModel.pop,
);
Widget _buildTitle() => const Text(
'Choose your language',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitle() => const Text(
'You can switch languages anytime',
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.languages.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildLanguage(
title: viewModel.languages[index]['language'],
selected: viewModel
.isSelectedLanguage(viewModel.languages[index]['language']),
onTap: () =>
viewModel.setSelectedLanguage(viewModel.languages[index]),
),
);
Widget _buildLanguage(
{required String title,
required bool selected,
required GestureTapCallback onTap}) =>
CustomSmallRadioButton(
title: title,
onTap: onTap,
selected: selected,
);
}

View File

@ -0,0 +1,35 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class LanguageViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Languages
Map<String, dynamic> _selectedLanguage = {
'code': 'EN',
'language': 'English'
};
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
final List<Map<String, dynamic>> _languages = [
{'code': 'አማ', 'language': 'አማርኛ'},
{'code': 'EN', 'language': 'English'},
];
List<Map<String, dynamic>> get languages => _languages;
// Languages
void setSelectedLanguage(Map<String, dynamic> title) {
_selectedLanguage = title;
rebuildUi();
}
bool isSelectedLanguage(String title) =>
_selectedLanguage['language'] == title;
// Navigation
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/learn_app_bar.dart';
import 'package:yimaru_app/ui/widgets/learn_level_tile.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import 'learn_viewmodel.dart';
class LearnView extends StackedView<LearnViewModel> {
const LearnView({Key? key}) : super(key: key);
@override
LearnViewModel viewModelBuilder(BuildContext context) => LearnViewModel();
@override
Widget builder(
BuildContext context,
LearnViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(LearnViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(),
_buildLevelsColumnWrapper(viewModel)
],
);
Widget _buildAppBar() => const LearnAppBar();
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(LearnViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(LearnViewModel viewModel) =>
[verticalSpaceLarge, _buildListView(viewModel)];
Widget _buildListView(LearnViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.learnLevels.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
title: viewModel.learnLevels[index]['title'],
status: viewModel.learnLevels[index]['status'],
subtitle: viewModel.learnLevels[index]['subtitle']),
);
Widget _buildTile(
{required String title,
required String subtitle,
required LearnLevelStatus status}) =>
LearnLevelTile(
title: title,
status: status,
subtitle: subtitle,
);
}

View File

@ -0,0 +1,33 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
class LearnViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final List<Map<String, dynamic>> _learnLevels = [
{
'title': 'Beginner',
'status': LearnLevelStatus.completed,
'subtitle': 'Start your journey with the basics of English.',
},
{
'title': 'Intermediate',
'status': LearnLevelStatus.started,
'subtitle': 'Practice real conversations and expand vocabulary.',
},
{
'title': 'Advanced',
'status': LearnLevelStatus.pending,
'subtitle': 'Achieve fluency and master complex topics.',
},
];
List<Map<String, dynamic>> get learnLevels => _learnLevels;
Future<void> navigateToLearnLevel() async =>
_navigationService.navigateToLearnLevelView();
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/learn_sub_level_tile.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import 'learn_level_viewmodel.dart';
class LearnLevelView extends StackedView<LearnLevelViewModel> {
const LearnLevelView({Key? key}) : super(key: key);
@override
LearnLevelViewModel viewModelBuilder(BuildContext context) =>
LearnLevelViewModel();
@override
Widget builder(
BuildContext context,
LearnLevelViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnLevelViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnLevelViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(LearnLevelViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnLevelViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildLevelsColumnWrapper(viewModel)
],
);
Widget _buildAppBar(LearnLevelViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
);
Widget _buildLevelsColumnWrapper(LearnLevelViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnLevelViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(LearnLevelViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(LearnLevelViewModel viewModel) =>
[verticalSpaceLarge, _buildListView(viewModel)];
Widget _buildListView(LearnLevelViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.learnSubLevels.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
title: viewModel.learnSubLevels[index]['title'],
current: viewModel.learnSubLevels[index]['current'],
subtitle: viewModel.learnSubLevels[index]['subtitle']),
);
Widget _buildTile({
required String title,
required bool current,
required String subtitle,
}) =>
LearnSubLevelTile(
title: title,
current: current,
subtitle: subtitle,
);
}

View File

@ -0,0 +1,29 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
class LearnLevelViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final List<Map<String, dynamic>> _learnSubLevels = [
{
'title': 'A1',
'current': true,
'subtitle': 'Start your journey with the basics of English.',
},
{
'title': 'A2',
'current': false,
'subtitle': 'Build upon your foundational knowledge.',
},
];
List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels;
Future<void> navigateToLearnModule() async =>
_navigationService.navigateToLearnModuleView();
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/learn_module_tile.dart';
import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_module_viewmodel.dart';
class LearnModuleView extends StackedView<LearnModuleViewModel> {
const LearnModuleView({Key? key}) : super(key: key);
@override
LearnModuleViewModel viewModelBuilder(BuildContext context) =>
LearnModuleViewModel();
@override
Widget builder(
BuildContext context,
LearnModuleViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnModuleViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnModuleViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(LearnModuleViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnModuleViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildLevelsColumnWrapper(viewModel),
],
);
Widget _buildAppBar(LearnModuleViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
);
Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnModuleViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(LearnModuleViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(LearnModuleViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildOverallProgress(),
verticalSpaceMedium,
_buildListView(viewModel)
];
Widget _buildTitle() => const Text(
'A1 - Beginner',
style: TextStyle(
fontSize: 18,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitle() => const Text(
'Your Current Level',
style: TextStyle(
color: kcDarkGrey,
),
);
Widget _buildOverallProgress() => const OverallLearnProgress();
Widget _buildListView(LearnModuleViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.modules.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
title: viewModel.modules[index]['title'],
status: viewModel.modules[index]['status'],
subtitle: viewModel.modules[index]['subtitle']),
);
Widget _buildTile({
required String title,
required String subtitle,
required LearnLevelStatus status,
}) =>
LearnModuleTile(
title: title,
status: status,
subtitle: subtitle,
);
}

View File

@ -0,0 +1,38 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../common/enmus.dart';
class LearnModuleViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final List<Map<String, dynamic>> _modules = [
{
'status': LearnLevelStatus.started,
'title': 'Module 1: Greetings & Introductions',
'subtitle':
'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.',
},
{
'status': LearnLevelStatus.pending,
'title': 'Module 2: Everyday Basics',
'subtitle': 'Learn numbers, colors, and common objects.',
},
{
'title': 'Module 3: At the Cafe',
'status': LearnLevelStatus.pending,
'subtitle': 'Practice ordering food and drinks confidently.',
},
{
'progress': 0,
'status': LearnLevelStatus.pending,
'title': 'Module 4: Asking for Directions',
'subtitle': 'Learn numbers, colors, and common objects.',
},
];
List<Map<String, dynamic>> get modules => _modules;
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart';
import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart';
import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart';
import '../../common/app_colors.dart';
import '../../common/validators/form_validator.dart';
import '../../widgets/large_app_bar.dart';
import '../../widgets/page_loading_indicator.dart';
import 'login_viewmodel.dart';
import 'login_view.form.dart';
@FormView(fields: [
FormTextField(name: 'otp', validator: FormValidator.validateForm),
FormTextField(name: 'email', validator: FormValidator.validateEmail),
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm)
])
class LoginView extends StackedView<LoginViewModel> with $LoginView {
const LoginView({Key? key}) : super(key: key);
@override
void onViewModelReady(LoginViewModel viewModel) {
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel();
@override
Widget builder(
BuildContext context,
LoginViewModel viewModel,
Widget? child,
) =>
_buildLoginScreensWrapper(viewModel);
Widget _buildLoginScreensWrapper(LoginViewModel viewModel) => PopScope(
canPop: true,
onPopInvokedWithResult: (value, data) {
if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(LoginViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LoginViewModel viewModel) =>
Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]);
Widget _buildScaffold(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(LoginViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(LoginViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(LoginViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(LoginViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildLoginWithEmailScreen(),
_buildLoginWithPhoneScreen(),
_buildLoginOtpScreen()
];
Widget _buildLoginWithEmailScreen() => LoginWithEmailScreen(
emailController: emailController, passwordController: passwordController);
Widget _buildLoginWithPhoneScreen() =>
LoginWithPhoneNumberScreen(phoneNumberController: phoneNumberController);
Widget _buildLoginOtpScreen() => LoginOtpScreen(
otpController: otpController,
phoneNumberController: phoneNumberController);
Widget _buildBusyLogin(LoginViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
}

View File

@ -0,0 +1,269 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// StackedFormGenerator
// **************************************************************************
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String OtpValueKey = 'otp';
const String EmailValueKey = 'email';
const String PasswordValueKey = 'password';
const String PhoneNumberValueKey = 'phoneNumber';
final Map<String, TextEditingController> _LoginViewTextEditingControllers = {};
final Map<String, FocusNode> _LoginViewFocusNodes = {};
final Map<String, String? Function(String?)?> _LoginViewTextValidations = {
OtpValueKey: FormValidator.validateForm,
EmailValueKey: FormValidator.validateEmail,
PasswordValueKey: FormValidator.validateForm,
PhoneNumberValueKey: FormValidator.validateForm,
};
mixin $LoginView {
TextEditingController get otpController =>
_getFormTextEditingController(OtpValueKey);
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get phoneNumberController =>
_getFormTextEditingController(PhoneNumberValueKey);
FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
TextEditingController _getFormTextEditingController(
String key, {
String? initialValue,
}) {
if (_LoginViewTextEditingControllers.containsKey(key)) {
return _LoginViewTextEditingControllers[key]!;
}
_LoginViewTextEditingControllers[key] =
TextEditingController(text: initialValue);
return _LoginViewTextEditingControllers[key]!;
}
FocusNode _getFormFocusNode(String key) {
if (_LoginViewFocusNodes.containsKey(key)) {
return _LoginViewFocusNodes[key]!;
}
_LoginViewFocusNodes[key] = FocusNode();
return _LoginViewFocusNodes[key]!;
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
otpController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
@Deprecated(
'Use syncFormWithViewModel instead.'
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
otpController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Updates the formData on the FormViewModel
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
model.setData(
model.formValueMap
..addAll({
OtpValueKey: otpController.text,
EmailValueKey: emailController.text,
PasswordValueKey: passwordController.text,
PhoneNumberValueKey: phoneNumberController.text,
}),
);
if (_autoTextFieldValidation || forceValidate) {
updateValidationData(model);
}
}
bool validateFormFields(FormViewModel model) {
_updateFormData(model, forceValidate: true);
return model.isFormValid;
}
/// Calls dispose on all the generated controllers and focus nodes
void disposeForm() {
// The dispose function for a TextEditingController sets all listeners to null
for (var controller in _LoginViewTextEditingControllers.values) {
controller.dispose();
}
for (var focusNode in _LoginViewFocusNodes.values) {
focusNode.dispose();
}
_LoginViewTextEditingControllers.clear();
_LoginViewFocusNodes.clear();
}
}
extension ValueProperties on FormStateHelper {
bool get hasAnyValidationMessage => this
.fieldsValidationMessages
.values
.any((validation) => validation != null);
bool get isFormValid {
if (!_autoTextFieldValidation) this.validateForm();
return !hasAnyValidationMessage;
}
String? get otpValue => this.formValueMap[OtpValueKey] as String?;
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get phoneNumberValue =>
this.formValueMap[PhoneNumberValueKey] as String?;
set otpValue(String? value) {
this.setData(
this.formValueMap..addAll({OtpValueKey: value}),
);
if (_LoginViewTextEditingControllers.containsKey(OtpValueKey)) {
_LoginViewTextEditingControllers[OtpValueKey]?.text = value ?? '';
}
}
set emailValue(String? value) {
this.setData(
this.formValueMap..addAll({EmailValueKey: value}),
);
if (_LoginViewTextEditingControllers.containsKey(EmailValueKey)) {
_LoginViewTextEditingControllers[EmailValueKey]?.text = value ?? '';
}
}
set passwordValue(String? value) {
this.setData(
this.formValueMap..addAll({PasswordValueKey: value}),
);
if (_LoginViewTextEditingControllers.containsKey(PasswordValueKey)) {
_LoginViewTextEditingControllers[PasswordValueKey]?.text = value ?? '';
}
}
set phoneNumberValue(String? value) {
this.setData(
this.formValueMap..addAll({PhoneNumberValueKey: value}),
);
if (_LoginViewTextEditingControllers.containsKey(PhoneNumberValueKey)) {
_LoginViewTextEditingControllers[PhoneNumberValueKey]?.text = value ?? '';
}
}
bool get hasOtp =>
this.formValueMap.containsKey(OtpValueKey) &&
(otpValue?.isNotEmpty ?? false);
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasPhoneNumber =>
this.formValueMap.containsKey(PhoneNumberValueKey) &&
(phoneNumberValue?.isNotEmpty ?? false);
bool get hasOtpValidationMessage =>
this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false;
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasPhoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false;
String? get otpValidationMessage =>
this.fieldsValidationMessages[OtpValueKey];
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get phoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey];
}
extension Methods on FormStateHelper {
setOtpValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[OtpValueKey] = validationMessage;
setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setPhoneNumberValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage;
/// Clears text input fields on the Form
void clearForm() {
otpValue = '';
emailValue = '';
passwordValue = '';
phoneNumberValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
OtpValueKey: getValidationMessage(OtpValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
});
}
}
/// Returns the validation message for the given key
String? getValidationMessage(String key) {
final validatorForKey = _LoginViewTextValidations[key];
if (validatorForKey == null) return null;
String? validationMessageForKey = validatorForKey(
_LoginViewTextEditingControllers[key]!.text,
);
return validationMessageForKey;
}
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
OtpValueKey: getValidationMessage(OtpValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
});

View File

@ -0,0 +1,161 @@
import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../home/home_view.dart';
class LoginViewModel extends FormViewModel {
final _apiService = locator<ApiService>();
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
// Navigation
int _currentIndex = 0;
int get currentIndex => _currentIndex;
// Email
bool _focusEmail = false;
bool get focusEmail => _focusEmail;
// Password
bool _focusPassword = false;
bool get focusPassword => _focusPassword;
bool _obscurePassword = true;
bool get obscurePassword => _obscurePassword;
// Phone number
bool _focusPhoneNumber = false;
bool get focusPhoneNumber => _focusPhoneNumber;
// Focus otp
bool _focusOtp = false;
bool get focusOtp => _focusOtp;
// Focus node
final FocusNode _focusNode = FocusNode();
FocusNode get focusNode => _focusNode;
// Resend button state
bool _buttonActive = false;
bool get buttonActive => _buttonActive;
// User data
final Map<String, dynamic> _userData = {};
Map<String, dynamic> get userData => _userData;
// Email
void setEmailFocus() {
_focusEmail = true;
rebuildUi();
}
// Password
void setPasswordFocus() {
_focusPassword = true;
rebuildUi();
}
void setObscurePassword() {
_obscurePassword = !_obscurePassword;
rebuildUi();
}
// Phone number
void setPhoneNumberFocus() {
_focusPhoneNumber = true;
rebuildUi();
}
// Otp
void setOtpFocus() {
_focusOtp = true;
rebuildUi();
}
// Validate otp
Future<void> validateOtp(String otp) async {}
void setResendButton() {
_buttonActive = true;
rebuildUi();
}
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// Remote api calls
Future<void> login() async {
Map<String, dynamic> response =
await runBusyFuture<Map<String, dynamic>>(_login());
if (response['status'] == ResponseStatus.success) {
await replaceWithHome();
}
}
Future<Map<String, dynamic>> _login() async {
Map<String, dynamic> response = await _apiService.login(_userData);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserData(data);
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
return response;
}
// Navigation
void goTo(int page) {
_currentIndex = page;
rebuildUi();
}
void goBack() {
if (_currentIndex == 1) {
_currentIndex = 0;
rebuildUi();
} else if (_currentIndex == 2) {
_currentIndex = 1;
rebuildUi();
} else {
_navigationService.back();
}
}
Future<void> navigateToRegister() async =>
await _navigationService.navigateToRegisterView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
}

View File

@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:flutter_timer_countdown/flutter_timer_countdown.dart';
import 'package:pinput/pinput.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/register/register_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../login_viewmodel.dart';
import '../login_view.form.dart';
class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
final TextEditingController otpController;
final TextEditingController phoneNumberController;
const LoginOtpScreen(
{super.key,
required this.otpController,
required this.phoneNumberController});
@override
Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubtitleWrapper(),
verticalSpaceMedium,
_buildPinPutWrapper(viewModel),
if (viewModel.hasOtpValidationMessage && viewModel.focusOtp)
verticalSpaceTiny,
if (viewModel.hasOtpValidationMessage && viewModel.focusOtp)
_buildOtpValidatorWrapper(viewModel),
verticalSpaceSmall,
_buildTimerWrapper(viewModel)
];
Widget _buildTitle() => Text(
'Verification Code',
style: style25DG600,
);
Widget _buildSubtitleWrapper() =>
phoneNumberController.text.length == 9 ? _buildSubtitle() : Container();
Widget _buildSubtitle() => Text(
'Code sent to your number +251${phoneNumberController.text.substring(0, 5)}****',
style: style14DG400,
);
Widget _buildPinPutWrapper(LoginViewModel viewModel) => Center(
child: _buildPinPut(viewModel),
);
Widget _buildPinPut(LoginViewModel viewModel) => Pinput(
controller: otpController,
defaultPinTheme: defaultPin,
cursor: const CustomCursor(),
errorPinTheme: errorPinTheme,
onTap: viewModel.setOtpFocus,
focusNode: viewModel.focusNode,
errorTextStyle: validationStyle,
//smsRetriever: locator<KewedeSmsRetriever>(),
focusedPinTheme: focusedThemePin,
submittedPinTheme: submittedThemePin,
hapticFeedbackType: HapticFeedbackType.heavyImpact,
separatorBuilder: (index) => const SizedBox(width: 25),
onCompleted: (otp) async => await viewModel.validateOtp(otp),
);
Widget _buildOtpValidatorWrapper(LoginViewModel viewModel) =>
viewModel.hasOtpValidationMessage
? _buildOtpValidator(viewModel)
: Container();
Widget _buildOtpValidator(LoginViewModel viewModel) => Text(
viewModel.otpValidationMessage!,
style: style12R700,
);
Widget _buildTimerWrapper(LoginViewModel viewModel) => !viewModel.buttonActive
? _buildTimerSection(viewModel)
: _buildResendButton();
Widget _buildResendButton() =>
TextButton(onPressed: () {}, child: _buildResendText());
Widget _buildResendText() => Text(
'Resend code',
style: style14P600.copyWith(fontStyle: FontStyle.italic),
);
Widget _buildTimerSection(LoginViewModel viewModel) => Row(
children: [
_buildCountdownText(),
horizontalSpaceSmall,
_buildTimer(viewModel)
],
);
Widget _buildCountdownText() => Text('Resend code in ', style: style14DG400);
Widget _buildTimer(LoginViewModel viewModel) => TimerCountdown(
enableDescriptions: false,
timeTextStyle: style14P600,
endTime: DateTime.now().add(const Duration(minutes: 3, seconds: 0)),
onEnd: viewModel.setResendButton,
format: CountDownTimerFormat.minutesSeconds,
colonsTextStyle: const TextStyle(color: kcPrimaryColor),
);
Widget _buildContinueButtonWrapper(LoginViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: viewModel.focusOtp &&
otpController.text.length == 4 &&
!viewModel.hasOtpValidationMessage
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.focusOtp &&
otpController.text.length == 4 &&
!viewModel.hasOtpValidationMessage
? () => viewModel.replaceWithHome()
: null,
);
}

View File

@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/login/login_view.form.dart';
import 'package:yimaru_app/ui/widgets/obscure_password.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/option_text_divider.dart';
import '../../../widgets/register_for_account.dart';
import '../login_viewmodel.dart';
class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
final TextEditingController emailController;
final TextEditingController passwordController;
const LoginWithEmailScreen(
{super.key,
required this.emailController,
required this.passwordController});
Future<void> _login(LoginViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'email': emailController.text,
'password': passwordController.text,
};
viewModel.addUserData(data);
await viewModel.login();
}
@override
Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitleWrapper(viewModel),
verticalSpaceLarge,
_buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
verticalSpaceTiny,
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
_buildEmailValidatorWrapper(viewModel),
verticalSpaceMedium,
_buildPasswordFormField(viewModel),
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
verticalSpaceTiny,
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
_buildPasswordValidationWrapper(viewModel),
_buildForgetPasswordTextButtonWrapper(),
];
Widget _buildTitle() => const Text(
'Welcome Back',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(),
);
Widget _buildEmailFormField(LoginViewModel viewModel) => TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
onTap: viewModel.setEmailFocus,
decoration: inputDecoration(
hint: 'Email',
focus: viewModel.focusEmail,
filled: emailController.text.isNotEmpty),
);
Widget _buildEmailValidatorWrapper(LoginViewModel viewModel) =>
viewModel.hasEmailValidationMessage
? _buildEmailValidator(viewModel)
: Container();
Widget _buildEmailValidator(LoginViewModel viewModel) => Text(
viewModel.emailValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField(
controller: passwordController,
onTap: viewModel.setPasswordFocus,
obscureText: viewModel.obscurePassword,
decoration: inputDecoration(
hint: 'Password',
focus: viewModel.focusPassword,
suffix: _buildObscureButton(viewModel),
filled: passwordController.text.isNotEmpty),
);
Widget _buildObscureButton(LoginViewModel viewModel) => ObscurePassword(
focus: viewModel.focusPassword,
obscure: viewModel.obscurePassword,
onTap: viewModel.setObscurePassword,
);
Widget _buildPasswordValidationWrapper(LoginViewModel viewModel) =>
viewModel.hasPasswordValidationMessage
? _buildPasswordValidator(viewModel)
: Container();
Widget _buildPasswordValidator(LoginViewModel viewModel) => Text(
viewModel.passwordValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildForgetPasswordTextButtonWrapper() => Align(
alignment: Alignment.centerRight,
child: _buildForgetPasswordTextButton(),
);
Widget _buildForgetPasswordTextButton() => TextButton(
onPressed: () {},
child: _buildForgetPasswordText(),
);
Widget _buildForgetPasswordText() => const Text(
'Forget Password?',
style: TextStyle(color: kcPrimaryColor),
);
Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [
_buildContinueButton(viewModel),
_buildOptionTextDivider(),
_buildLoginWithEmailButton(viewModel),
verticalSpaceMedium
];
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: (viewModel.focusEmail &&
emailController.text.isNotEmpty) &&
(viewModel.focusPassword && passwordController.text.isNotEmpty)
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: (viewModel.focusEmail && emailController.text.isNotEmpty) &&
(viewModel.focusPassword && passwordController.text.isNotEmpty)
? () async => await _login(viewModel)
: null,
);
Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildLoginWithEmailButton(LoginViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
leadingIcon: Icons.phone,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
text: 'Login with Phone Number',
onTap: () => viewModel.goTo(1),
);
}

View File

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/login/login_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/option_text_divider.dart';
import 'package:yimaru_app/ui/widgets/register_for_account.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/phone_number_prefix.dart';
import '../login_view.form.dart';
class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
final TextEditingController phoneNumberController;
const LoginWithPhoneNumberScreen(
{super.key, required this.phoneNumberController});
@override
Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitleWrapper(viewModel),
verticalSpaceMedium,
_buildSubtitle(),
verticalSpaceMedium,
_buildPhoneNumberWrapper(viewModel),
if (viewModel.hasPhoneNumberValidationMessage &&
viewModel.focusPhoneNumber)
verticalSpaceTiny,
if (viewModel.hasPhoneNumberValidationMessage &&
viewModel.focusPhoneNumber)
_buildPhoneNumberValidatorWrapper(viewModel),
];
Widget _buildTitle() => const Text(
'Welcome Back',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(),
);
Widget _buildSubtitle() => const Text(
'Enter your phone number. We will send you a confirmation code there',
style: TextStyle(color: kcMediumGrey),
);
Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildPhoneNumberChildren(viewModel),
);
List<Widget> _buildPhoneNumberChildren(LoginViewModel viewModel) => [
_buildPhoneNumberPrefix(viewModel),
horizontalSpaceSmall,
_buildPhoneNumberFormFieldWrapper(viewModel),
];
Widget _buildPhoneNumberPrefix(LoginViewModel viewModel) =>
PhoneNumberPrefix(selected: viewModel.focusPhoneNumber);
Widget _buildPhoneNumberFormFieldWrapper(LoginViewModel viewModel) =>
Expanded(child: _buildPhoneNumberFormField(viewModel));
Widget _buildPhoneNumberFormField(LoginViewModel viewModel) => TextFormField(
maxLength: 9,
keyboardType: TextInputType.phone,
controller: phoneNumberController,
onTap: viewModel.setPhoneNumberFocus,
decoration: inputDecoration(
focus: viewModel.focusPhoneNumber,
filled: phoneNumberController.text.isNotEmpty),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
);
Widget _buildPhoneNumberValidatorWrapper(LoginViewModel viewModel) =>
viewModel.hasPhoneNumberValidationMessage
? _buildPhoneNumberValidator(viewModel)
: Container();
Widget _buildPhoneNumberValidator(LoginViewModel viewModel) => Text(
viewModel.phoneNumberValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [
_buildContinueButton(viewModel),
_buildOptionTextDivider(),
_buildLoginWitPhoneNumberButton(viewModel),
verticalSpaceMedium
];
Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap:
viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty
? () => viewModel.goTo(2)
: null,
backgroundColor:
viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildLoginWitPhoneNumberButton(LoginViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Login with Email',
backgroundColor: kcWhite,
leadingIcon: Icons.email,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
onTap: () => viewModel.goTo(0),
);
}

View File

@ -1,52 +1,64 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart'; import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_completion.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_failure.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_intro.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_result.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_result_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/first_assessment_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/result_analysis.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/result_analysis_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/retake_assessment.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/second_assessment_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/start_lesson.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/start_lesson_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/assessment/third_assessment_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/age_group_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/age_group_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/challenge_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/challenge_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/country_region_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/country_region_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/educational_background_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/educational_background_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/full_name_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/full_name_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_goal_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_reason_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/occupation_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/occupation_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/forms/topic_form.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/forms/topic_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/welcome/first_welcome.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/welcome/first_welcome_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/welcome/second_welcome.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/welcome/second_welcome_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/welcome/third_welcome.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/welcome/third_welcome_screen.dart';
import '../../common/validators/onboarding_form_validator.dart'; import '../../common/validators/form_validator.dart';
import 'onboarding_viewmodel.dart'; import 'onboarding_viewmodel.dart';
import 'onboarding_view.form.dart'; import 'onboarding_view.form.dart';
@FormView(fields: [ @FormView(fields: [
FormTextField( FormTextField(name: 'answer', validator: FormValidator.validateForm),
name: 'answer', validator: OnboardingFormValidator.validateForm), FormTextField(name: 'fullName', validator: FormValidator.validateForm),
FormTextField( FormTextField(name: 'challenge', validator: FormValidator.validateForm),
name: 'fullName', validator: OnboardingFormValidator.validateForm), FormTextField(name: 'occupation', validator: FormValidator.validateForm),
FormTextField( FormTextField(name: 'learningReason', validator: FormValidator.validateForm),
name: 'challenge', validator: OnboardingFormValidator.validateForm), FormTextField(name: 'topic', validator: FormValidator.validateForm),
FormTextField(
name: 'occupation', validator: OnboardingFormValidator.validateForm),
FormTextField(
name: 'learningReason', validator: OnboardingFormValidator.validateForm),
FormTextField(name: 'topic', validator: OnboardingFormValidator.validateForm),
]) ])
class OnboardingView extends StackedView<OnboardingViewModel> class OnboardingView extends StackedView<OnboardingViewModel>
with $OnboardingView { with $OnboardingView {
const OnboardingView({Key? key}) : super(key: key); const OnboardingView({Key? key}) : super(key: key);
void _initFormFields() {
answerController.text = 'Book';
}
@override
void onViewModelReady(OnboardingViewModel viewModel) {
_initFormFields();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
OnboardingViewModel viewModelBuilder(
BuildContext context,
) =>
OnboardingViewModel();
@override @override
Widget builder( Widget builder(
BuildContext context, BuildContext context,
@ -94,68 +106,57 @@ class OnboardingView extends StackedView<OnboardingViewModel>
_buildLanguageSelector() _buildLanguageSelector()
]; ];
Widget _buildFirstWelcome() => const FirstWelcome(); Widget _buildFirstWelcome() => const FirstWelcomeScreen();
Widget _buildSecondWelcome() => const SecondWelcome(); Widget _buildSecondWelcome() => const SecondWelcomeScreen();
Widget _buildThirdWelcome() => const ThirdWelcome(); Widget _buildThirdWelcome() => const ThirdWelcomeScreen();
Widget _buildFullNameForm() => Widget _buildFullNameForm() =>
FullNameForm(fullNameController: fullNameController); FullNameFormScreen(fullNameController: fullNameController);
Widget _buildEducationalBackgroundForm() => const EducationalBackgroundForm(); Widget _buildEducationalBackgroundForm() =>
const EducationalBackgroundFormScreen();
Widget _buildAgeGroupForm() => const AgeGroupForm(); Widget _buildAgeGroupForm() => const AgeGroupFormScreen();
Widget _buildOccupationForm() => Widget _buildOccupationForm() =>
OccupationForm(occupationController: occupationController); OccupationFormScreen(occupationController: occupationController);
Widget _buildCountryRegionForm() => const CountryRegionForm(); Widget _buildCountryRegionForm() => const CountryRegionFormScreen();
Widget _buildLearningGoalForm() => const LearningGoalForm(); Widget _buildLearningGoalForm() => const LearningGoalFormScreen();
Widget _buildLearningReasonForm() => Widget _buildLearningReasonForm() => LearningReasonFormScreen(
LearningReasonForm(learningReasonController: learningReasonController); learningReasonController: learningReasonController);
Widget _buildChallengeForm() => Widget _buildChallengeForm() =>
ChallengeForm(challengeController: challengeController); ChallengeFormScreen(challengeController: challengeController);
Widget _buildTopicForm() => TopicForm(topicController: topicController); Widget _buildTopicForm() => TopicFormScreen(topicController: topicController);
Widget _buildAssessmentIntro() => const AssessmentIntro(); Widget _buildAssessmentIntro() => const AssessmentIntroScreen();
Widget _buildFirstAssessmentForm() => Widget _buildFirstAssessmentForm() =>
FirstAssessmentForm(answerController: answerController); FirstAssessmentFormScreen(answerController: answerController);
Widget _buildSecondAssessment() => const SecondAssessmentForm(); Widget _buildSecondAssessment() => const SecondAssessmentFormScreen();
Widget _buildThirdAssessment() => const ThirdAssessmentForm(); Widget _buildThirdAssessment() => const ThirdAssessmentFormScreen();
Widget _buildFourthAssessment() => const FourthAssessmentForm(); Widget _buildFourthAssessment() => const FourthAssessmentFormScreen();
Widget _buildAssessmentFailure() => const AssessmentFailure(); Widget _buildAssessmentFailure() => const AssessmentFailureScreen();
Widget _buildRetakeAssessment() => const RetakeAssessment(); Widget _buildRetakeAssessment() => const RetakeAssessmentScreen();
Widget _buildResultAnalysis() => const ResultAnalysis(); Widget _buildResultAnalysis() => const ResultAnalysisScreen();
Widget _buildAssessmentCompletion() => const AssessmentCompletion(); Widget _buildAssessmentCompletion() => const AssessmentCompletionScreen();
Widget _buildAssessmentResult() => const AssessmentResult(); Widget _buildAssessmentResult() => const AssessmentResultScreen();
Widget _buildStartLesson() => const StartLesson(); Widget _buildStartLesson() => const StartLessonScreen();
Widget _buildLanguageSelector() => const LanguageSelector(); Widget _buildLanguageSelector() => const LanguageSelector();
@override
void onViewModelReady(OnboardingViewModel viewModel) {
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
OnboardingViewModel viewModelBuilder(
BuildContext context,
) =>
OnboardingViewModel();
} }

View File

@ -8,7 +8,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/validators/onboarding_form_validator.dart'; import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true; const bool _autoTextFieldValidation = true;
@ -25,12 +25,12 @@ final Map<String, TextEditingController> _OnboardingViewTextEditingControllers =
final Map<String, FocusNode> _OnboardingViewFocusNodes = {}; final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = { final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
AnswerValueKey: OnboardingFormValidator.validateForm, AnswerValueKey: FormValidator.validateForm,
FullNameValueKey: OnboardingFormValidator.validateForm, FullNameValueKey: FormValidator.validateForm,
ChallengeValueKey: OnboardingFormValidator.validateForm, ChallengeValueKey: FormValidator.validateForm,
OccupationValueKey: OnboardingFormValidator.validateForm, OccupationValueKey: FormValidator.validateForm,
LearningReasonValueKey: OnboardingFormValidator.validateForm, LearningReasonValueKey: FormValidator.validateForm,
TopicValueKey: OnboardingFormValidator.validateForm, TopicValueKey: FormValidator.validateForm,
}; };
mixin $OnboardingView { mixin $OnboardingView {

View File

@ -2,7 +2,6 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import 'onboarding_view.form.dart';
class OnboardingViewModel extends FormViewModel { class OnboardingViewModel extends FormViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
@ -26,7 +25,7 @@ class OnboardingViewModel extends FormViewModel {
'Primary school', 'Primary school',
'Secondary /High school', 'Secondary /High school',
'College / Diploma', 'College / Diploma',
'Bachelors and above' 'Bachelors and above',
]; ];
List<String> get educationalBackgrounds => _educationalBackgrounds; List<String> get educationalBackgrounds => _educationalBackgrounds;
@ -357,6 +356,10 @@ class OnboardingViewModel extends FormViewModel {
bool isSelectedLanguage(String title) => bool isSelectedLanguage(String title) =>
_selectedLanguage['language'] == title; _selectedLanguage['language'] == title;
// Navigation
Future<void> navigateToHome() async =>
await _navigationService.navigateToHomeView();
void next({int? page}) async { void next({int? page}) async {
if (page == null) { if (page == null) {
if (_previousPage != 0) { if (_previousPage != 0) {

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AssessmentCompletion extends ViewModelWidget<OnboardingViewModel> { class AssessmentCompletionScreen extends ViewModelWidget<OnboardingViewModel> {
const AssessmentCompletion({super.key}); const AssessmentCompletionScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -27,7 +27,7 @@ class AssessmentCompletion extends ViewModelWidget<OnboardingViewModel> {
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(), _buildExpandedBody(viewModel)];
Widget _buildAppBar() => const OnboardingAppBar( Widget _buildAppBar() => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: false, showLanguageSelection: false,
); );
@ -73,7 +73,7 @@ class AssessmentCompletion extends ViewModelWidget<OnboardingViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -95,7 +95,7 @@ class AssessmentCompletion extends ViewModelWidget<OnboardingViewModel> {
borderRadius: 12, borderRadius: 12,
text: 'View My Results', text: 'View My Results',
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
} }

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AssessmentFailure extends ViewModelWidget<OnboardingViewModel> { class AssessmentFailureScreen extends ViewModelWidget<OnboardingViewModel> {
const AssessmentFailure({super.key}); const AssessmentFailureScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -25,9 +25,15 @@ class AssessmentFailure extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -69,7 +75,7 @@ class AssessmentFailure extends ViewModelWidget<OnboardingViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -87,17 +93,18 @@ class AssessmentFailure extends ViewModelWidget<OnboardingViewModel> {
List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [
_buildContinueButton(viewModel), _buildContinueButton(viewModel),
verticalSpaceMedium, verticalSpaceSmall,
_buildSkipButtonWrapper(viewModel) _buildSkipButtonWrapper(viewModel)
]; ];
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
safe: false,
borderRadius: 12, borderRadius: 12,
text: 'Continue Assessment', text: 'Continue Assessment',
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
@ -113,7 +120,7 @@ class AssessmentFailure extends ViewModelWidget<OnboardingViewModel> {
borderRadius: 12, borderRadius: 12,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhiteColor, backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -4,10 +4,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AssessmentIntro extends ViewModelWidget<OnboardingViewModel> { class AssessmentIntroScreen extends ViewModelWidget<OnboardingViewModel> {
const AssessmentIntro({super.key}); const AssessmentIntroScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -24,7 +24,7 @@ class AssessmentIntro extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -56,13 +56,19 @@ class AssessmentIntro extends ViewModelWidget<OnboardingViewModel> {
_buildSubTitle(), _buildSubTitle(),
]; ];
Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Want a quick assessment to know your English level?', 'Want a quick assessment to know your English level?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -79,17 +85,18 @@ class AssessmentIntro extends ViewModelWidget<OnboardingViewModel> {
List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [
_buildContinueButton(viewModel), _buildContinueButton(viewModel),
verticalSpaceMedium, verticalSpaceSmall,
_buildSkipButtonWrapper(viewModel) _buildSkipButtonWrapper(viewModel)
]; ];
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
@ -104,7 +111,7 @@ class AssessmentIntro extends ViewModelWidget<OnboardingViewModel> {
text: 'Skip', text: 'Skip',
borderRadius: 12, borderRadius: 12,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
backgroundColor: kcWhiteColor, backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AssessmentResult extends ViewModelWidget<OnboardingViewModel> { class AssessmentResultScreen extends ViewModelWidget<OnboardingViewModel> {
const AssessmentResult({super.key}); const AssessmentResultScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -25,9 +25,15 @@ class AssessmentResult extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -95,17 +101,18 @@ class AssessmentResult extends ViewModelWidget<OnboardingViewModel> {
List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [
_buildContinueButton(viewModel), _buildContinueButton(viewModel),
verticalSpaceMedium, verticalSpaceSmall,
_buildSkipButtonWrapper(viewModel) _buildSkipButtonWrapper(viewModel)
]; ];
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
@ -121,7 +128,7 @@ class AssessmentResult extends ViewModelWidget<OnboardingViewModel> {
text: 'Practice Speaking', text: 'Practice Speaking',
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhiteColor, backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../onboarding_view.form.dart'; import '../../onboarding_view.form.dart';
class FirstAssessmentForm extends ViewModelWidget<OnboardingViewModel> { class FirstAssessmentFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController answerController; final TextEditingController answerController;
const FirstAssessmentForm({super.key, required this.answerController}); const FirstAssessmentFormScreen({super.key, required this.answerController});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -66,7 +66,7 @@ class FirstAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
_buildFirstAssessmentValidatorWrapper(viewModel) _buildFirstAssessmentValidatorWrapper(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar( Widget _buildAppBar() => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: false, showLanguageSelection: false,
); );
@ -75,7 +75,7 @@ class FirstAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
'1. What is the plural of “book”?', '1. What is the plural of “book”?',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -84,7 +84,9 @@ class FirstAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
TextFormField( TextFormField(
controller: answerController, controller: answerController,
onTap: viewModel.setFirstAssessmentFocus, onTap: viewModel.setFirstAssessmentFocus,
decoration: inputDecoration(focus: viewModel.focusFirstAssessment), decoration: inputDecoration(
focus: viewModel.focusFirstAssessment,
filled: answerController.text.isNotEmpty),
); );
Widget _buildFirstAssessmentValidatorWrapper(OnboardingViewModel viewModel) => Widget _buildFirstAssessmentValidatorWrapper(OnboardingViewModel viewModel) =>
@ -111,13 +113,15 @@ class FirstAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: backgroundColor: answerController.text.isNotEmpty
viewModel.focusFirstAssessment && answerController.text.isNotEmpty
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : viewModel.focusFirstAssessment && answerController.text.isNotEmpty
onTap: ? kcPrimaryColor
viewModel.focusFirstAssessment && answerController.text.isNotEmpty : kcPrimaryColor.withOpacity(0.1),
onTap: answerController.text.isNotEmpty
? () => viewModel.next()
: viewModel.focusFirstAssessment && answerController.text.isNotEmpty
? () => viewModel.next() ? () => viewModel.next()
: null, : null,
); );

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class FourthAssessmentForm extends ViewModelWidget<OnboardingViewModel> { class FourthAssessmentFormScreen extends ViewModelWidget<OnboardingViewModel> {
const FourthAssessmentForm({super.key}); const FourthAssessmentFormScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -57,7 +57,7 @@ class FourthAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
_buildAnswers(viewModel) _buildAnswers(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar( Widget _buildAppBar() => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: false, showLanguageSelection: false,
); );
@ -66,7 +66,7 @@ class FourthAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
'Q4.  Choose the word that best matches the meaning of meticulous:', 'Q4.  Choose the word that best matches the meaning of meticulous:',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -104,11 +104,11 @@ class FourthAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: onTap:
viewModel.selectedA4Answer != null ? () => viewModel.next() : null, viewModel.selectedA4Answer != null ? () => viewModel.next() : null,
backgroundColor: viewModel.selectedA4Answer != null backgroundColor: viewModel.selectedA4Answer != null
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
); );
} }

View File

@ -3,12 +3,11 @@ import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class ResultAnalysis extends ViewModelWidget<OnboardingViewModel> { class ResultAnalysisScreen extends ViewModelWidget<OnboardingViewModel> {
const ResultAnalysis({super.key}); const ResultAnalysisScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -25,7 +24,7 @@ class ResultAnalysis extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -51,7 +50,11 @@ class ResultAnalysis extends ViewModelWidget<OnboardingViewModel> {
_buildSubTitle(), _buildSubTitle(),
]; ];
Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(language: false));
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/progress_indicator.svg', 'assets/icons/progress_indicator.svg',
@ -62,7 +65,7 @@ class ResultAnalysis extends ViewModelWidget<OnboardingViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );

View File

@ -4,10 +4,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class RetakeAssessment extends ViewModelWidget<OnboardingViewModel> { class RetakeAssessmentScreen extends ViewModelWidget<OnboardingViewModel> {
const RetakeAssessment({super.key}); const RetakeAssessmentScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -24,7 +24,7 @@ class RetakeAssessment extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -59,7 +59,11 @@ class RetakeAssessment extends ViewModelWidget<OnboardingViewModel> {
_buildSubTitle(), _buildSubTitle(),
]; ];
Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(language: false));
Widget _buildIcon() => const Icon( Widget _buildIcon() => const Icon(
Icons.warning_amber_rounded, Icons.warning_amber_rounded,
@ -72,7 +76,7 @@ class RetakeAssessment extends ViewModelWidget<OnboardingViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -90,17 +94,18 @@ class RetakeAssessment extends ViewModelWidget<OnboardingViewModel> {
List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(OnboardingViewModel viewModel) => [
_buildContinueButton(viewModel), _buildContinueButton(viewModel),
verticalSpaceMedium, verticalSpaceSmall,
_buildSkipButtonWrapper(viewModel) _buildSkipButtonWrapper(viewModel)
]; ];
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
safe: false,
borderRadius: 12, borderRadius: 12,
text: 'Retake Assessment', text: 'Retake Assessment',
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
@ -116,7 +121,7 @@ class RetakeAssessment extends ViewModelWidget<OnboardingViewModel> {
borderRadius: 12, borderRadius: 12,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhiteColor, backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class SecondAssessmentForm extends ViewModelWidget<OnboardingViewModel> { class SecondAssessmentFormScreen extends ViewModelWidget<OnboardingViewModel> {
const SecondAssessmentForm({super.key}); const SecondAssessmentFormScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -57,7 +57,7 @@ class SecondAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
_buildAnswers(viewModel) _buildAnswers(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar( Widget _buildAppBar() => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: false, showLanguageSelection: false,
); );
@ -66,7 +66,7 @@ class SecondAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
'Q2. Choose the correct word to complete the sentence:\nI ____ to school yesterday. ', 'Q2. Choose the correct word to complete the sentence:\nI ____ to school yesterday. ',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -103,12 +103,11 @@ class SecondAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.selectedA2Answer != null onTap:
? () => viewModel.next() viewModel.selectedA2Answer != null ? () => viewModel.next() : null,
: null,
backgroundColor: viewModel.selectedA2Answer != null backgroundColor: viewModel.selectedA2Answer != null
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
); );
} }

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class StartLesson extends ViewModelWidget<OnboardingViewModel> { class StartLessonScreen extends ViewModelWidget<OnboardingViewModel> {
const StartLesson({super.key}); const StartLessonScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -27,7 +27,7 @@ class StartLesson extends ViewModelWidget<OnboardingViewModel> {
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(), _buildExpandedBody(viewModel)];
Widget _buildAppBar() => const OnboardingAppBar( Widget _buildAppBar() => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: false, showLanguageSelection: false,
); );
@ -71,7 +71,7 @@ class StartLesson extends ViewModelWidget<OnboardingViewModel> {
text: 'Welcome aboard', text: 'Welcome aboard',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
children: [ children: [
@ -97,11 +97,12 @@ class StartLesson extends ViewModelWidget<OnboardingViewModel> {
); );
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
const CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Go to My Lessons', text: 'Go to My Lessons',
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.navigateToHome(),
); );
} }

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class ThirdAssessmentForm extends ViewModelWidget<OnboardingViewModel> { class ThirdAssessmentFormScreen extends ViewModelWidget<OnboardingViewModel> {
const ThirdAssessmentForm({super.key}); const ThirdAssessmentFormScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -57,7 +57,7 @@ class ThirdAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
_buildAnswers(viewModel) _buildAnswers(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar( Widget _buildAppBar() => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: false, showLanguageSelection: false,
); );
@ -66,7 +66,7 @@ class ThirdAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
'Q3. Which word means the same as expand?', 'Q3. Which word means the same as expand?',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -103,10 +103,10 @@ class ThirdAssessmentForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: viewModel.selectedA3Answer != null backgroundColor: viewModel.selectedA3Answer != null
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
onTap: onTap:
viewModel.selectedA3Answer != null ? () => viewModel.next() : null, viewModel.selectedA3Answer != null ? () => viewModel.next() : null,
); );

View File

@ -5,17 +5,15 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AgeGroupForm extends ViewModelWidget<OnboardingViewModel> { class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
const AgeGroupForm({super.key}); const AgeGroupFormScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffold(viewModel),
@ -27,11 +25,15 @@ class AgeGroupForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildColumnScroller(viewModel));
Widget _buildColumnScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(viewModel),
@ -61,13 +63,20 @@ class AgeGroupForm extends ViewModelWidget<OnboardingViewModel> {
_buildAgeGroups(viewModel) _buildAgeGroups(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Which age range are you in?', 'Which age range are you in?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -109,10 +118,10 @@ class AgeGroupForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: viewModel.selectedAgeGroup != null backgroundColor: viewModel.selectedAgeGroup != null
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
onTap: onTap:
viewModel.selectedAgeGroup != null ? () => viewModel.next() : null, viewModel.selectedAgeGroup != null ? () => viewModel.next() : null,
); );

View File

@ -6,12 +6,12 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class ChallengeForm extends ViewModelWidget<OnboardingViewModel> { class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController challengeController; final TextEditingController challengeController;
const ChallengeForm({super.key, required this.challengeController}); const ChallengeFormScreen({super.key, required this.challengeController});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -28,17 +28,18 @@ class ChallengeForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyScroller(viewModel));
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Widget _buildBodyScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: Padding( child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(viewModel),
),
); );
Widget _buildBody(OnboardingViewModel viewModel) => Column( Widget _buildBody(OnboardingViewModel viewModel) => Column(
@ -75,13 +76,20 @@ class ChallengeForm extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'What challenge do you face most with English?', 'What challenge do you face most with English?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -119,7 +127,10 @@ class ChallengeForm extends ViewModelWidget<OnboardingViewModel> {
maxLines: 3, maxLines: 3,
controller: challengeController, controller: challengeController,
onTap: viewModel.setChallengesFocus, onTap: viewModel.setChallengesFocus,
decoration: inputDecoration(focus: true, hint: 'Write your challenge…'), decoration: inputDecoration(
focus: true,
hint: 'Write your challenge…',
filled: challengeController.text.isNotEmpty),
); );
Widget _buildChallengeValidatorWrapper(OnboardingViewModel viewModel) => Widget _buildChallengeValidatorWrapper(OnboardingViewModel viewModel) =>
@ -146,7 +157,7 @@ class ChallengeForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.selectedChallenge != null onTap: viewModel.selectedChallenge != null
? viewModel.selectedChallenge?.toLowerCase() == 'other' ? viewModel.selectedChallenge?.toLowerCase() == 'other'
? viewModel.focusChallenge ? viewModel.focusChallenge
@ -159,7 +170,7 @@ class ChallengeForm extends ViewModelWidget<OnboardingViewModel> {
? viewModel.focusChallenge && ? viewModel.focusChallenge &&
challengeController.text.isNotEmpty challengeController.text.isNotEmpty
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2) : kcPrimaryColor.withOpacity(0.1)
: kcPrimaryColor : kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2)); : kcPrimaryColor.withOpacity(0.1));
} }

View File

@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_dropdown.dart'; import 'package:yimaru_app/ui/widgets/custom_dropdown.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class CountryRegionForm extends ViewModelWidget<OnboardingViewModel> { class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
const CountryRegionForm({super.key}); const CountryRegionFormScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -25,7 +25,7 @@ class CountryRegionForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -42,7 +42,12 @@ class CountryRegionForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) => List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -62,13 +67,20 @@ class CountryRegionForm extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Where are you from?', 'Where are you from?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -79,7 +91,7 @@ class CountryRegionForm extends ViewModelWidget<OnboardingViewModel> {
); );
Widget _buildCountryDropDown(OnboardingViewModel viewModel) => Widget _buildCountryDropDown(OnboardingViewModel viewModel) =>
CustomDropDownPicker( CustomDropdownPicker(
onChanged: (value) {}, onChanged: (value) {},
hint: 'Select country', hint: 'Select country',
icon: _buildSearchIcon(), icon: _buildSearchIcon(),
@ -88,7 +100,7 @@ class CountryRegionForm extends ViewModelWidget<OnboardingViewModel> {
); );
Widget _buildRegionDropDown(OnboardingViewModel viewModel) => Widget _buildRegionDropDown(OnboardingViewModel viewModel) =>
CustomDropDownPicker( CustomDropdownPicker(
hint: 'Select region', hint: 'Select region',
onChanged: (value) {}, onChanged: (value) {},
icon: _buildSearchIcon(), icon: _buildSearchIcon(),
@ -112,7 +124,7 @@ class CountryRegionForm extends ViewModelWidget<OnboardingViewModel> {
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
} }

View File

@ -5,16 +5,16 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class EducationalBackgroundForm extends ViewModelWidget<OnboardingViewModel> { class EducationalBackgroundFormScreen
const EducationalBackgroundForm({super.key}); extends ViewModelWidget<OnboardingViewModel> {
const EducationalBackgroundFormScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffold(viewModel),
@ -26,10 +26,15 @@ class EducationalBackgroundForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildColumnScroller(viewModel));
Widget _buildColumnScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
@ -60,13 +65,20 @@ class EducationalBackgroundForm extends ViewModelWidget<OnboardingViewModel> {
_buildEducationalLevels(viewModel) _buildEducationalLevels(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Whats your current educational level?', 'Whats your current educational level?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -110,12 +122,12 @@ class EducationalBackgroundForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.selectedEducationalBackground != null onTap: viewModel.selectedEducationalBackground != null
? () => viewModel.next() ? () => viewModel.next()
: null, : null,
backgroundColor: viewModel.selectedEducationalBackground != null backgroundColor: viewModel.selectedEducationalBackground != null
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
); );
} }

View File

@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../onboarding_view.form.dart'; import '../../onboarding_view.form.dart';
class FullNameForm extends ViewModelWidget<OnboardingViewModel> { class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController fullNameController; final TextEditingController fullNameController;
const FullNameForm({super.key, required this.fullNameController}); const FullNameFormScreen({super.key, required this.fullNameController});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -28,7 +28,7 @@ class FullNameForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -45,7 +45,12 @@ class FullNameForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) => List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -66,13 +71,19 @@ class FullNameForm extends ViewModelWidget<OnboardingViewModel> {
_buildFullNameValidatorWrapper(viewModel) _buildFullNameValidatorWrapper(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'What should we call you? 😊', 'What should we call you? 😊',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -86,7 +97,10 @@ class FullNameForm extends ViewModelWidget<OnboardingViewModel> {
TextFormField( TextFormField(
controller: fullNameController, controller: fullNameController,
onTap: viewModel.setFullNameFocus, onTap: viewModel.setFullNameFocus,
decoration: inputDecoration(focus: viewModel.focusFullName), decoration: inputDecoration(
hint: 'Enter Your Name',
focus: viewModel.focusFullName,
filled: fullNameController.text.isNotEmpty),
); );
Widget _buildFullNameValidatorWrapper(OnboardingViewModel viewModel) => Widget _buildFullNameValidatorWrapper(OnboardingViewModel viewModel) =>
@ -113,13 +127,13 @@ class FullNameForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.focusFullName && fullNameController.text.isNotEmpty onTap: viewModel.focusFullName && fullNameController.text.isNotEmpty
? () => viewModel.next() ? () => viewModel.next()
: null, : null,
backgroundColor: backgroundColor:
viewModel.focusFullName && fullNameController.text.isNotEmpty viewModel.focusFullName && fullNameController.text.isNotEmpty
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
); );
} }

View File

@ -6,10 +6,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_large_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_large_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class LearningGoalForm extends ViewModelWidget<OnboardingViewModel> { class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
const LearningGoalForm({super.key}); const LearningGoalFormScreen({super.key});
IconData getIcon(int icon) { IconData getIcon(int icon) {
switch (icon) { switch (icon) {
@ -38,11 +38,15 @@ class LearningGoalForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyScroller(viewModel));
Widget _buildBodyScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(viewModel),
@ -70,13 +74,20 @@ class LearningGoalForm extends ViewModelWidget<OnboardingViewModel> {
_buildLearningGoals(viewModel) _buildLearningGoals(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Hi Johnny, Choose your learning goal.', 'Hi Johnny, Choose your learning goal.',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -120,12 +131,12 @@ class LearningGoalForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.selectedLearningGoal != null onTap: viewModel.selectedLearningGoal != null
? () => viewModel.next() ? () => viewModel.next()
: null, : null,
backgroundColor: viewModel.selectedLearningGoal != null backgroundColor: viewModel.selectedLearningGoal != null
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
); );
} }

View File

@ -6,12 +6,13 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class LearningReasonForm extends ViewModelWidget<OnboardingViewModel> { class LearningReasonFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController learningReasonController; final TextEditingController learningReasonController;
const LearningReasonForm({super.key, required this.learningReasonController}); const LearningReasonFormScreen(
{super.key, required this.learningReasonController});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -28,17 +29,19 @@ class LearningReasonForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyScroller(viewModel));
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Widget _buildBodyScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: Padding( child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(viewModel),
),
); );
Widget _buildBody(OnboardingViewModel viewModel) => Column( Widget _buildBody(OnboardingViewModel viewModel) => Column(
@ -75,13 +78,18 @@ class LearningReasonForm extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(language: false));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Whats your main goal for improving your English?', 'Whats your main goal for improving your English?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -121,7 +129,10 @@ class LearningReasonForm extends ViewModelWidget<OnboardingViewModel> {
maxLines: 3, maxLines: 3,
controller: learningReasonController, controller: learningReasonController,
onTap: viewModel.setLearningReasonFocus, onTap: viewModel.setLearningReasonFocus,
decoration: inputDecoration(focus: true, hint: 'Write your goal…'), decoration: inputDecoration(
focus: true,
hint: 'Write your goal…',
filled: learningReasonController.text.isNotEmpty),
); );
Widget _buildReasonValidatorWrapper(OnboardingViewModel viewModel) => Widget _buildReasonValidatorWrapper(OnboardingViewModel viewModel) =>
@ -148,7 +159,7 @@ class LearningReasonForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.selectedLearningReason != null onTap: viewModel.selectedLearningReason != null
? viewModel.selectedLearningReason?.toLowerCase() == 'other' ? viewModel.selectedLearningReason?.toLowerCase() == 'other'
? viewModel.focusLearningReason ? viewModel.focusLearningReason
@ -158,9 +169,10 @@ class LearningReasonForm extends ViewModelWidget<OnboardingViewModel> {
: null, : null,
backgroundColor: viewModel.selectedLearningReason != null backgroundColor: viewModel.selectedLearningReason != null
? viewModel.selectedLearningReason?.toLowerCase() == 'other' ? viewModel.selectedLearningReason?.toLowerCase() == 'other'
? viewModel.focusLearningReason && learningReasonController.text.isNotEmpty ? viewModel.focusLearningReason &&
learningReasonController.text.isNotEmpty
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2) : kcPrimaryColor.withOpacity(0.1)
: kcPrimaryColor : kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2)); : kcPrimaryColor.withOpacity(0.1));
} }

View File

@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../onboarding_view.form.dart'; import '../../onboarding_view.form.dart';
class OccupationForm extends ViewModelWidget<OnboardingViewModel> { class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController occupationController; final TextEditingController occupationController;
const OccupationForm({super.key, required this.occupationController}); const OccupationFormScreen({super.key, required this.occupationController});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -28,7 +28,7 @@ class OccupationForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -45,7 +45,12 @@ class OccupationForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) => List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -68,13 +73,20 @@ class OccupationForm extends ViewModelWidget<OnboardingViewModel> {
_buildOccupationValidatorWrapper(viewModel) _buildOccupationValidatorWrapper(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Whats your occupation?', 'Whats your occupation?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -88,7 +100,10 @@ class OccupationForm extends ViewModelWidget<OnboardingViewModel> {
TextFormField( TextFormField(
controller: occupationController, controller: occupationController,
onTap: viewModel.setOccupationFocus, onTap: viewModel.setOccupationFocus,
decoration: inputDecoration(focus: viewModel.focusOccupation), decoration: inputDecoration(
hint: 'Enter Your Occupation',
focus: viewModel.focusOccupation,
filled: occupationController.text.isNotEmpty),
); );
Widget _buildOccupationValidatorWrapper(OnboardingViewModel viewModel) => Widget _buildOccupationValidatorWrapper(OnboardingViewModel viewModel) =>
@ -115,13 +130,13 @@ class OccupationForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.focusOccupation && occupationController.text.isNotEmpty onTap: viewModel.focusOccupation && occupationController.text.isNotEmpty
? () => viewModel.next() ? () => viewModel.next()
: null, : null,
backgroundColor: backgroundColor:
viewModel.focusOccupation && occupationController.text.isNotEmpty viewModel.focusOccupation && occupationController.text.isNotEmpty
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2), : kcPrimaryColor.withOpacity(0.1),
); );
} }

View File

@ -6,18 +6,17 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class TopicForm extends ViewModelWidget<OnboardingViewModel> { class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController topicController; final TextEditingController topicController;
const TopicForm({super.key, required this.topicController}); const TopicFormScreen({super.key, required this.topicController});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffold(viewModel),
@ -29,17 +28,19 @@ class TopicForm extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyScroller(viewModel));
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Widget _buildBodyScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: Padding( child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(viewModel),
),
); );
Widget _buildBody(OnboardingViewModel viewModel) => Column( Widget _buildBody(OnboardingViewModel viewModel) => Column(
@ -76,13 +77,20 @@ class TopicForm extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
]; ];
Widget _buildAppBar() => const OnboardingAppBar(); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: false,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Which topics interest you most?', 'Which topics interest you most?',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -118,7 +126,10 @@ class TopicForm extends ViewModelWidget<OnboardingViewModel> {
maxLines: 3, maxLines: 3,
controller: topicController, controller: topicController,
onTap: viewModel.setTopicsFocus, onTap: viewModel.setTopicsFocus,
decoration: inputDecoration(focus: true, hint: 'Write you interest…'), decoration: inputDecoration(
focus: true,
hint: 'Write you interest…',
filled: topicController.text.isNotEmpty),
); );
Widget _buildTopicWrapper(OnboardingViewModel viewModel) => Widget _buildTopicWrapper(OnboardingViewModel viewModel) =>
@ -145,7 +156,7 @@ class TopicForm extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
onTap: viewModel.selectedTopic != null onTap: viewModel.selectedTopic != null
? viewModel.selectedTopic?.toLowerCase() == 'other' ? viewModel.selectedTopic?.toLowerCase() == 'other'
? viewModel.focusTopic ? viewModel.focusTopic
@ -157,7 +168,7 @@ class TopicForm extends ViewModelWidget<OnboardingViewModel> {
? viewModel.selectedTopic?.toLowerCase() == 'other' ? viewModel.selectedTopic?.toLowerCase() == 'other'
? viewModel.focusTopic && topicController.text.isNotEmpty ? viewModel.focusTopic && topicController.text.isNotEmpty
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2) : kcPrimaryColor.withOpacity(0.1)
: kcPrimaryColor : kcPrimaryColor
: kcPrimaryColor.withOpacity(0.2)); : kcPrimaryColor.withOpacity(0.1));
} }

View File

@ -6,7 +6,7 @@ import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/custom_small_radio_button.dart'; import '../../../widgets/custom_small_radio_button.dart';
import '../../../widgets/onboarding_app_bar.dart'; import '../../../widgets/large_app_bar.dart';
class LanguageSelector extends ViewModelWidget<OnboardingViewModel> { class LanguageSelector extends ViewModelWidget<OnboardingViewModel> {
const LanguageSelector({Key? key}) : super(key: key); const LanguageSelector({Key? key}) : super(key: key);
@ -26,7 +26,7 @@ class LanguageSelector extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildExpandedBody(OnboardingViewModel viewModel) => Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -43,7 +43,12 @@ class LanguageSelector extends ViewModelWidget<OnboardingViewModel> {
); );
List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) => List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(OnboardingViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -60,13 +65,20 @@ class LanguageSelector extends ViewModelWidget<OnboardingViewModel> {
_buildLanguages(viewModel) _buildLanguages(viewModel)
]; ];
Widget _buildAppBar() => const OnboardingAppBar(language: true); Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true,
onLanguage: () => viewModel.next(page: 23),
onTap: () => viewModel.pop(
language: true,
));
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Choose your language', 'Choose your language',
style: TextStyle( style: TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGreyColor, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -109,7 +121,7 @@ class LanguageSelector extends ViewModelWidget<OnboardingViewModel> {
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhiteColor, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () => viewModel.pop(language: true), onTap: () => viewModel.pop(language: true),
); );

View File

@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart';
class FirstWelcome extends ViewModelWidget<OnboardingViewModel> { class FirstWelcomeScreen extends ViewModelWidget<OnboardingViewModel> {
const FirstWelcome({super.key}); const FirstWelcomeScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -55,14 +53,17 @@ class FirstWelcome extends ViewModelWidget<OnboardingViewModel> {
_buildTitle(), _buildTitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg'); Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
height: 50,
);
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Small daily practice. Big lifelong results.', 'Small daily practice. Big lifelong results.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 30, fontSize: 30,
color: kcWhiteColor, color: kcWhite,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -80,11 +81,11 @@ class FirstWelcome extends ViewModelWidget<OnboardingViewModel> {
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
icon: true,
borderRadius: 12, borderRadius: 12,
text: 'Start Learning', text: 'Start Learning',
backgroundColor: kcWhite,
trailingIcon: Icons.arrow_forward,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhiteColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart';
class SecondWelcome extends ViewModelWidget<OnboardingViewModel> { class SecondWelcomeScreen extends ViewModelWidget<OnboardingViewModel> {
const SecondWelcome({super.key}); const SecondWelcomeScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -57,6 +55,7 @@ class SecondWelcome extends ViewModelWidget<OnboardingViewModel> {
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo.svg',
height: 50,
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
@ -64,7 +63,7 @@ class SecondWelcome extends ViewModelWidget<OnboardingViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 30, fontSize: 30,
color: kcWhiteColor, color: kcWhite,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -82,11 +81,11 @@ class SecondWelcome extends ViewModelWidget<OnboardingViewModel> {
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
icon: true,
borderRadius: 12, borderRadius: 12,
text: 'Start Learning', text: 'Start Learning',
backgroundColor: kcWhite,
trailingIcon: Icons.arrow_forward,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhiteColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart';
class ThirdWelcome extends ViewModelWidget<OnboardingViewModel> { class ThirdWelcomeScreen extends ViewModelWidget<OnboardingViewModel> {
const ThirdWelcome({super.key}); const ThirdWelcomeScreen({super.key});
@override @override
Widget build(BuildContext context, OnboardingViewModel viewModel) => Widget build(BuildContext context, OnboardingViewModel viewModel) =>
@ -54,15 +52,17 @@ class ThirdWelcome extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset(
Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg'); 'assets/icons/logo.svg',
height: 50,
);
Widget _buildTitle() => const Text( Widget _buildTitle() => const Text(
'Every conversation brings you closer to the life you want.', 'Every conversation brings you closer to the life you want.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 30, fontSize: 30,
color: kcWhiteColor, color: kcWhite,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
@ -80,11 +80,11 @@ class ThirdWelcome extends ViewModelWidget<OnboardingViewModel> {
Widget _buildContinueButton(OnboardingViewModel viewModel) => Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
icon: true,
borderRadius: 12, borderRadius: 12,
text: 'Start Learning', text: 'Start Learning',
backgroundColor: kcWhite,
trailingIcon: Icons.arrow_forward,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhiteColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_progress_section.dart';
import 'package:yimaru_app/ui/widgets/learning_progress_card.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'ongoing_progress_viewmodel.dart';
class OngoingProgressView extends StackedView<OngoingProgressViewModel> {
const OngoingProgressView({Key? key}) : super(key: key);
@override
OngoingProgressViewModel viewModelBuilder(
BuildContext context,
) =>
OngoingProgressViewModel();
@override
Widget builder(
BuildContext context,
OngoingProgressViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(OngoingProgressViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(OngoingProgressViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(OngoingProgressViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(OngoingProgressViewModel viewModel) =>
_buildNestedScrollView(viewModel);
Widget _buildNestedScrollView(OngoingProgressViewModel viewModel) =>
NestedScrollView(
scrollDirection: Axis.vertical,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) =>
[_buildSliverAppbarWrapper(viewModel)],
body: _buildContentScrollViewWrapper(viewModel));
Widget _buildSliverAppbarWrapper(OngoingProgressViewModel viewModel) =>
SliverAppBar(
pinned: true,
automaticallyImplyLeading: false,
backgroundColor: kcBackgroundColor,
surfaceTintColor: kcBackgroundColor,
title: _buildAppbar(viewModel),
);
Widget _buildAppbar(OngoingProgressViewModel viewModel) => SmallAppBar(
title: 'My Progress',
onTap: viewModel.pop,
);
Widget _buildContentScrollViewWrapper(OngoingProgressViewModel viewModel) =>
SingleChildScrollView(
child: _buildContentWrapper(viewModel),
);
Widget _buildContentWrapper(OngoingProgressViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildContentColumn(viewModel),
);
Widget _buildContentColumn(OngoingProgressViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildContentChildren(viewModel),
);
List<Widget> _buildContentChildren(OngoingProgressViewModel viewModel) => [
verticalSpaceMedium,
_buildText(),
verticalSpaceMedium,
_buildLearningProgressCard(),
verticalSpaceMedium,
_buildCourseProgressSection()
];
Widget _buildText() => const Text(
'Track your learning journey and see your growth over time.',
style: TextStyle(color: kcDarkGrey),
);
Widget _buildLearningProgressCard() => const LearningProgressCard();
Widget _buildCourseProgressSection() => const CourseProgressSection();
}

View File

@ -0,0 +1,21 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class OngoingProgressViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final List<Map<String, dynamic>> _courses = [
{
'title': 'IELTS Preparation',
},
{
'title': 'Duolingo English Test',
},
];
List<Map<String, dynamic>> get courses => _courses;
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/privacy_policy_tile.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'privacy_policy_viewmodel.dart';
class PrivacyPolicyView extends StackedView<PrivacyPolicyViewModel> {
const PrivacyPolicyView({Key? key}) : super(key: key);
@override
PrivacyPolicyViewModel viewModelBuilder(BuildContext context) =>
PrivacyPolicyViewModel();
@override
Widget builder(
BuildContext context,
PrivacyPolicyViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(PrivacyPolicyViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(PrivacyPolicyViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(PrivacyPolicyViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(PrivacyPolicyViewModel viewModel) =>
_buildColumn(viewModel);
Widget _buildColumn(PrivacyPolicyViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(PrivacyPolicyViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
verticalSpaceSmall,
_buildContentWrapper(viewModel)
];
Widget _buildAppBarWrapper(PrivacyPolicyViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar(
title: 'Privacy Policy',
onTap: viewModel.pop,
);
Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) =>
Expanded(child: _buildContentColumnWrapper(viewModel));
Widget _buildContentColumnWrapper(PrivacyPolicyViewModel viewModel) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildMenuColumnScrollView(viewModel),
);
Widget _buildMenuColumnScrollView(PrivacyPolicyViewModel viewModel) =>
SingleChildScrollView(
child: _buildMenuColumn(viewModel),
);
Widget _buildMenuColumn(PrivacyPolicyViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildMenuColumnChildren(viewModel),
);
List<Widget> _buildMenuColumnChildren(PrivacyPolicyViewModel viewModel) =>
[verticalSpaceLarge, _buildListView(viewModel)];
Widget _buildListView(PrivacyPolicyViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.privacyPolicies.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) =>
_buildTile(viewModel.privacyPolicies[index]['title']),
);
Widget _buildTile(String title) => PrivacyPolicyTile(title: title);
}

View File

@ -0,0 +1,23 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class PrivacyPolicyViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Privacy policy
final List<Map<String, dynamic>> _privacyPolicies = [
{'title': 'Introduction'},
{'title': 'Information We Collect'},
{'title': 'How We Use Your Information'},
{'title': 'Data Sharing and Disclosure'},
{'title': 'Your Rights and Choices'},
{'title': 'Data Security'}
];
List<Map<String, dynamic>> get privacyPolicies => _privacyPolicies;
// Navigation
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/profile_card.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
import '../../widgets/custom_elevated_button.dart';
import 'profile_viewmodel.dart';
class ProfileView extends StackedView<ProfileViewModel> {
const ProfileView({Key? key}) : super(key: key);
@override
ProfileViewModel viewModelBuilder(
BuildContext context,
) =>
ProfileViewModel();
@override
Widget builder(
BuildContext context,
ProfileViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(ProfileViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView(
child: _buildBody(viewModel),
);
Widget _buildBody(ProfileViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(ProfileViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildNotificationIconWrapper(),
_buildProfileSection(),
verticalSpaceSmall,
_buildViewProfileButton(viewModel),
verticalSpaceLarge,
_buildSettingsSection(viewModel),
verticalSpaceLarge,
_buildLogOutButton(viewModel),
verticalSpaceLarge,
],
);
Widget _buildNotificationIconWrapper() =>
Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon());
Widget _buildNotificationIcon() => const Icon(
Icons.notifications_none,
color: kcDarkGrey,
);
Widget _buildProfileSection() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildProfileSectionChildren(),
);
List<Widget> _buildProfileSectionChildren() => [
_buildProfileImage(),
verticalSpaceSmall,
_buildProfileName(),
];
Widget _buildProfileImage() => const ProfileImage();
Widget _buildProfileName() => const Text(
'Hi, Bisrat 👋',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildViewProfileButton(ProfileViewModel viewModel) =>
ViewProfileButton(
onTap: () async => await viewModel.navigateToProfileDetail(),
);
Widget _buildSettingsSection(ProfileViewModel viewModel) => GridView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, mainAxisSpacing: 15, crossAxisSpacing: 15),
children: _buildSettingsChildren(viewModel));
List<Widget> _buildSettingsChildren(ProfileViewModel viewModel) => [
_buildDownloadsCard(viewModel),
_buildProgressCard(viewModel),
_buildAccountCard(viewModel),
_buildSupportCard(viewModel)
];
Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard(
icon: Icons.download,
title: 'My Downloads',
subTitle: 'Access offline lessons and saved videos',
onTap: () async => await viewModel.navigateToDownloads(),
);
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
title: 'My Progress',
icon: Icons.stacked_bar_chart,
subTitle: 'Track your achievements and learning streak',
onTap: () async => await viewModel.navigateToProgress(),
);
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Account & Privacy',
icon: Icons.privacy_tip_outlined,
subTitle: 'Manage setting and app preference',
onTap: () async => await viewModel.navigateToAccountPrivacy(),
);
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Support',
icon: Icons.headphones,
subTitle: 'Get help through phone or Telegram',
onTap: () async => await viewModel.navigateToSupport(),
);
Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton(
height: 55,
text: 'Log Out',
borderRadius: 12,
foregroundColor: kcRed,
onTap: () async => await viewModel.logOut(),
backgroundColor: kcRed.withOpacity(0.25),
);
}

View File

@ -0,0 +1,32 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../../services/authentication_service.dart';
class ProfileViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
Future<void> logOut() async {
await _authenticationService.logOut();
await _navigationService.replaceWithLoginView();
}
Future<void> navigateToProfileDetail() async =>
await _navigationService.navigateToProfileDetailView();
Future<void> navigateToDownloads() async =>
await _navigationService.navigateToDownloadsView();
Future<void> navigateToProgress() async =>
await _navigationService.navigateToProgressView();
Future<void> navigateToAccountPrivacy() async =>
await _navigationService.navigateToAccountPrivacyView();
Future<void> navigateToSupport() async =>
await _navigationService.navigateToSupportView();
}

View File

@ -0,0 +1,569 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/widgets/birthday_selector.dart';
import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../common/validators/form_validator.dart';
import '../../widgets/custom_dropdown.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/profile_image.dart';
import 'profile_detail_viewmodel.dart';
import 'profile_detail_view.form.dart';
@FormView(fields: [
FormTextField(name: 'email', validator: FormValidator.validateForm),
FormTextField(
name: 'phoneNumber', validator: FormValidator.validatePhoneNumber),
FormTextField(name: 'lastName', validator: FormValidator.validateForm),
FormTextField(name: 'firstName', validator: FormValidator.validateForm),
])
class ProfileDetailView extends StackedView<ProfileDetailViewModel>
with $ProfileDetailView {
const ProfileDetailView({Key? key}) : super(key: key);
void _onModelReady() {
firstNameController.text = 'Abel';
lastNameController.text = 'Abebe';
phoneNumberController.text = '251900000000';
emailController.text = 'email@test.com';
}
@override
void onViewModelReady(ProfileDetailViewModel viewModel) {
_onModelReady();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
ProfileDetailViewModel viewModelBuilder(BuildContext context) =>
ProfileDetailViewModel();
@override
Widget builder(
BuildContext context,
ProfileDetailViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(ProfileDetailViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(ProfileDetailViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(ProfileDetailViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(ProfileDetailViewModel viewModel) => [
verticalSpaceMedium,
_buildAppbar(viewModel),
verticalSpaceSmall,
_buildColumnWrapper(viewModel)
];
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
title: 'Edit Profile',
onTap: viewModel.pop,
);
Widget _buildColumnWrapper(ProfileDetailViewModel viewModel) =>
Expanded(child: _buildBodyColumn(viewModel));
Widget _buildBodyColumn(ProfileDetailViewModel viewModel) =>
SingleChildScrollView(
child: _buildColumn(viewModel),
);
Widget _buildColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(ProfileDetailViewModel viewModel) => [
verticalSpaceMedium,
_buildProfileImage(),
verticalSpaceMedium,
_buildNameFormSection(viewModel),
verticalSpaceMedium,
_buildGenderFormFieldWrapper(viewModel),
verticalSpaceSmall,
_buildBirthdayColumn(viewModel),
verticalSpaceSmall,
_buildPhoneNumberFormFieldSection(viewModel),
verticalSpaceTiny,
_buildEmailFormFieldSection(viewModel),
verticalSpaceMedium,
_buildCountryRegionSection(viewModel),
verticalSpaceMedium,
_buildOccupationDropdownWrapper(viewModel),
verticalSpaceLarge,
_buildLowerColumn(viewModel)
];
Widget _buildProfileImage() =>
const Align(alignment: Alignment.center, child: ProfileImage());
Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildNameFormChildren(viewModel),
);
List<Widget> _buildNameFormChildren(ProfileDetailViewModel viewModel) => [
_buildFirstNameFormFieldWrapper(viewModel),
const SizedBox(width: 20),
_buildLastNameFormFieldWrapper(viewModel)
];
Widget _buildFirstNameFormFieldWrapper(ProfileDetailViewModel viewModel) =>
Expanded(child: _buildFirstNameFormFieldColumn(viewModel));
Widget _buildFirstNameFormFieldColumn(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildFirstNameFormFieldChildren(viewModel),
);
List<Widget> _buildFirstNameFormFieldChildren(
ProfileDetailViewModel viewModel) =>
[
_buildFirstNameLabel(),
verticalSpaceSmall,
_buildFirstNameFormField(viewModel),
if (viewModel.hasFirstNameValidationMessage && viewModel.focusFirstName)
verticalSpaceTiny,
if (viewModel.hasFirstNameValidationMessage && viewModel.focusFirstName)
_buildFirstNameValidatorWrapper(viewModel)
];
Widget _buildFirstNameLabel() => CustomFormLabel(
label: 'First Name',
style: style16DG600,
);
Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
controller: firstNameController,
onTap: viewModel.setFirstNameFocus,
decoration: inputDecoration(
focus: viewModel.focusFirstName,
filled: firstNameController.text.isNotEmpty),
);
Widget _buildFirstNameValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasFirstNameValidationMessage
? _buildFirstNameValidator(viewModel)
: Container();
Widget _buildFirstNameValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.firstNameValidationMessage!,
style: validationStyle,
);
Widget _buildLastNameFormFieldWrapper(ProfileDetailViewModel viewModel) =>
Expanded(child: _buildLastNameFormFieldColumn(viewModel));
Widget _buildLastNameFormFieldColumn(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLastNameFormFieldChildren(viewModel),
);
List<Widget> _buildLastNameFormFieldChildren(
ProfileDetailViewModel viewModel) =>
[
_buildLastNameLabel(),
verticalSpaceSmall,
_buildLastNameFormField(viewModel),
if (viewModel.hasLastNameValidationMessage && viewModel.focusLastName)
verticalSpaceTiny,
if (viewModel.hasLastNameValidationMessage && viewModel.focusLastName)
_buildLastNameValidatorWrapper(viewModel)
];
Widget _buildLastNameLabel() => CustomFormLabel(
label: 'Last Name',
style: style16DG600,
);
Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
controller: lastNameController,
onTap: viewModel.setLastNameFocus,
decoration: inputDecoration(
focus: viewModel.focusLastName,
filled: lastNameController.text.isNotEmpty),
);
Widget _buildLastNameValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasLastNameValidationMessage
? _buildLastNameValidator(viewModel)
: Container();
Widget _buildLastNameValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.lastNameValidationMessage!,
style: validationStyle,
);
Widget _buildGenderFormFieldWrapper(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildGenderFormFieldChildren(viewModel),
);
List<Widget> _buildGenderFormFieldChildren(
ProfileDetailViewModel viewModel) =>
[
_buildGenderLabel(),
verticalSpaceTiny,
_buildRadioButtonWrapper(viewModel),
];
Widget _buildGenderLabel() => CustomFormLabel(
label: 'Gender',
style: style16DG600,
);
Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row(
mainAxisSize: MainAxisSize.min,
children: _buildRadioButtonChildren(viewModel),
);
List<Widget> _buildRadioButtonChildren(ProfileDetailViewModel viewModel) =>
[_buildMaleRadioButton(viewModel), _buildFemaleRadioButton(viewModel)];
Widget _buildMaleRadioButton(ProfileDetailViewModel viewModel) =>
RadioGroup<String?>(
groupValue: viewModel.selectedGender,
onChanged: (value) => viewModel.setGender(value ?? ''),
child: _buildMaleRadioTileWrapper(viewModel));
Widget _buildMaleRadioTileWrapper(ProfileDetailViewModel viewModel) =>
Container(
width: 125,
alignment: Alignment.centerLeft,
child: _buildMaleRadioTile(viewModel));
Widget _buildMaleRadioTile(ProfileDetailViewModel viewModel) =>
RadioListTile<String?>(
value: 'Male',
title: _buildMaleTitle(),
activeColor: kcPrimaryColor,
contentPadding: EdgeInsets.zero,
);
Widget _buildMaleTitle() => const Text(
'Male',
style: TextStyle(
fontSize: 14,
color: kcDarkGrey,
fontWeight: FontWeight.w500,
),
);
Widget _buildFemaleRadioButton(ProfileDetailViewModel viewModel) =>
RadioGroup<String?>(
groupValue: viewModel.selectedGender,
onChanged: (value) => viewModel.setGender(value ?? ''),
child: _buildFemaleRadioTileWrapper(viewModel));
Widget _buildFemaleRadioTileWrapper(ProfileDetailViewModel viewModel) =>
Container(
width: 125,
alignment: Alignment.centerLeft,
child: _buildFemaleRadioTile(viewModel),
);
Widget _buildFemaleRadioTile(ProfileDetailViewModel viewModel) =>
RadioListTile<String?>(
value: 'Female',
title: _buildFemaleTitle(),
activeColor: kcPrimaryColor,
contentPadding: EdgeInsets.zero,
);
Widget _buildFemaleTitle() => const Text(
'Female',
style: TextStyle(
fontSize: 14,
color: kcDarkGrey,
fontWeight: FontWeight.w500,
),
);
Widget _buildBirthdayColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBirthdayChildren(viewModel),
);
List<Widget> _buildBirthdayChildren(ProfileDetailViewModel viewModel) => [
_buildBirthdayLabel(),
verticalSpaceSmall,
_buildBirthdayFormField(),
];
Widget _buildBirthdayLabel() => CustomFormLabel(
label: 'Birthday',
style: style16DG600,
);
Widget _buildBirthdayFormField() => const BirthdaySelector();
Widget _buildPhoneNumberFormFieldSection(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildPhoneNumberFormFieldChildren(viewModel),
);
List<Widget> _buildPhoneNumberFormFieldChildren(
ProfileDetailViewModel viewModel) =>
[
_buildPhoneNumberLabel(),
verticalSpaceSmall,
_buildPhoneNumberFormField(viewModel),
if (viewModel.hasPhoneNumberValidationMessage &&
viewModel.focusPhoneNumber)
verticalSpaceTiny,
if (viewModel.hasPhoneNumberValidationMessage &&
viewModel.focusPhoneNumber)
_buildPhoneNumberValidatorWrapper(viewModel)
];
Widget _buildPhoneNumberLabel() => CustomFormLabel(
label: 'Phone Number',
style: style16DG600,
);
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
maxLength: 12,
keyboardType: TextInputType.phone,
controller: phoneNumberController,
onTap: viewModel.setPhoneNumberFocus,
decoration: inputDecoration(
hint: '251',
focus: viewModel.focusPhoneNumber,
filled: phoneNumberController.text.isNotEmpty),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
);
Widget _buildPhoneNumberValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasPhoneNumberValidationMessage
? _buildPhoneNumberValidator(viewModel)
: Container();
Widget _buildPhoneNumberValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.phoneNumberValidationMessage!,
style: validationStyle,
);
Widget _buildEmailFormFieldSection(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildEmailFormFieldChildren(viewModel),
);
List<Widget> _buildEmailFormFieldChildren(ProfileDetailViewModel viewModel) =>
[
_buildEmailLabel(),
verticalSpaceSmall,
_buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
verticalSpaceTiny,
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
_buildEmailValidatorWrapper(viewModel)
];
Widget _buildEmailLabel() => CustomFormLabel(
label: 'Email',
style: style16DG600,
);
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
controller: emailController,
onTap: viewModel.setPhoneNumberFocus,
keyboardType: TextInputType.emailAddress,
decoration: inputDecoration(
focus: viewModel.focusEmail,
filled: emailController.text.isNotEmpty),
);
Widget _buildEmailValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasEmailValidationMessage
? _buildEmailValidator(viewModel)
: Container();
Widget _buildEmailValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.emailValidationMessage!,
style: validationStyle,
);
Widget _buildCountryRegionSection(ProfileDetailViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildCountryRegionChildren(viewModel),
);
List<Widget> _buildCountryRegionChildren(ProfileDetailViewModel viewModel) =>
[
_buildCountryDropdownColumnWrapper(viewModel),
const SizedBox(width: 20),
_buildRegionDropdownColumnWrapper(viewModel)
];
Widget _buildCountryDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
Expanded(
child: _buildCountryDropdownColumn(viewModel),
);
Widget _buildCountryDropdownColumn(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildCountryDropdownChildren(viewModel),
);
List<Widget> _buildCountryDropdownChildren(
ProfileDetailViewModel viewModel) =>
[
_buildCountryDropdownLabel(),
verticalSpaceSmall,
_buildCountryDropdown(viewModel)
];
Widget _buildCountryDropdownLabel() => CustomFormLabel(
label: 'Country',
style: style16DG600,
);
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
onChanged: (value) {},
hint: 'Select country',
selectedItem: 'Ethiopia',
items: (value, props) => viewModel.getCountries(),
);
Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
Expanded(
child: _buildRegionDropdownColumn(viewModel),
);
Widget _buildRegionDropdownColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildRegionDropdownChildren(viewModel),
);
List<Widget> _buildRegionDropdownChildren(ProfileDetailViewModel viewModel) =>
[
_buildRegionDropdownLabel(),
verticalSpaceSmall,
_buildRegionDropdown(viewModel)
];
Widget _buildRegionDropdownLabel() => CustomFormLabel(
label: 'Region',
style: style16DG600,
);
Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select region',
onChanged: (value) {},
selectedItem: 'Addis Ababa',
items: (value, props) => viewModel.getRegions('Addis Ababa'),
);
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildOccupationDropdownChildren(viewModel),
);
List<Widget> _buildOccupationDropdownChildren(
ProfileDetailViewModel viewModel) =>
[
_buildOccupationDropdownLabel(),
verticalSpaceSmall,
_buildOccupationDropdown(viewModel)
];
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
label: 'Occupation',
style: style16DG600,
);
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select occupation',
onChanged: (value) {},
selectedItem: 'Student',
items: (value, props) => viewModel.getOccupations('Student'),
);
Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [
_buildSaveButton(viewModel),
verticalSpaceSmall,
_buildCancelButtonWrapper(viewModel)
];
Widget _buildSaveButton(ProfileDetailViewModel viewModel) =>
const CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Save Changes',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildCancelButton(viewModel),
);
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
const CustomElevatedButton(
height: 55,
text: 'Cancel',
borderRadius: 12,
borderColor: kcPrimaryColor,
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,278 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// StackedFormGenerator
// **************************************************************************
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String EmailValueKey = 'email';
const String PhoneNumberValueKey = 'phoneNumber';
const String LastNameValueKey = 'lastName';
const String FirstNameValueKey = 'firstName';
final Map<String, TextEditingController>
_ProfileDetailViewTextEditingControllers = {};
final Map<String, FocusNode> _ProfileDetailViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_ProfileDetailViewTextValidations = {
EmailValueKey: FormValidator.validateForm,
PhoneNumberValueKey: FormValidator.validatePhoneNumber,
LastNameValueKey: FormValidator.validateForm,
FirstNameValueKey: FormValidator.validateForm,
};
mixin $ProfileDetailView {
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get phoneNumberController =>
_getFormTextEditingController(PhoneNumberValueKey);
TextEditingController get lastNameController =>
_getFormTextEditingController(LastNameValueKey);
TextEditingController get firstNameController =>
_getFormTextEditingController(FirstNameValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey);
FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey);
TextEditingController _getFormTextEditingController(
String key, {
String? initialValue,
}) {
if (_ProfileDetailViewTextEditingControllers.containsKey(key)) {
return _ProfileDetailViewTextEditingControllers[key]!;
}
_ProfileDetailViewTextEditingControllers[key] =
TextEditingController(text: initialValue);
return _ProfileDetailViewTextEditingControllers[key]!;
}
FocusNode _getFormFocusNode(String key) {
if (_ProfileDetailViewFocusNodes.containsKey(key)) {
return _ProfileDetailViewFocusNodes[key]!;
}
_ProfileDetailViewFocusNodes[key] = FocusNode();
return _ProfileDetailViewFocusNodes[key]!;
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
emailController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
@Deprecated(
'Use syncFormWithViewModel instead.'
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
emailController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Updates the formData on the FormViewModel
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
model.setData(
model.formValueMap
..addAll({
EmailValueKey: emailController.text,
PhoneNumberValueKey: phoneNumberController.text,
LastNameValueKey: lastNameController.text,
FirstNameValueKey: firstNameController.text,
}),
);
if (_autoTextFieldValidation || forceValidate) {
updateValidationData(model);
}
}
bool validateFormFields(FormViewModel model) {
_updateFormData(model, forceValidate: true);
return model.isFormValid;
}
/// Calls dispose on all the generated controllers and focus nodes
void disposeForm() {
// The dispose function for a TextEditingController sets all listeners to null
for (var controller in _ProfileDetailViewTextEditingControllers.values) {
controller.dispose();
}
for (var focusNode in _ProfileDetailViewFocusNodes.values) {
focusNode.dispose();
}
_ProfileDetailViewTextEditingControllers.clear();
_ProfileDetailViewFocusNodes.clear();
}
}
extension ValueProperties on FormStateHelper {
bool get hasAnyValidationMessage => this
.fieldsValidationMessages
.values
.any((validation) => validation != null);
bool get isFormValid {
if (!_autoTextFieldValidation) this.validateForm();
return !hasAnyValidationMessage;
}
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get phoneNumberValue =>
this.formValueMap[PhoneNumberValueKey] as String?;
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?;
set emailValue(String? value) {
this.setData(
this.formValueMap..addAll({EmailValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(EmailValueKey)) {
_ProfileDetailViewTextEditingControllers[EmailValueKey]?.text =
value ?? '';
}
}
set phoneNumberValue(String? value) {
this.setData(
this.formValueMap..addAll({PhoneNumberValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(
PhoneNumberValueKey)) {
_ProfileDetailViewTextEditingControllers[PhoneNumberValueKey]?.text =
value ?? '';
}
}
set lastNameValue(String? value) {
this.setData(
this.formValueMap..addAll({LastNameValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(
LastNameValueKey)) {
_ProfileDetailViewTextEditingControllers[LastNameValueKey]?.text =
value ?? '';
}
}
set firstNameValue(String? value) {
this.setData(
this.formValueMap..addAll({FirstNameValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(
FirstNameValueKey)) {
_ProfileDetailViewTextEditingControllers[FirstNameValueKey]?.text =
value ?? '';
}
}
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasPhoneNumber =>
this.formValueMap.containsKey(PhoneNumberValueKey) &&
(phoneNumberValue?.isNotEmpty ?? false);
bool get hasLastName =>
this.formValueMap.containsKey(LastNameValueKey) &&
(lastNameValue?.isNotEmpty ?? false);
bool get hasFirstName =>
this.formValueMap.containsKey(FirstNameValueKey) &&
(firstNameValue?.isNotEmpty ?? false);
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasPhoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false;
bool get hasLastNameValidationMessage =>
this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false;
bool get hasFirstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false;
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get phoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey];
String? get lastNameValidationMessage =>
this.fieldsValidationMessages[LastNameValueKey];
String? get firstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey];
}
extension Methods on FormStateHelper {
setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
setPhoneNumberValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage;
setLastNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[LastNameValueKey] = validationMessage;
setFirstNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FirstNameValueKey] = validationMessage;
/// Clears text input fields on the Form
void clearForm() {
emailValue = '';
phoneNumberValue = '';
lastNameValue = '';
firstNameValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
});
}
}
/// Returns the validation message for the given key
String? getValidationMessage(String key) {
final validatorForKey = _ProfileDetailViewTextValidations[key];
if (validatorForKey == null) return null;
String? validationMessageForKey = validatorForKey(
_ProfileDetailViewTextEditingControllers[key]!.text,
);
return validationMessageForKey;
}
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
});

View File

@ -0,0 +1,86 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class ProfileDetailViewModel extends FormViewModel {
final _navigationService = locator<NavigationService>();
// First name
bool _focusFirstName = false;
bool get focusFirstName => _focusFirstName;
// Last name
bool _focusLastName = false;
bool get focusLastName => _focusLastName;
// Gender
String? _selectedGender;
String? get selectedGender => _selectedGender;
// Birthday
String? _selectedBirthday;
String? get selectedBirthday => _selectedBirthday;
// First name
bool _focusPhoneNumber = false;
bool get focusPhoneNumber => _focusPhoneNumber;
// Email
bool _focusEmail = false;
bool get focusEmail => _focusEmail;
// First name
void setFirstNameFocus() {
_focusFirstName = true;
rebuildUi();
}
// Last name
void setLastNameFocus() {
_focusLastName = true;
rebuildUi();
}
// Gender
void setGender(String value) {
_selectedGender = value;
rebuildUi();
}
// Birthday
void setBirthday(String value) {
_selectedBirthday = value;
rebuildUi();
}
// Phone number
void setPhoneNumberFocus() {
_focusPhoneNumber = true;
rebuildUi();
}
// Email
void setEmailFocus() {
_focusEmail = true;
rebuildUi();
}
// Country
Future<List<String>> getCountries() async => ['Ethiopia', 'Djibouti'];
// Region
Future<List<String>> getRegions(String country) async =>
['Addis Ababa', 'Oromia'];
// Occupation
Future<List<String>> getOccupations(String country) async =>
['Student', 'Worker'];
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_level_card.dart';
import 'package:yimaru_app/ui/widgets/skill_progress.dart';
import 'package:yimaru_app/ui/widgets/suggestion_card.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'progress_viewmodel.dart';
class ProgressView extends StackedView<ProgressViewModel> {
const ProgressView({Key? key}) : super(key: key);
@override
ProgressViewModel viewModelBuilder(BuildContext context) =>
ProgressViewModel();
@override
Widget builder(
BuildContext context,
ProgressViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(ProgressViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(ProgressViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(ProgressViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(ProgressViewModel viewModel) => _buildColumn(viewModel);
Widget _buildColumn(ProgressViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(ProgressViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
verticalSpaceSmall,
_buildContentWrapper(viewModel)
];
Widget _buildAppBarWrapper(ProgressViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(ProgressViewModel viewModel) => SmallAppBar(
title: 'My Progress',
onTap: viewModel.pop,
);
Widget _buildContentWrapper(ProgressViewModel viewModel) =>
Expanded(child: _buildContentScrollView(viewModel));
Widget _buildContentScrollView(ProgressViewModel viewModel) =>
SingleChildScrollView(
child: _buildContentColumn(viewModel),
);
Widget _buildContentColumn(ProgressViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildContentChildren(viewModel),
);
List<Widget> _buildContentChildren(ProgressViewModel viewModel) => [
verticalSpaceMedium,
_buildCourseProgressSection(viewModel),
verticalSpaceMedium,
_buildSkillTitleWrapper(),
verticalSpaceMedium,
_buildSkillsWrapper(viewModel),
verticalSpaceLarge,
_buildSuggestionCard(),
verticalSpaceMassive
];
Widget _buildCourseProgressSection(ProgressViewModel viewModel) => SizedBox(
height: 250,
width: double.maxFinite,
child: _buildListView(viewModel),
);
Widget _buildListView(ProgressViewModel viewModel) => ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: viewModel.progresses.length,
controller: PageController(viewportFraction: 0.9),
itemBuilder: (context, index) => _buildCourseLeveCard(
viewModel: viewModel,
icon: viewModel.progresses[index]['icon'],
title: viewModel.progresses[index]['title'],
color: viewModel.progresses[index]['color'],
status: viewModel.progresses[index]['status'],
subTitle: viewModel.progresses[index]['subTitle'],
isCompleted: viewModel.progresses[index]['isCompleted'],
),
);
Widget _buildCourseLeveCard(
{required Color color,
required String title,
required String icon,
required String status,
required String subTitle,
required bool isCompleted,
required ProgressViewModel viewModel}) =>
CourseLevelCard(
icon: icon,
title: title,
color: color,
status: status,
subTitle: subTitle,
isCompleted: isCompleted,
onTap: viewModel.navigateToOngoingProgress,
);
Widget _buildSkillTitleWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildSkillTitle(),
);
Widget _buildSkillTitle() => const Text(
'Skill Proficiency',
style: TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSkillsWrapper(ProgressViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildSkills(viewModel),
);
Widget _buildSkills(ProgressViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.skillsLevel.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildSkill(
skill: viewModel.skillsLevel[index]['skill'],
progress: viewModel.skillsLevel[index]['progress'],
),
);
Widget _buildSkill({
required String skill,
required double progress,
}) =>
SkillProgress(
skill: skill,
progress: progress,
);
Widget _buildSuggestionCard() => const SuggestionCard();
}

View File

@ -0,0 +1,65 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import '../../../app/app.locator.dart';
class ProgressViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final List<Map<String, dynamic>> _progresses = [
{
'color': kcGreen,
'title': 'Beginner',
'isCompleted': true,
'status': 'Completed',
'icon': 'assets/icons/b1.svg',
'subTitle': 'Youve mastered everyday English basics!',
},
{
'title': 'Elementary',
'isCompleted': false,
'status': 'In Progress',
'color': kcPrimaryColor,
'icon': 'assets/icons/b1.svg',
'subTitle': 'Continue improving your conversations and fluency.',
},
{
'title': 'Beginner',
'isCompleted': true,
'status': 'In Progress',
'color': kcPrimaryColor,
'icon': 'assets/icons/b1.svg',
'subTitle': 'Youve mastered everyday English basics!',
},
];
List<Map<String, dynamic>> get progresses => _progresses;
final List<Map<String, dynamic>> _skillsLevel = [
{
'progress': 0.8,
'skill': 'Speaking',
},
{
'progress': 0.95,
'skill': 'Listening',
},
{
'progress': 0.75,
'skill': 'Writing',
},
{
'progress': 0.8,
'skill': 'Reading',
},
];
List<Map<String, dynamic>> get skillsLevel => _skillsLevel;
Future<void> navigateToOngoingProgress() async =>
await _navigationService.navigateToOngoingProgressView();
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/registration_otp_screen.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
import '../../common/app_colors.dart';
import '../../common/validators/form_validator.dart';
import 'register_viewmodel.dart';
import 'register_view.form.dart';
@FormView(fields: [
FormTextField(name: 'otp', validator: FormValidator.validateForm),
FormTextField(name: 'email', validator: FormValidator.validateEmail),
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm),
FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm),
])
class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
const RegisterView({Key? key}) : super(key: key);
@override
void onViewModelReady(RegisterViewModel viewModel) {
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
RegisterViewModel viewModelBuilder(BuildContext context) =>
RegisterViewModel();
@override
Widget builder(
BuildContext context,
RegisterViewModel viewModel,
Widget? child,
) =>
_buildRegisterScreensWrapper(viewModel);
Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope(
canPop: false,
onPopInvokedWithResult: (value, data) {
if (value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(
children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]);
Widget _buildScaffold(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(RegisterViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
);
Widget _buildExpandedBody(RegisterViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(RegisterViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildRegisterWithEmailScreen(),
_buildRegisterWithPhoneScreen(),
_buildCreatePasswordScreen(),
_buildRegistrationOtpScreen(),
];
Widget _buildRegisterWithEmailScreen() =>
RegisterWithEmailScreen(emailController: emailController);
Widget _buildRegisterWithPhoneScreen() => RegisterWithPhoneNumberScreen(
phoneNumberController: phoneNumberController);
Widget _buildRegistrationOtpScreen() => RegistrationOtpScreen(
otpController: otpController,
emailController: emailController,
phoneNumberController: phoneNumberController,
);
Widget _buildCreatePasswordScreen() => CreatePasswordScreen(
passwordController: passwordController,
confirmPasswordController: confirmPasswordController);
Widget _buildBusyRegistration(RegisterViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
}

View File

@ -0,0 +1,308 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// StackedFormGenerator
// **************************************************************************
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String OtpValueKey = 'otp';
const String EmailValueKey = 'email';
const String PasswordValueKey = 'password';
const String PhoneNumberValueKey = 'phoneNumber';
const String ConfirmPasswordValueKey = 'confirmPassword';
final Map<String, TextEditingController> _RegisterViewTextEditingControllers =
{};
final Map<String, FocusNode> _RegisterViewFocusNodes = {};
final Map<String, String? Function(String?)?> _RegisterViewTextValidations = {
OtpValueKey: FormValidator.validateForm,
EmailValueKey: FormValidator.validateEmail,
PasswordValueKey: FormValidator.validateForm,
PhoneNumberValueKey: FormValidator.validateForm,
ConfirmPasswordValueKey: FormValidator.validateForm,
};
mixin $RegisterView {
TextEditingController get otpController =>
_getFormTextEditingController(OtpValueKey);
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get phoneNumberController =>
_getFormTextEditingController(PhoneNumberValueKey);
TextEditingController get confirmPasswordController =>
_getFormTextEditingController(ConfirmPasswordValueKey);
FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
FocusNode get confirmPasswordFocusNode =>
_getFormFocusNode(ConfirmPasswordValueKey);
TextEditingController _getFormTextEditingController(
String key, {
String? initialValue,
}) {
if (_RegisterViewTextEditingControllers.containsKey(key)) {
return _RegisterViewTextEditingControllers[key]!;
}
_RegisterViewTextEditingControllers[key] =
TextEditingController(text: initialValue);
return _RegisterViewTextEditingControllers[key]!;
}
FocusNode _getFormFocusNode(String key) {
if (_RegisterViewFocusNodes.containsKey(key)) {
return _RegisterViewFocusNodes[key]!;
}
_RegisterViewFocusNodes[key] = FocusNode();
return _RegisterViewFocusNodes[key]!;
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
otpController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
@Deprecated(
'Use syncFormWithViewModel instead.'
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
otpController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Updates the formData on the FormViewModel
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
model.setData(
model.formValueMap
..addAll({
OtpValueKey: otpController.text,
EmailValueKey: emailController.text,
PasswordValueKey: passwordController.text,
PhoneNumberValueKey: phoneNumberController.text,
ConfirmPasswordValueKey: confirmPasswordController.text,
}),
);
if (_autoTextFieldValidation || forceValidate) {
updateValidationData(model);
}
}
bool validateFormFields(FormViewModel model) {
_updateFormData(model, forceValidate: true);
return model.isFormValid;
}
/// Calls dispose on all the generated controllers and focus nodes
void disposeForm() {
// The dispose function for a TextEditingController sets all listeners to null
for (var controller in _RegisterViewTextEditingControllers.values) {
controller.dispose();
}
for (var focusNode in _RegisterViewFocusNodes.values) {
focusNode.dispose();
}
_RegisterViewTextEditingControllers.clear();
_RegisterViewFocusNodes.clear();
}
}
extension ValueProperties on FormStateHelper {
bool get hasAnyValidationMessage => this
.fieldsValidationMessages
.values
.any((validation) => validation != null);
bool get isFormValid {
if (!_autoTextFieldValidation) this.validateForm();
return !hasAnyValidationMessage;
}
String? get otpValue => this.formValueMap[OtpValueKey] as String?;
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get phoneNumberValue =>
this.formValueMap[PhoneNumberValueKey] as String?;
String? get confirmPasswordValue =>
this.formValueMap[ConfirmPasswordValueKey] as String?;
set otpValue(String? value) {
this.setData(
this.formValueMap..addAll({OtpValueKey: value}),
);
if (_RegisterViewTextEditingControllers.containsKey(OtpValueKey)) {
_RegisterViewTextEditingControllers[OtpValueKey]?.text = value ?? '';
}
}
set emailValue(String? value) {
this.setData(
this.formValueMap..addAll({EmailValueKey: value}),
);
if (_RegisterViewTextEditingControllers.containsKey(EmailValueKey)) {
_RegisterViewTextEditingControllers[EmailValueKey]?.text = value ?? '';
}
}
set passwordValue(String? value) {
this.setData(
this.formValueMap..addAll({PasswordValueKey: value}),
);
if (_RegisterViewTextEditingControllers.containsKey(PasswordValueKey)) {
_RegisterViewTextEditingControllers[PasswordValueKey]?.text = value ?? '';
}
}
set phoneNumberValue(String? value) {
this.setData(
this.formValueMap..addAll({PhoneNumberValueKey: value}),
);
if (_RegisterViewTextEditingControllers.containsKey(PhoneNumberValueKey)) {
_RegisterViewTextEditingControllers[PhoneNumberValueKey]?.text =
value ?? '';
}
}
set confirmPasswordValue(String? value) {
this.setData(
this.formValueMap..addAll({ConfirmPasswordValueKey: value}),
);
if (_RegisterViewTextEditingControllers.containsKey(
ConfirmPasswordValueKey)) {
_RegisterViewTextEditingControllers[ConfirmPasswordValueKey]?.text =
value ?? '';
}
}
bool get hasOtp =>
this.formValueMap.containsKey(OtpValueKey) &&
(otpValue?.isNotEmpty ?? false);
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasPhoneNumber =>
this.formValueMap.containsKey(PhoneNumberValueKey) &&
(phoneNumberValue?.isNotEmpty ?? false);
bool get hasConfirmPassword =>
this.formValueMap.containsKey(ConfirmPasswordValueKey) &&
(confirmPasswordValue?.isNotEmpty ?? false);
bool get hasOtpValidationMessage =>
this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false;
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasPhoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false;
bool get hasConfirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ??
false;
String? get otpValidationMessage =>
this.fieldsValidationMessages[OtpValueKey];
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get phoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey];
String? get confirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey];
}
extension Methods on FormStateHelper {
setOtpValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[OtpValueKey] = validationMessage;
setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setPhoneNumberValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage;
setConfirmPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ConfirmPasswordValueKey] =
validationMessage;
/// Clears text input fields on the Form
void clearForm() {
otpValue = '';
emailValue = '';
passwordValue = '';
phoneNumberValue = '';
confirmPasswordValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
OtpValueKey: getValidationMessage(OtpValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});
}
}
/// Returns the validation message for the given key
String? getValidationMessage(String key) {
final validatorForKey = _RegisterViewTextValidations[key];
if (validatorForKey == null) return null;
String? validationMessageForKey = validatorForKey(
_RegisterViewTextEditingControllers[key]!.text,
);
return validationMessageForKey;
}
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
OtpValueKey: getValidationMessage(OtpValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});

View File

@ -0,0 +1,310 @@
import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/api_service.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/home/home_view.dart';
import 'package:yimaru_app/ui/views/login/login_view.dart';
import '../../../app/app.locator.dart';
import '../../../models/user_model.dart';
class RegisterViewModel extends FormViewModel {
final _apiService = locator<ApiService>();
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
// Navigation
int _currentIndex = 0;
int get currentIndex => _currentIndex;
// Email
bool _focusEmail = false;
bool get focusEmail => _focusEmail;
// Password
bool _length = false;
bool get length => _length;
bool _number = false;
bool get number => _number;
bool _specialChar = false;
bool get specialChar => _specialChar;
bool _focusPassword = false;
bool get focusPassword => _focusPassword;
bool _obscurePassword = true;
bool get obscurePassword => _obscurePassword;
bool _passwordMatch = false;
bool get passwordMatch => _passwordMatch;
// Confirm password
bool _focusConfirmPassword = false;
bool get focusConfirmPassword => _focusConfirmPassword;
bool _obscureConfirmPassword = true;
bool get obscureConfirmPassword => _obscureConfirmPassword;
// Phone number
bool _focusPhoneNumber = false;
bool get focusPhoneNumber => _focusPhoneNumber;
// Terms and conditions
bool _agree = false;
bool get agree => _agree;
// Focus otp
bool _focusOtp = false;
bool get focusOtp => _focusOtp;
// Focus node
final FocusNode _focusNode = FocusNode();
FocusNode get focusNode => _focusNode;
// Registration type
RegistrationType? _registrationType;
RegistrationType? get registrationType => _registrationType;
// Resend button state
bool _buttonActive = false;
bool get buttonActive => _buttonActive;
DateTime _resendTime =
DateTime.now().add(const Duration(minutes: 3, seconds: 0));
DateTime get resendTime => _resendTime;
// User data
final Map<String, dynamic> _userData = {};
Map<String, dynamic> get userData => _userData;
// Email
void setEmailFocus() {
_focusEmail = true;
rebuildUi();
}
// Password
void setPasswordFocus() {
_focusPassword = true;
rebuildUi();
}
void validatePassword(
{required String password, required String confirmPassword}) {
if (password.length > 8) {
_length = true;
} else {
_length = false;
}
if (RegExp(r'\d').hasMatch(password)) {
_number = true;
} else {
_number = false;
}
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
_specialChar = true;
} else {
_specialChar = false;
}
if (password == confirmPassword) {
_passwordMatch = true;
} else {
_passwordMatch = false;
}
rebuildUi();
}
double validationProgress() {
int completed = 0;
if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++;
return completed / 4; // returns 0.0 1.0
}
void setObscurePassword() {
_obscurePassword = !_obscurePassword;
rebuildUi();
}
// Confirm password
void setConfirmPasswordFocus() {
_focusConfirmPassword = true;
rebuildUi();
}
void setObscureConfirmPassword() {
_obscureConfirmPassword = !_obscureConfirmPassword;
rebuildUi();
}
// Phone number
void setPhoneNumberFocus() {
_focusPhoneNumber = true;
rebuildUi();
}
// Otp
void setOtpFocus() {
_focusOtp = true;
rebuildUi();
}
// Terms and Conditions
void setAgreement(bool value) {
_agree = value;
rebuildUi();
}
void setResendButton() {
_buttonActive = true;
rebuildUi();
}
void resetButton() {
_buttonActive = false;
_resendTime = DateTime.now().add(const Duration(minutes: 3, seconds: 0));
}
// Validate otp
Future<void> validateOtp(String otp) async {}
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// Remote api calls
Future<void> register() async {
Map<String, dynamic> response = await runBusyFuture<Map<String, dynamic>>(
_apiService.register(_userData));
if (response['status'] == ResponseStatus.success) {
goTo(page: 3);
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
Future<void> verifyOtp() async {
Map<String, dynamic> response =
await runBusyFuture<Map<String, dynamic>>(_verifyOtp());
if (response['status'] == ResponseStatus.success) {
await replaceWithHome();
}
}
Future<Map<String, dynamic>> _verifyOtp() async {
Map<String, dynamic> response = await _apiService.verifyOtp(_userData);
if (response['status'] == ResponseStatus.success) {
// UserModel user = response['data'] as UserModel;
// Map<String, dynamic> data = {
// 'userId': user.userId,
// 'accessToken': user.accessToken,
// 'refreshToken': user.refreshToken
// };
await _authenticationService.saveUserData({
'userId': 10,
'accessToken': 'accessToken',
'refreshToken': 'refreshToken'
});
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
return response;
}
Future<void> resendOtp() async {
resetButton();
Map<String, dynamic> response = await runBusyFuture<Map<String, dynamic>>(
_apiService.resendOtp(_userData));
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
// Navigation
void goTo({required int page, RegistrationType? type}) {
_currentIndex = page;
if (type != null) {
_registrationType = type;
}
rebuildUi();
}
void goBack() {
if (_currentIndex == 1) {
_currentIndex = 0;
rebuildUi();
} else if (_currentIndex == 2) {
_currentIndex = 0;
rebuildUi();
} else if (_currentIndex == 3) {
if (_registrationType == RegistrationType.phone) {
_currentIndex = 1;
} else {
_currentIndex = 2;
}
rebuildUi();
} else {
_navigationService.back();
}
}
Future<void> navigateToTermsAndConditions() async =>
await _navigationService.navigateToTermsAndConditionsView();
Future<void> navigateToPrivacyPolicy() async =>
await _navigationService.navigateToPrivacyPolicyView();
Future<void> replaceToLogin() async =>
await _navigationService.replaceWithLoginView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
}

View File

@ -0,0 +1,255 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/register/register_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/validator_list_tile.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/obscure_password.dart';
import '../register_view.form.dart';
class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
final TextEditingController passwordController;
final TextEditingController confirmPasswordController;
const CreatePasswordScreen(
{super.key,
required this.passwordController,
required this.confirmPasswordController});
Future<void> _signUp(RegisterViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'role': 'STUDENT',
'otp_medium': 'email',
'password': passwordController.text,
};
viewModel.addUserData(data);
await viewModel.register();
}
@override
Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBodyChildren(viewModel);
Widget _buildBodyChildren(RegisterViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(RegisterViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildPasswordLabel('Password'),
verticalSpaceSmall,
_buildPasswordFormField(viewModel),
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
verticalSpaceTiny,
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
_buildPasswordValidationWrapper(viewModel),
verticalSpaceMedium,
_buildPasswordLabel('Confirm Password'),
verticalSpaceSmall,
_buildConfirmPasswordFormField(viewModel),
if (viewModel.hasConfirmPasswordValidationMessage &&
viewModel.focusConfirmPassword)
verticalSpaceTiny,
if (viewModel.hasConfirmPasswordValidationMessage &&
viewModel.focusConfirmPassword)
_buildConfirmPasswordValidationWrapper(viewModel),
verticalSpaceMedium,
_buildLinearProgressIndicator(viewModel),
verticalSpaceSmall,
_buildCharLengthValidator(viewModel),
_buildNumberValidator(viewModel),
_buildSymbolValidator(viewModel),
_buildPasswordMatchValidator(viewModel),
_buildCheckBox(viewModel),
verticalSpaceSmall,
_buildSignUpButton(viewModel),
verticalSpaceMedium
];
Widget _buildTitle() => const Text(
'Create Password',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildPasswordLabel(String label) => CustomFormLabel(
label: label,
style: style14DG400,
);
Widget _buildPasswordFormField(RegisterViewModel viewModel) => TextFormField(
controller: passwordController,
onTap: viewModel.setPasswordFocus,
obscureText: viewModel.obscurePassword,
decoration: inputDecoration(
hint: 'Password',
focus: viewModel.focusPassword,
suffix: _buildObscurePassword(viewModel),
filled: passwordController.text.isNotEmpty),
onChanged: (value) => viewModel.validatePassword(
password: passwordController.text,
confirmPassword: confirmPasswordController.text),
);
Widget _buildObscurePassword(RegisterViewModel viewModel) => ObscurePassword(
focus: viewModel.focusPassword,
obscure: viewModel.obscurePassword,
onTap: viewModel.setObscurePassword,
);
Widget _buildPasswordValidationWrapper(RegisterViewModel viewModel) =>
viewModel.hasPasswordValidationMessage
? _buildPasswordValidator(viewModel)
: Container();
Widget _buildPasswordValidator(RegisterViewModel viewModel) => Text(
viewModel.passwordValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildConfirmPasswordFormField(RegisterViewModel viewModel) =>
TextFormField(
controller: confirmPasswordController,
onTap: viewModel.setConfirmPasswordFocus,
obscureText: viewModel.obscureConfirmPassword,
onChanged: (value) => viewModel.validatePassword(
password: passwordController.text,
confirmPassword: confirmPasswordController.text),
decoration: inputDecoration(
hint: 'Confirm Password',
focus: viewModel.focusConfirmPassword,
suffix: _buildObscureConfirmPassword(viewModel),
filled: confirmPasswordController.text.isNotEmpty),
);
Widget _buildObscureConfirmPassword(RegisterViewModel viewModel) =>
ObscurePassword(
focus: viewModel.focusConfirmPassword,
obscure: viewModel.obscureConfirmPassword,
onTap: viewModel.setObscureConfirmPassword,
);
Widget _buildConfirmPasswordValidationWrapper(RegisterViewModel viewModel) =>
viewModel.hasConfirmPasswordValidationMessage
? _buildConfirmPasswordValidator(viewModel)
: Container();
Widget _buildConfirmPasswordValidator(RegisterViewModel viewModel) => Text(
viewModel.confirmPasswordValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildLinearProgressIndicator(RegisterViewModel viewModel) =>
CustomLinearProgressIndicator(
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
progress: viewModel.validationProgress(),
);
Widget _buildCharLengthValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey,
label: '8 characters minimum');
Widget _buildNumberValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey,
label: 'a number');
Widget _buildSymbolValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey,
label: 'one symbol minimum');
Widget _buildPasswordMatchValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
backgroundColor:
viewModel.passwordMatch ? kcPrimaryColor : kcLightGrey,
label: 'password match');
Widget _buildCheckBox(RegisterViewModel viewMode) => CheckboxListTile(
value: viewMode.agree,
activeColor: kcPrimaryColor,
title: _buildCheckBoxTitle(viewMode),
controlAffinity: ListTileControlAffinity.leading,
onChanged: (value) => viewMode.setAgreement(value ?? false));
Widget _buildCheckBoxTitle(RegisterViewModel viewMode) => Text.rich(
TextSpan(
text: 'By clicking "Sign Up", you agree to our',
style: style14DG400,
children: [
TextSpan(
text: ' Terms of Service',
style: style14P600,
recognizer: TapGestureRecognizer()
..onTap = () => viewMode.navigateToTermsAndConditions()),
TextSpan(text: ' and ', style: style14DG400),
TextSpan(
text: 'Privacy Policy',
style: style14P600,
recognizer: TapGestureRecognizer()
..onTap = () => viewMode.navigateToPrivacyPolicy()),
]),
);
Widget _buildSignUpButton(RegisterViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Sign Up',
borderRadius: 12,
foregroundColor: kcWhite,
onTap:
(viewModel.focusPassword && passwordController.text.isNotEmpty) &&
(viewModel.focusConfirmPassword &&
confirmPasswordController.text.isNotEmpty) &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch &&
viewModel.agree
? () async => await _signUp(viewModel)
: null,
backgroundColor:
(viewModel.focusPassword && passwordController.text.isNotEmpty) &&
(viewModel.focusConfirmPassword &&
confirmPasswordController.text.isNotEmpty) &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch &&
viewModel.agree
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/login_account.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/option_text_divider.dart';
import '../register_viewmodel.dart';
import '../register_view.form.dart';
class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
final TextEditingController emailController;
const RegisterWithEmailScreen({
super.key,
required this.emailController,
});
void _addUserData(RegisterViewModel viewModel) {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'email': emailController.text,
};
viewModel.addUserData(data);
viewModel.goTo(page: 2, type: RegistrationType.email);
}
@override
Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(RegisterViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildColumnScroller(RegisterViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitleWrapper(viewModel),
verticalSpaceLarge,
_buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
verticalSpaceTiny,
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
_buildEmailValidatorWrapper(viewModel),
];
Widget _buildTitle() => const Text(
'Create an Account',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount(
onTap: () async => await viewModel.replaceToLogin(),
);
Widget _buildEmailFormField(RegisterViewModel viewModel) => TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
onTap: viewModel.setEmailFocus,
decoration: inputDecoration(
hint: 'Email',
focus: viewModel.focusEmail,
filled: emailController.text.isNotEmpty),
);
Widget _buildEmailValidatorWrapper(RegisterViewModel viewModel) =>
viewModel.hasEmailValidationMessage
? _buildEmailValidator(viewModel)
: Container();
Widget _buildEmailValidator(RegisterViewModel viewModel) => Text(
viewModel.emailValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildLowerColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(RegisterViewModel viewModel) => [
_buildContinueButton(viewModel),
_buildOptionTextDivider(),
_buildRegisterWithEmailButton(viewModel),
verticalSpaceMedium
];
Widget _buildContinueButton(RegisterViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: viewModel.focusEmail &&
emailController.text.isNotEmpty &&
!viewModel.hasEmailValidationMessage
? () => _addUserData(viewModel)
: null,
backgroundColor: viewModel.focusEmail &&
emailController.text.isNotEmpty &&
!viewModel.hasEmailValidationMessage
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildRegisterWithEmailButton(RegisterViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
leadingIcon: Icons.phone,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
text: 'Register with Phone Number',
onTap: () => viewModel.goTo(page: 1),
);
}

View File

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/login_account.dart';
import 'package:yimaru_app/ui/widgets/option_text_divider.dart';
import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/phone_number_prefix.dart';
import '../register_viewmodel.dart';
import '../register_view.form.dart';
class RegisterWithPhoneNumberScreen extends ViewModelWidget<RegisterViewModel> {
final TextEditingController phoneNumberController;
const RegisterWithPhoneNumberScreen(
{super.key, required this.phoneNumberController});
@override
Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(RegisterViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildColumnScroller(RegisterViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitleWrapper(viewModel),
verticalSpaceMedium,
_buildSubtitle(),
verticalSpaceMedium,
_buildPhoneNumberWrapper(viewModel),
if (viewModel.hasPhoneNumberValidationMessage &&
viewModel.focusPhoneNumber)
verticalSpaceTiny,
if (viewModel.hasPhoneNumberValidationMessage &&
viewModel.focusPhoneNumber)
_buildPhoneNumberValidatorWrapper(viewModel),
];
Widget _buildTitle() => const Text(
'Create an Account',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount(
onTap: () async => await viewModel.replaceToLogin(),
);
Widget _buildSubtitle() => const Text(
'Enter your phone number. We will send you a confirmation code there',
style: TextStyle(color: kcMediumGrey),
);
Widget _buildPhoneNumberWrapper(RegisterViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildPhoneNumberChildren(viewModel),
);
List<Widget> _buildPhoneNumberChildren(RegisterViewModel viewModel) => [
_buildPhoneNumberPrefix(viewModel),
horizontalSpaceSmall,
_buildPhoneNumberFormFieldWrapper(viewModel),
];
Widget _buildPhoneNumberPrefix(RegisterViewModel viewModel) =>
PhoneNumberPrefix(selected: viewModel.focusPhoneNumber);
Widget _buildPhoneNumberFormFieldWrapper(RegisterViewModel viewModel) =>
Expanded(child: _buildPhoneNumberFormField(viewModel));
Widget _buildPhoneNumberFormField(RegisterViewModel viewModel) =>
TextFormField(
maxLength: 9,
keyboardType: TextInputType.phone,
controller: phoneNumberController,
onTap: viewModel.setPhoneNumberFocus,
decoration: inputDecoration(
focus: viewModel.focusPhoneNumber,
filled: phoneNumberController.text.isNotEmpty),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
);
Widget _buildPhoneNumberValidatorWrapper(RegisterViewModel viewModel) =>
viewModel.hasPhoneNumberValidationMessage
? _buildPhoneNumberValidator(viewModel)
: Container();
Widget _buildPhoneNumberValidator(RegisterViewModel viewModel) => Text(
viewModel.phoneNumberValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildLowerColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(RegisterViewModel viewModel) => [
_buildContinueButton(viewModel),
_buildOptionTextDivider(),
_buildRegisterWitPhoneNumberButton(viewModel),
verticalSpaceMedium
];
Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildContinueButton(RegisterViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap:
viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty
? () => viewModel.goTo(page: 3, type: RegistrationType.phone)
: null,
backgroundColor:
viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildRegisterWitPhoneNumberButton(RegisterViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
leadingIcon: Icons.email,
borderColor: kcPrimaryColor,
text: 'Register with Email',
foregroundColor: kcPrimaryColor,
onTap: () => viewModel.goTo(page: 0),
);
}

View File

@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
import 'package:flutter_timer_countdown/flutter_timer_countdown.dart';
import 'package:pinput/pinput.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/register/register_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../register_view.form.dart';
class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
final TextEditingController otpController;
final TextEditingController emailController;
final TextEditingController phoneNumberController;
const RegistrationOtpScreen(
{super.key,
required this.otpController,
required this.emailController,
required this.phoneNumberController});
Future<void> _verifyOtp(RegisterViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'otp': otpController.text,
'email': emailController.text,
};
viewModel.clearUserData();
viewModel.addUserData(data);
await viewModel.verifyOtp();
}
@override
Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(RegisterViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(RegisterViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubtitleWrapper(),
verticalSpaceMedium,
_buildPinPutWrapper(viewModel),
if (viewModel.hasOtpValidationMessage && viewModel.focusOtp)
verticalSpaceTiny,
if (viewModel.hasOtpValidationMessage && viewModel.focusOtp)
_buildOtpValidatorWrapper(viewModel),
verticalSpaceSmall,
_buildTimerWrapper(viewModel)
];
Widget _buildTitle() => Text(
'Verification Code',
style: style25DG600,
);
Widget _buildSubtitleWrapper() =>
phoneNumberController.text.length == 9 ? _buildSubtitle() : Container();
Widget _buildSubtitle() => Text(
'Code sent to your number +251${phoneNumberController.text.substring(0, 5)}****',
style: style14DG400,
);
Widget _buildPinPutWrapper(RegisterViewModel viewModel) => Center(
child: _buildPinPut(viewModel),
);
Widget _buildPinPut(RegisterViewModel viewModel) => Pinput(
length: 6,
controller: otpController,
defaultPinTheme: defaultPin,
cursor: const CustomCursor(),
errorPinTheme: errorPinTheme,
onTap: viewModel.setOtpFocus,
focusNode: viewModel.focusNode,
errorTextStyle: validationStyle,
//smsRetriever: locator<KewedeSmsRetriever>(),
focusedPinTheme: focusedThemePin,
submittedPinTheme: submittedThemePin,
hapticFeedbackType: HapticFeedbackType.heavyImpact,
separatorBuilder: (index) => const SizedBox(width: 10),
onCompleted: (otp) async => await _verifyOtp(viewModel),
);
Widget _buildOtpValidatorWrapper(RegisterViewModel viewModel) =>
viewModel.hasOtpValidationMessage
? _buildOtpValidator(viewModel)
: Container();
Widget _buildOtpValidator(RegisterViewModel viewModel) => Text(
viewModel.otpValidationMessage!,
style: style12R700,
);
Widget _buildTimerWrapper(RegisterViewModel viewModel) =>
!viewModel.buttonActive
? _buildTimerSection(viewModel)
: _buildResendButton(viewModel);
Widget _buildResendButton(RegisterViewModel viewModel) => TextButton(
onPressed: () async => await viewModel.resendOtp(),
child: _buildResendText());
Widget _buildResendText() => Text(
'Resend code',
style: style14P600.copyWith(fontStyle: FontStyle.italic),
);
Widget _buildTimerSection(RegisterViewModel viewModel) => Row(
children: [
_buildCountdownText(),
horizontalSpaceSmall,
_buildTimer(viewModel)
],
);
Widget _buildCountdownText() => Text('Resend code in ', style: style14DG400);
Widget _buildTimer(RegisterViewModel viewModel) => TimerCountdown(
enableDescriptions: false,
timeTextStyle: style14P600,
endTime: viewModel.resendTime,
onEnd: viewModel.setResendButton,
format: CountDownTimerFormat.minutesSeconds,
colonsTextStyle: const TextStyle(color: kcPrimaryColor),
);
Widget _buildContinueButtonWrapper(RegisterViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(RegisterViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: viewModel.focusOtp &&
otpController.text.length == 6 &&
!viewModel.hasOtpValidationMessage
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.focusOtp &&
otpController.text.length == 6 &&
!viewModel.hasOtpValidationMessage
? () async => await _verifyOtp(viewModel)
: null,
);
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import 'startup_viewmodel.dart'; import 'startup_viewmodel.dart';
@ -47,7 +48,9 @@ class StartupView extends StackedView<StartupViewModel> {
); );
List<Widget> _buildUpperColumnChildren() => List<Widget> _buildUpperColumnChildren() =>
[_buildIconWrapper(), _buildLoadingTextContainer()]; [_buildIconWrapper(), _buildSafeWrapper()];
Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer());
Widget _buildLoadingTextContainer() => Padding( Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50), padding: const EdgeInsets.only(bottom: 50),
@ -66,8 +69,8 @@ class StartupView extends StackedView<StartupViewModel> {
_buildIndicatorWrapper(), _buildIndicatorWrapper(),
]; ];
Widget _buildLoadingText() => const Text('Loading ...', Widget _buildLoadingText() =>
style: TextStyle(color: kcWhiteColor, fontSize: 16)); const Text('Loading ...', style: TextStyle(color: kcWhite, fontSize: 16));
Widget _buildIndicatorWrapper() => SizedBox( Widget _buildIndicatorWrapper() => SizedBox(
width: 16, width: 16,
@ -75,10 +78,8 @@ class StartupView extends StackedView<StartupViewModel> {
child: _buildIndicator(), child: _buildIndicator(),
); );
Widget _buildIndicator() => const CircularProgressIndicator( Widget _buildIndicator() =>
strokeWidth: 6, const CustomCircularProgressIndicator(color: kcWhite);
color: kcWhiteColor,
);
Widget _buildIconWrapper() => Padding( Widget _buildIconWrapper() => Padding(
padding: const EdgeInsets.only(top: 100), padding: const EdgeInsets.only(top: 100),
@ -87,6 +88,7 @@ class StartupView extends StackedView<StartupViewModel> {
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo.svg',
height: 50,
); );
@override @override

View File

@ -1,19 +1,21 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../app/app.router.dart'; import '../../../app/app.router.dart';
class StartupViewModel extends BaseViewModel { class StartupViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
// Place anything here that needs to happen before we get into the application // Place anything here that needs to happen before we get into the application
Future runStartupLogic() async { Future runStartupLogic() async {
await Future.delayed(const Duration(seconds: 3)); final response = await _authenticationService.userLoggedIn();
if (response) {
// This is where you can make decisions on where your app should navigate when _navigationService.replaceWithHomeView();
// you have custom startup logic } else {
_navigationService.replaceWithLoginView();
_navigationService.replaceWithOnboardingView(); }
} }
} }

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/support_card.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'support_viewmodel.dart';
class SupportView extends StackedView<SupportViewModel> {
const SupportView({Key? key}) : super(key: key);
@override
SupportViewModel viewModelBuilder(BuildContext context) => SupportViewModel();
@override
Widget builder(
BuildContext context,
SupportViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(SupportViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(SupportViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(SupportViewModel viewModel) => _buildBody(viewModel);
Widget _buildBody(SupportViewModel viewModel) => _buildColumn(viewModel);
Widget _buildColumn(SupportViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(SupportViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
verticalSpaceSmall,
_buildContentWrapper(viewModel)
];
Widget _buildAppBarWrapper(SupportViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar(
title: 'Need Help?',
onTap: viewModel.pop,
);
Widget _buildContentWrapper(SupportViewModel viewModel) =>
Expanded(child: _buildContentColumnWrapper(viewModel));
Widget _buildContentColumnWrapper(SupportViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildMenuColumnScrollView(viewModel),
);
Widget _buildMenuColumnScrollView(SupportViewModel viewModel) =>
SingleChildScrollView(
child: _buildMenuColumn(viewModel),
);
Widget _buildMenuColumn(SupportViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildMenuColumnChildren(viewModel),
);
List<Widget> _buildMenuColumnChildren(SupportViewModel viewModel) => [
verticalSpaceLarge,
_buildCallSupport(viewModel),
verticalSpaceMedium,
_buildTelegramSupport(viewModel)
];
Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard(
icon: Icons.call,
color: kcPrimaryColor,
title: 'Call Support',
subtitle: 'Talk with our support team directly',
onTap: () async => await viewModel.navigateToCallSupport(),
);
Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard(
color: kcSkyBlue,
icon: Icons.telegram,
title: 'Telegram Support',
subtitle: 'Chat Instantly via Telegram',
onTap: () async => await viewModel.navigateToTelegramSupport(),
);
}

View File

@ -0,0 +1,18 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
class SupportViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToTelegramSupport() async =>
await _navigationService.navigateToTelegramSupportView();
Future<void> navigateToCallSupport() async =>
await _navigationService.navigateToCallSupportView();
}

View File

@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/circular_icon.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/option_text_divider.dart';
import '../../widgets/small_app_bar.dart';
import 'telegram_support_viewmodel.dart';
class TelegramSupportView extends StackedView<TelegramSupportViewModel> {
const TelegramSupportView({Key? key}) : super(key: key);
@override
TelegramSupportViewModel viewModelBuilder(BuildContext context) =>
TelegramSupportViewModel();
@override
Widget builder(
BuildContext context,
TelegramSupportViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(TelegramSupportViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(TelegramSupportViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(TelegramSupportViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(TelegramSupportViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
_buildExpandedColumn(viewModel)
];
Widget _buildAppBarWrapper(TelegramSupportViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(TelegramSupportViewModel viewModel) => SmallAppBar(
title: 'Telegram Support',
onTap: viewModel.pop,
);
Widget _buildExpandedColumn(TelegramSupportViewModel viewModel) =>
Expanded(child: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper(TelegramSupportViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(TelegramSupportViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(TelegramSupportViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(TelegramSupportViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(TelegramSupportViewModel viewModel) =>
[
verticalSpaceLarge,
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
];
Widget _buildIcon() =>
const CircularIcon(icon: Icons.telegram, size: 50, color: kcSkyBlue);
Widget _buildTitle() => const Text(
'Join Yimaru Academy on Telegram',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitle() => const Text(
'Connect with our support team instantly on Telegram for quick assistance and community updates',
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLowerColumn(TelegramSupportViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(TelegramSupportViewModel viewModel) =>
[
_buildContinueButton(viewModel),
verticalSpaceSmall,
_buildOptionTextDivider(),
verticalSpaceSmall,
_buildSearchText(),
verticalSpaceMedium
];
Widget _buildContinueButton(TelegramSupportViewModel viewModel) =>
const CustomElevatedButton(
height: 55,
borderRadius: 12,
leadingIcon: Icons.telegram,
text: 'Open in Telegram',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildSearchText() => const Text.rich(
TextSpan(
text: 'Search for',
style: TextStyle(
color: kcDarkGrey,
),
children: [
TextSpan(
text: ' @YimaruSupport',
style: TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
)
]),
);
}

View File

@ -0,0 +1,9 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class TelegramSupportViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_strings.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'terms_and_conditions_viewmodel.dart';
class TermsAndConditionsView extends StackedView<TermsAndConditionsViewModel> {
const TermsAndConditionsView({Key? key}) : super(key: key);
@override
TermsAndConditionsViewModel viewModelBuilder(BuildContext context) =>
TermsAndConditionsViewModel();
@override
Widget builder(
BuildContext context,
TermsAndConditionsViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(TermsAndConditionsViewModel viewModel) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(TermsAndConditionsViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(TermsAndConditionsViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(TermsAndConditionsViewModel viewModel) =>
_buildColumn(viewModel);
Widget _buildColumn(TermsAndConditionsViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(TermsAndConditionsViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
verticalSpaceSmall,
_buildContentWrapper(viewModel)
];
Widget _buildAppBarWrapper(TermsAndConditionsViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar(
title: 'Terms and Conditions',
onTap: viewModel.pop,
);
Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) =>
Expanded(child: _buildContentColumnWrapper(viewModel));
Widget _buildContentColumnWrapper(TermsAndConditionsViewModel viewModel) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildMenuColumnScrollView(viewModel),
);
Widget _buildMenuColumnScrollView(TermsAndConditionsViewModel viewModel) =>
SingleChildScrollView(
child: _buildContentColumn(),
);
Widget _buildContentColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildContentColumnChildren(),
);
List<Widget> _buildContentColumnChildren() => [
_buildContent(),
verticalSpaceMedium,
_buildDownloadButtonWrapper(),
];
Widget _buildContent() => Html(
data: ksTerms,
shrinkWrap: true,
style: htmlStyle,
);
Widget _buildDownloadButtonWrapper() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildDownloadButton(),
);
Widget _buildDownloadButton() => const CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Download PDF',
leadingIcon: Icons.download,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,11 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class TermsAndConditionsViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Navigation
void pop() => _navigationService.back();
}

Some files were not shown because too many files have changed in this diff Show More