Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2c54487e8 | |||
| 69e2047122 | |||
| c7d0539cc0 | |||
| 4698ddd5fb | |||
| 37473951c4 | |||
| 42b106495a | |||
| faae4a6b66 | |||
| 1664f83333 | |||
| 6635f64c27 | |||
| 18cce5658d | |||
| 788e591d83 | |||
| 04381714db | |||
| 517c232891 | |||
| 73d027f11c | |||
| 4fdda31eba | |||
| 876c7a91e3 | |||
| 0824019612 | |||
| da7fac2b12 | |||
| 0a20c639f0 | |||
| 0936c04f3b | |||
| 11f5161956 | |||
| ae1c12e665 | |||
| 2554b80359 | |||
| 54d9ef56a2 | |||
| 7a2a72a554 |
24
assets/icons/logo_purple.svg
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<svg width="524" height="173" viewBox="0 0 524 173" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_23_9019)">
|
||||||
|
<path d="M194.31 28.74L206.48 51L219.27 28.76H232.21L212.29 62.86V83H200.67V62.84L180.75 28.74H194.31Z" fill="#9E2891"/>
|
||||||
|
<path d="M247.79 32.69C247.809 33.51 247.661 34.3252 247.353 35.0855C247.045 35.8457 246.584 36.5347 246 37.11C245.412 37.6962 244.71 38.1564 243.938 38.4625C243.166 38.7685 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.5121 235.18 32.69C235.159 31.8731 235.31 31.0608 235.623 30.3062C235.937 29.5515 236.406 28.8714 237 28.31C237.604 27.735 238.317 27.2853 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.2878 245.41 27.737 246 28.31C246.586 28.8755 247.048 29.5571 247.356 30.3113C247.665 31.0655 247.812 31.8756 247.79 32.69ZM246.93 43.15V83H236V43.15H246.93Z" fill="#9E2891"/>
|
||||||
|
<path d="M316.92 61.13V83.07H306V60.74C306 54.7 303.7 51.68 299.1 51.68C298.017 51.6409 296.939 51.8473 295.947 52.2837C294.954 52.7201 294.074 53.3752 293.37 54.2C291.819 56.1914 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.4268 267.57 54.27C266.061 56.2815 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.5457 294.41 44.42C296.907 43.2298 299.645 42.6342 302.41 42.68C304.364 42.6151 306.308 42.9667 308.115 43.7116C309.922 44.4564 311.55 45.5774 312.89 47C315.57 49.8067 316.914 54.5167 316.92 61.13Z" fill="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<path d="M391.54 53.07H387.2C384.1 53.07 381.827 54 380.38 55.86C378.827 58.0793 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.5902 387.239 43.0702 389.68 43.18H391.54V53.07Z" fill="#9E2891"/>
|
||||||
|
<path d="M433.94 83H424.25L423.25 78.19C421.853 79.9282 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.4641C403.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.7731C412.149 74.2564 413.28 74.4687 414.41 74.39C415.642 74.4479 416.869 74.1975 417.979 73.6614C419.09 73.1253 420.049 72.3205 420.77 71.32C422.257 69.28 423 66.4267 423 62.76V43.15H433.93L433.94 83Z" fill="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<path d="M379.88 105.9C383.105 104.169 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.585 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.582 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="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
<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="#9E2891"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_23_9019">
|
||||||
|
<rect width="523.37" height="172.42" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB |
BIN
assets/images/landing_1.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 30 KiB |
BIN
assets/images/landing_2.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 27 KiB |
BIN
assets/images/landing_3.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 448 KiB |
|
|
@ -193,7 +193,8 @@
|
||||||
"keep_momentum":"በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።",
|
"keep_momentum":"በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።",
|
||||||
"completed_practices": "የተጠናቀቁ ልምምዶች",
|
"completed_practices": "የተጠናቀቁ ልምምዶች",
|
||||||
"total_practices": "ጠቅላላ ልምምዶች",
|
"total_practices": "ጠቅላላ ልምምዶች",
|
||||||
"progress_percentage": "የእድገት መቶኛ"
|
"progress_percentage": "የእድገት መቶኛ",
|
||||||
|
"notifications": "ማሳወቂያዎች"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,5 +193,6 @@
|
||||||
"keep_momentum":"Great job! Keep the momentum.",
|
"keep_momentum":"Great job! Keep the momentum.",
|
||||||
"completed_practices": "Completed Practices",
|
"completed_practices": "Completed Practices",
|
||||||
"total_practices": "Total Practices",
|
"total_practices": "Total Practices",
|
||||||
"progress_percentage": "Progress Percentage"
|
"progress_percentage": "Progress Percentage",
|
||||||
|
"notifications": "Notifications"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
|
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
|
||||||
import 'package:yimaru_app/services/notification_service.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
|
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
|
||||||
import 'package:yimaru_app/services/smart_auth_service.dart';
|
import 'package:yimaru_app/services/smart_auth_service.dart';
|
||||||
import 'package:yimaru_app/services/course_service.dart';
|
import 'package:yimaru_app/services/course_service.dart';
|
||||||
|
|
@ -58,6 +57,9 @@ import 'package:yimaru_app/ui/views/course_module/course_module_view.dart';
|
||||||
import 'package:yimaru_app/services/onboarding_service.dart';
|
import 'package:yimaru_app/services/onboarding_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
|
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/payment/payment_view.dart';
|
import 'package:yimaru_app/ui/views/payment/payment_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/notification/notification_view.dart';
|
||||||
|
import 'package:yimaru_app/services/in_app_notification_service.dart';
|
||||||
|
import 'package:yimaru_app/services/push_notification_service.dart';
|
||||||
// @stacked-import
|
// @stacked-import
|
||||||
|
|
||||||
@StackedApp(
|
@StackedApp(
|
||||||
|
|
@ -97,6 +99,7 @@ import 'package:yimaru_app/ui/views/payment/payment_view.dart';
|
||||||
MaterialRoute(page: CourseModuleView),
|
MaterialRoute(page: CourseModuleView),
|
||||||
MaterialRoute(page: LearnCourseView),
|
MaterialRoute(page: LearnCourseView),
|
||||||
MaterialRoute(page: PaymentView),
|
MaterialRoute(page: PaymentView),
|
||||||
|
MaterialRoute(page: NotificationView),
|
||||||
// @stacked-route
|
// @stacked-route
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
@ -112,7 +115,6 @@ import 'package:yimaru_app/ui/views/payment/payment_view.dart';
|
||||||
LazySingleton(classType: ImagePickerService),
|
LazySingleton(classType: ImagePickerService),
|
||||||
LazySingleton(classType: GoogleAuthService),
|
LazySingleton(classType: GoogleAuthService),
|
||||||
LazySingleton(classType: ImageDownloaderService),
|
LazySingleton(classType: ImageDownloaderService),
|
||||||
LazySingleton(classType: NotificationService),
|
|
||||||
LazySingleton(classType: SmartAuthService),
|
LazySingleton(classType: SmartAuthService),
|
||||||
LazySingleton(classType: CourseService),
|
LazySingleton(classType: CourseService),
|
||||||
LazySingleton(classType: AudioPlayerService),
|
LazySingleton(classType: AudioPlayerService),
|
||||||
|
|
@ -124,6 +126,8 @@ import 'package:yimaru_app/ui/views/payment/payment_view.dart';
|
||||||
LazySingleton(classType: LearnService),
|
LazySingleton(classType: LearnService),
|
||||||
LazySingleton(classType: LocalizationService),
|
LazySingleton(classType: LocalizationService),
|
||||||
LazySingleton(classType: OnboardingService),
|
LazySingleton(classType: OnboardingService),
|
||||||
|
LazySingleton(classType: InAppNotificationService),
|
||||||
|
LazySingleton(classType: PushNotificationService),
|
||||||
// @stacked-service
|
// @stacked-service
|
||||||
],
|
],
|
||||||
bottomsheets: [
|
bottomsheets: [
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,14 @@ import '../services/dio_service.dart';
|
||||||
import '../services/google_auth_service.dart';
|
import '../services/google_auth_service.dart';
|
||||||
import '../services/image_downloader_service.dart';
|
import '../services/image_downloader_service.dart';
|
||||||
import '../services/image_picker_service.dart';
|
import '../services/image_picker_service.dart';
|
||||||
|
import '../services/in_app_notification_service.dart';
|
||||||
import '../services/in_app_update_service.dart';
|
import '../services/in_app_update_service.dart';
|
||||||
import '../services/learn_service.dart';
|
import '../services/learn_service.dart';
|
||||||
import '../services/localization_service.dart';
|
import '../services/localization_service.dart';
|
||||||
import '../services/notification_service.dart';
|
|
||||||
import '../services/onboarding_service.dart';
|
import '../services/onboarding_service.dart';
|
||||||
import '../services/permission_handler_service.dart';
|
import '../services/permission_handler_service.dart';
|
||||||
import '../services/phone_caller_service.dart';
|
import '../services/phone_caller_service.dart';
|
||||||
|
import '../services/push_notification_service.dart';
|
||||||
import '../services/secure_storage_service.dart';
|
import '../services/secure_storage_service.dart';
|
||||||
import '../services/smart_auth_service.dart';
|
import '../services/smart_auth_service.dart';
|
||||||
import '../services/status_checker_service.dart';
|
import '../services/status_checker_service.dart';
|
||||||
|
|
@ -55,7 +56,6 @@ Future<void> setupLocator(
|
||||||
locator.registerLazySingleton(() => ImagePickerService());
|
locator.registerLazySingleton(() => ImagePickerService());
|
||||||
locator.registerLazySingleton(() => GoogleAuthService());
|
locator.registerLazySingleton(() => GoogleAuthService());
|
||||||
locator.registerLazySingleton(() => ImageDownloaderService());
|
locator.registerLazySingleton(() => ImageDownloaderService());
|
||||||
locator.registerLazySingleton(() => NotificationService());
|
|
||||||
locator.registerLazySingleton(() => SmartAuthService());
|
locator.registerLazySingleton(() => SmartAuthService());
|
||||||
locator.registerLazySingleton(() => CourseService());
|
locator.registerLazySingleton(() => CourseService());
|
||||||
locator.registerLazySingleton(() => AudioPlayerService());
|
locator.registerLazySingleton(() => AudioPlayerService());
|
||||||
|
|
@ -67,4 +67,6 @@ Future<void> setupLocator(
|
||||||
locator.registerLazySingleton(() => LearnService());
|
locator.registerLazySingleton(() => LearnService());
|
||||||
locator.registerLazySingleton(() => LocalizationService());
|
locator.registerLazySingleton(() => LocalizationService());
|
||||||
locator.registerLazySingleton(() => OnboardingService());
|
locator.registerLazySingleton(() => OnboardingService());
|
||||||
|
locator.registerLazySingleton(() => InAppNotificationService());
|
||||||
|
locator.registerLazySingleton(() => PushNotificationService());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ 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/app/app.router.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/services/notification_service.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:yimaru_app/services/push_notification_service.dart';
|
||||||
import 'package:yimaru_app/ui/common/translations/codegen_loader.g.dart';
|
import 'package:yimaru_app/ui/common/translations/codegen_loader.g.dart';
|
||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ Future<void> main() async {
|
||||||
|
|
||||||
await setupLocator();
|
await setupLocator();
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
await locator<NotificationService>().initialize();
|
await locator<PushNotificationService>().initialize();
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
setupDialogUi();
|
setupDialogUi();
|
||||||
setupBottomSheetUi();
|
setupBottomSheetUi();
|
||||||
|
|
|
||||||
57
lib/models/in_app_notification.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:yimaru_app/models/notification_payload.dart';
|
||||||
|
|
||||||
|
part 'in_app_notification.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class InAppNotification {
|
||||||
|
final String? id;
|
||||||
|
|
||||||
|
final String? type;
|
||||||
|
|
||||||
|
final String? level;
|
||||||
|
|
||||||
|
final String? image;
|
||||||
|
|
||||||
|
final NotificationPayload? payload;
|
||||||
|
|
||||||
|
@JsonKey(name: 'is_read')
|
||||||
|
final bool? isRead;
|
||||||
|
|
||||||
|
@JsonKey(name: 'reciever')
|
||||||
|
final String? receiver;
|
||||||
|
|
||||||
|
@JsonKey(name: 'recipient_id')
|
||||||
|
final int? recipientId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'receiver_type')
|
||||||
|
final String? receiverType;
|
||||||
|
|
||||||
|
@JsonKey(name: 'error_severity')
|
||||||
|
final String? errorSeverity;
|
||||||
|
|
||||||
|
@JsonKey(name: 'delivery_status')
|
||||||
|
final String? deliveryStatus;
|
||||||
|
|
||||||
|
@JsonKey(name: 'delivery_channel')
|
||||||
|
final String? deliveryChannel;
|
||||||
|
|
||||||
|
const InAppNotification(
|
||||||
|
{this.id,
|
||||||
|
this.type,
|
||||||
|
this.image,
|
||||||
|
this.level,
|
||||||
|
this.isRead,
|
||||||
|
this.payload,
|
||||||
|
this.receiver,
|
||||||
|
this.recipientId,
|
||||||
|
this.receiverType,
|
||||||
|
this.errorSeverity,
|
||||||
|
this.deliveryStatus,
|
||||||
|
this.deliveryChannel});
|
||||||
|
|
||||||
|
factory InAppNotification.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$InAppNotificationFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$InAppNotificationToJson(this);
|
||||||
|
}
|
||||||
42
lib/models/in_app_notification.g.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'in_app_notification.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
InAppNotification _$InAppNotificationFromJson(Map<String, dynamic> json) =>
|
||||||
|
InAppNotification(
|
||||||
|
id: json['id'] as String?,
|
||||||
|
type: json['type'] as String?,
|
||||||
|
image: json['image'] as String?,
|
||||||
|
level: json['level'] as String?,
|
||||||
|
isRead: json['is_read'] as bool?,
|
||||||
|
payload: json['payload'] == null
|
||||||
|
? null
|
||||||
|
: NotificationPayload.fromJson(
|
||||||
|
json['payload'] as Map<String, dynamic>),
|
||||||
|
receiver: json['reciever'] as String?,
|
||||||
|
recipientId: (json['recipient_id'] as num?)?.toInt(),
|
||||||
|
receiverType: json['receiver_type'] as String?,
|
||||||
|
errorSeverity: json['error_severity'] as String?,
|
||||||
|
deliveryStatus: json['delivery_status'] as String?,
|
||||||
|
deliveryChannel: json['delivery_channel'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$InAppNotificationToJson(InAppNotification instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'type': instance.type,
|
||||||
|
'level': instance.level,
|
||||||
|
'image': instance.image,
|
||||||
|
'payload': instance.payload,
|
||||||
|
'is_read': instance.isRead,
|
||||||
|
'reciever': instance.receiver,
|
||||||
|
'recipient_id': instance.recipientId,
|
||||||
|
'receiver_type': instance.receiverType,
|
||||||
|
'error_severity': instance.errorSeverity,
|
||||||
|
'delivery_status': instance.deliveryStatus,
|
||||||
|
'delivery_channel': instance.deliveryChannel,
|
||||||
|
};
|
||||||
17
lib/models/notification_payload.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'notification_payload.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class NotificationPayload {
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
final String? headline;
|
||||||
|
|
||||||
|
const NotificationPayload({this.message, this.headline});
|
||||||
|
|
||||||
|
factory NotificationPayload.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$NotificationPayloadFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$NotificationPayloadToJson(this);
|
||||||
|
}
|
||||||
20
lib/models/notification_payload.g.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'notification_payload.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
NotificationPayload _$NotificationPayloadFromJson(Map<String, dynamic> json) =>
|
||||||
|
NotificationPayload(
|
||||||
|
message: json['message'] as String?,
|
||||||
|
headline: json['headline'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$NotificationPayloadToJson(
|
||||||
|
NotificationPayload instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'message': instance.message,
|
||||||
|
'headline': instance.headline,
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:yimaru_app/models/app_update.dart';
|
import 'package:yimaru_app/models/app_update.dart';
|
||||||
|
import 'package:yimaru_app/models/in_app_notification.dart';
|
||||||
import 'package:yimaru_app/models/learn_lesson.dart';
|
import 'package:yimaru_app/models/learn_lesson.dart';
|
||||||
import 'package:yimaru_app/models/learn_practice.dart';
|
import 'package:yimaru_app/models/learn_practice.dart';
|
||||||
import 'package:yimaru_app/models/learn_program.dart';
|
import 'package:yimaru_app/models/learn_program.dart';
|
||||||
|
|
@ -355,6 +356,55 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark notifications
|
||||||
|
Future<void> markNotificationsRead() async {
|
||||||
|
try {
|
||||||
|
await _service.dio.post(
|
||||||
|
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kNotificationsUrl/$kMarkNotificationRead');
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get unread notifications
|
||||||
|
Future<int> getUnreadNotifications() async {
|
||||||
|
try {
|
||||||
|
final Response response = await _service.dio.get(
|
||||||
|
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kNotificationsUrl/$kUnreadUrl');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.data['unread'];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get notifications
|
||||||
|
Future<List<InAppNotification>> getAllNotifications() async {
|
||||||
|
try {
|
||||||
|
List<InAppNotification> notifications = [];
|
||||||
|
|
||||||
|
final Response response = await _service.dio
|
||||||
|
.get('$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kNotificationsUrl');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['notifications'] as List;
|
||||||
|
notifications = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return InAppNotification.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get assessment question sets
|
// Get assessment question sets
|
||||||
Future<List<Assessment>> getAssessments() async {
|
Future<List<Assessment>> getAssessments() async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
|
|
||||||
StateObjects get state => _state;
|
StateObjects get state => _state;
|
||||||
|
|
||||||
|
|
||||||
// Check user logged in
|
// Check user logged in
|
||||||
Future<bool> userLoggedIn() async {
|
Future<bool> userLoggedIn() async {
|
||||||
if (await _secureService.getString('userId') != null) {
|
if (await _secureService.getString('userId') != null) {
|
||||||
|
|
@ -202,7 +201,6 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
await setFirstTimeInstall(firstTimeInstall);
|
await setFirstTimeInstall(firstTimeInstall);
|
||||||
await _secureService.setString('language', language);
|
await _secureService.setString('language', language);
|
||||||
|
|
||||||
|
|
||||||
_state = StateObjects.none;
|
_state = StateObjects.none;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ class GoogleAuthService with ListenableServiceMixin {
|
||||||
|
|
||||||
_googleUser ??=
|
_googleUser ??=
|
||||||
await _signIn.authenticate(scopeHint: ['email', 'profile']);
|
await _signIn.authenticate(scopeHint: ['email', 'profile']);
|
||||||
|
|
||||||
});
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
44
lib/services/in_app_notification_service.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/models/in_app_notification.dart';
|
||||||
|
|
||||||
|
import '../app/app.locator.dart';
|
||||||
|
import 'api_service.dart';
|
||||||
|
|
||||||
|
class InAppNotificationService with ListenableServiceMixin {
|
||||||
|
// Dependency injection
|
||||||
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
learnService() {
|
||||||
|
listenToReactiveValues([_unreadCount, _notifications]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unread count
|
||||||
|
int _unreadCount = 0;
|
||||||
|
|
||||||
|
int get unreadCount => _unreadCount;
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
List<InAppNotification> _notifications = [];
|
||||||
|
|
||||||
|
List<InAppNotification> get notifications => _notifications;
|
||||||
|
|
||||||
|
// Unread notifications
|
||||||
|
Future<void> markNotificationRead() async {
|
||||||
|
await _apiService.markNotificationsRead();
|
||||||
|
_unreadCount = await _apiService.getUnreadNotifications();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// All notifications
|
||||||
|
Future<void> getAllNotifications() async {
|
||||||
|
_notifications = await _apiService.getAllNotifications();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unread notifications
|
||||||
|
Future<void> getUnreadNotifications() async {
|
||||||
|
_unreadCount = await _apiService.getUnreadNotifications();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,8 +58,6 @@ class LearnService with ListenableServiceMixin {
|
||||||
|
|
||||||
List<LearnLesson> get lessons => _lessons;
|
List<LearnLesson> get lessons => _lessons;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Learn programs
|
// Learn programs
|
||||||
Future<String?> refreshObject(String url) async {
|
Future<String?> refreshObject(String url) async {
|
||||||
Map<String, dynamic> data = {'reference': url};
|
Map<String, dynamic> data = {'reference': url};
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
||||||
import 'package:yimaru_app/app/app.locator.dart';
|
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
||||||
await locator<NotificationService>().setupFlutterNotifications();
|
|
||||||
await locator<NotificationService>().showNotification(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotificationService {
|
|
||||||
final _messaging = FirebaseMessaging.instance;
|
|
||||||
|
|
||||||
bool _isFlutterLocalNotificationInitialized = false;
|
|
||||||
|
|
||||||
final _localNotifications = FlutterLocalNotificationsPlugin();
|
|
||||||
|
|
||||||
Future<void> initialize() async {
|
|
||||||
// Initialize FCM token
|
|
||||||
await updateFCMToken();
|
|
||||||
|
|
||||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
|
||||||
|
|
||||||
// Request permission
|
|
||||||
await _requestPermission();
|
|
||||||
|
|
||||||
// setup message handle
|
|
||||||
await _setupMessageHandler();
|
|
||||||
|
|
||||||
// Subscribe to all devices
|
|
||||||
subscribeToTopic('yimaru');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _requestPermission() async {
|
|
||||||
await _messaging.requestPermission(
|
|
||||||
alert: true,
|
|
||||||
badge: true,
|
|
||||||
sound: true,
|
|
||||||
carPlay: false,
|
|
||||||
provisional: false,
|
|
||||||
announcement: false,
|
|
||||||
criticalAlert: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setupFlutterNotifications() async {
|
|
||||||
if (_isFlutterLocalNotificationInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android setup
|
|
||||||
const channel = AndroidNotificationChannel(
|
|
||||||
'yimaru', // id
|
|
||||||
'Yimaru', // title
|
|
||||||
importance: Importance.high,
|
|
||||||
);
|
|
||||||
|
|
||||||
await _localNotifications
|
|
||||||
.resolvePlatformSpecificImplementation<
|
|
||||||
AndroidFlutterLocalNotificationsPlugin>()
|
|
||||||
?.createNotificationChannel(channel);
|
|
||||||
|
|
||||||
const initializationSettingsAndroid =
|
|
||||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
||||||
|
|
||||||
// IOS setup
|
|
||||||
const initializationSettingsDarwin = DarwinInitializationSettings();
|
|
||||||
|
|
||||||
const initializationSettings = InitializationSettings(
|
|
||||||
android: initializationSettingsAndroid,
|
|
||||||
iOS: initializationSettingsDarwin);
|
|
||||||
|
|
||||||
// Flutter notification setup
|
|
||||||
await _localNotifications.initialize(
|
|
||||||
settings: initializationSettings,
|
|
||||||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
|
||||||
if (response.payload == 'Page') {
|
|
||||||
// navigatorKey.currentState?.pushNamed('RouteName');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
_isFlutterLocalNotificationInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> showNotification(RemoteMessage message) async {
|
|
||||||
RemoteNotification? notification = message.notification;
|
|
||||||
AndroidNotification? android = message.notification?.android;
|
|
||||||
|
|
||||||
if (notification != null && android != null) {
|
|
||||||
await _localNotifications.show(
|
|
||||||
id: notification.hashCode,
|
|
||||||
title: notification.title,
|
|
||||||
body: notification.body,
|
|
||||||
notificationDetails: const NotificationDetails(
|
|
||||||
android: AndroidNotificationDetails('yimaru', 'Yimaru',
|
|
||||||
enableVibration: true,
|
|
||||||
priority: Priority.high,
|
|
||||||
icon: '@mipmap/ic_launcher',
|
|
||||||
importance: Importance.high),
|
|
||||||
iOS: DarwinNotificationDetails(
|
|
||||||
presentAlert: true, presentBadge: true, presentSound: true)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setupMessageHandler() async {
|
|
||||||
// Foreground message
|
|
||||||
FirebaseMessaging.onMessage
|
|
||||||
.listen((RemoteMessage message) => showNotification(message));
|
|
||||||
|
|
||||||
// Background message
|
|
||||||
FirebaseMessaging.onMessageOpenedApp.listen(_handleBackgroundMessage);
|
|
||||||
|
|
||||||
// Opened app
|
|
||||||
final initialMessage = await _messaging.getInitialMessage();
|
|
||||||
|
|
||||||
if (initialMessage != null) {
|
|
||||||
_handleBackgroundMessage(initialMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleBackgroundMessage(RemoteMessage message) {
|
|
||||||
if (message.data['type'] == 'Page') {
|
|
||||||
// navigatorKey.currentState?.pushNamed('RouteName');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> subscribeToTopic(String topic) async {
|
|
||||||
await FirebaseMessaging.instance.subscribeToTopic(topic);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateFCMToken() async {
|
|
||||||
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
|
|
||||||
_messaging.onTokenRefresh.listen((newToken) {
|
|
||||||
// updateTokenOnServer(newToken);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
136
lib/services/push_notification_service.dart
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
await locator<PushNotificationService>().setupFlutterNotifications();
|
||||||
|
await locator<PushNotificationService>().showNotification(message);
|
||||||
|
}
|
||||||
|
class PushNotificationService { final _messaging = FirebaseMessaging.instance;
|
||||||
|
|
||||||
|
bool _isFlutterLocalNotificationInitialized = false;
|
||||||
|
|
||||||
|
final _localNotifications = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
// Initialize FCM token
|
||||||
|
await updateFCMToken();
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
|
// Request permission
|
||||||
|
await _requestPermission();
|
||||||
|
|
||||||
|
// setup message handle
|
||||||
|
await _setupMessageHandler();
|
||||||
|
|
||||||
|
// Subscribe to all devices
|
||||||
|
subscribeToTopic('yimaru');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestPermission() async {
|
||||||
|
await _messaging.requestPermission(
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
carPlay: false,
|
||||||
|
provisional: false,
|
||||||
|
announcement: false,
|
||||||
|
criticalAlert: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupFlutterNotifications() async {
|
||||||
|
if (_isFlutterLocalNotificationInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android setup
|
||||||
|
const channel = AndroidNotificationChannel(
|
||||||
|
'yimaru', // id
|
||||||
|
'Yimaru', // title
|
||||||
|
importance: Importance.high,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _localNotifications
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(channel);
|
||||||
|
|
||||||
|
const initializationSettingsAndroid =
|
||||||
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
|
// IOS setup
|
||||||
|
const initializationSettingsDarwin = DarwinInitializationSettings();
|
||||||
|
|
||||||
|
const initializationSettings = InitializationSettings(
|
||||||
|
android: initializationSettingsAndroid,
|
||||||
|
iOS: initializationSettingsDarwin);
|
||||||
|
|
||||||
|
// Flutter notification setup
|
||||||
|
await _localNotifications.initialize(
|
||||||
|
settings: initializationSettings,
|
||||||
|
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||||
|
if (response.payload == 'Page') {
|
||||||
|
// navigatorKey.currentState?.pushNamed('RouteName');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_isFlutterLocalNotificationInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showNotification(RemoteMessage message) async {
|
||||||
|
RemoteNotification? notification = message.notification;
|
||||||
|
AndroidNotification? android = message.notification?.android;
|
||||||
|
|
||||||
|
if (notification != null && android != null) {
|
||||||
|
await _localNotifications.show(
|
||||||
|
id: notification.hashCode,
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body,
|
||||||
|
notificationDetails: const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails('yimaru', 'Yimaru',
|
||||||
|
enableVibration: true,
|
||||||
|
priority: Priority.high,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
importance: Importance.high),
|
||||||
|
iOS: DarwinNotificationDetails(
|
||||||
|
presentAlert: true, presentBadge: true, presentSound: true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupMessageHandler() async {
|
||||||
|
// Foreground message
|
||||||
|
FirebaseMessaging.onMessage
|
||||||
|
.listen((RemoteMessage message) => showNotification(message));
|
||||||
|
|
||||||
|
// Background message
|
||||||
|
FirebaseMessaging.onMessageOpenedApp.listen(_handleBackgroundMessage);
|
||||||
|
|
||||||
|
// Opened app
|
||||||
|
final initialMessage = await _messaging.getInitialMessage();
|
||||||
|
|
||||||
|
if (initialMessage != null) {
|
||||||
|
_handleBackgroundMessage(initialMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBackgroundMessage(RemoteMessage message) {
|
||||||
|
if (message.data['type'] == 'Page') {
|
||||||
|
// navigatorKey.currentState?.pushNamed('RouteName');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> subscribeToTopic(String topic) async {
|
||||||
|
await FirebaseMessaging.instance.subscribeToTopic(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateFCMToken() async {
|
||||||
|
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
|
||||||
|
_messaging.onTokenRefresh.listen((newToken) {
|
||||||
|
// updateTokenOnServer(newToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,12 +13,15 @@ String kCheckUrl = 'check';
|
||||||
|
|
||||||
String kFilesUrl = 'files';
|
String kFilesUrl = 'files';
|
||||||
|
|
||||||
|
String kUnreadUrl = 'unread';
|
||||||
|
|
||||||
String kApiVersionUrl = 'v1';
|
String kApiVersionUrl = 'v1';
|
||||||
|
|
||||||
String kLevelsUrl = 'levels';
|
String kLevelsUrl = 'levels';
|
||||||
|
|
||||||
String kCoursesUrl = 'courses';
|
String kCoursesUrl = 'courses';
|
||||||
|
|
||||||
|
|
||||||
String kModulesUrl = 'modules';
|
String kModulesUrl = 'modules';
|
||||||
|
|
||||||
String kLessonsUrl = 'lessons';
|
String kLessonsUrl = 'lessons';
|
||||||
|
|
@ -69,6 +72,8 @@ String kQuestionSetsUrl = 'question-sets';
|
||||||
|
|
||||||
String kRequestResetCode = 'sendResetCode';
|
String kRequestResetCode = 'sendResetCode';
|
||||||
|
|
||||||
|
String kNotificationsUrl = 'notifications';
|
||||||
|
|
||||||
String kSubcategoriesUrl = 'sub-categories';
|
String kSubcategoriesUrl = 'sub-categories';
|
||||||
|
|
||||||
String kProgressSummary = 'progress-summary';
|
String kProgressSummary = 'progress-summary';
|
||||||
|
|
@ -79,6 +84,8 @@ String kCoursePracticeQuestions = 'questions';
|
||||||
|
|
||||||
String kCatalogCoursesUrl = 'catalog-courses';
|
String kCatalogCoursesUrl = 'catalog-courses';
|
||||||
|
|
||||||
|
String kMarkNotificationRead = 'mark-all-read';
|
||||||
|
|
||||||
String kUpdateProfileImage = 'profile-picture';
|
String kUpdateProfileImage = 'profile-picture';
|
||||||
|
|
||||||
String kSubscriptionsUrl = 'subscription-plans';
|
String kSubscriptionsUrl = 'subscription-plans';
|
||||||
|
|
@ -124,5 +131,4 @@ String kTelegramSupportLink = 'https://t.me/yimaruacademy2026';
|
||||||
|
|
||||||
String kErrorUrl = 'https://api.yimaruacademy.com/payment/error';
|
String kErrorUrl = 'https://api.yimaruacademy.com/payment/error';
|
||||||
|
|
||||||
String kSuccessUrl =
|
String kSuccessUrl = 'https://api.yimaruacademy.com/payment/success';
|
||||||
'https://api.yimaruacademy.com/payment/success';
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ enum StateObjects {
|
||||||
profileUpdate,
|
profileUpdate,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
learnPractice,
|
learnPractice,
|
||||||
|
notifications,
|
||||||
courseCatalogs,
|
courseCatalogs,
|
||||||
loginWithEmail,
|
loginWithEmail,
|
||||||
coursePractice,
|
coursePractice,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ class CodegenLoader extends AssetLoader{
|
||||||
"confirm_password": "የይለፍ ቃል ያረጋግጡ",
|
"confirm_password": "የይለፍ ቃል ያረጋግጡ",
|
||||||
"eight_character_minimum": "ቢያንስ 8 ፊደላት",
|
"eight_character_minimum": "ቢያንስ 8 ፊደላት",
|
||||||
"password_match": "የይለፍ ቃሉ ተመሳስሏል",
|
"password_match": "የይለፍ ቃሉ ተመሳስሏል",
|
||||||
"sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።",
|
"sign_up_agreement":
|
||||||
|
"‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።",
|
||||||
"terms_of_services": "የአገልግሎት ውሎች",
|
"terms_of_services": "የአገልግሎት ውሎች",
|
||||||
"and": "እና",
|
"and": "እና",
|
||||||
"privacy_policy": "የግላዊነት ፖሊሲ",
|
"privacy_policy": "የግላዊነት ፖሊሲ",
|
||||||
|
|
@ -137,7 +138,8 @@ class CodegenLoader extends AssetLoader{
|
||||||
"call_our_support": "ከ3 ጠዋት እስከ 12 ማታ ድረስ የድጋፍ ቡድናችንን ይደውሉ",
|
"call_our_support": "ከ3 ጠዋት እስከ 12 ማታ ድረስ የድጋፍ ቡድናችንን ይደውሉ",
|
||||||
"tap_to_call": "ለመደወል ይንኩ",
|
"tap_to_call": "ለመደወል ይንኩ",
|
||||||
"join_telegram": "በቴሌግራም የይማሩ አካዳሚን ይቀላቀሉ",
|
"join_telegram": "በቴሌግራም የይማሩ አካዳሚን ይቀላቀሉ",
|
||||||
"connect_with_support_team": "ለፈጣን እርዳታ እና የማህበረሰብ ዝማኔዎች፣ በቴሌግራም ከድጋፍ ቡድናችን ጋር ወዲያውኑ ይገናኙ።",
|
"connect_with_support_team":
|
||||||
|
"ለፈጣን እርዳታ እና የማህበረሰብ ዝማኔዎች፣ በቴሌግራም ከድጋፍ ቡድናችን ጋር ወዲያውኑ ይገናኙ።",
|
||||||
"open_in_telegram": "በቴሌግራም ይክፈቱ",
|
"open_in_telegram": "በቴሌግራም ይክፈቱ",
|
||||||
"search_for": "ፈልጉት",
|
"search_for": "ፈልጉት",
|
||||||
"current_level": "የአሁኑ ደረጃ",
|
"current_level": "የአሁኑ ደረጃ",
|
||||||
|
|
@ -187,7 +189,8 @@ class CodegenLoader extends AssetLoader{
|
||||||
"evey_one_has_strugle": "ሁሉም ሰው ችግሮች አሉት፣ የአንተን እንጀምር እንፍታ",
|
"evey_one_has_strugle": "ሁሉም ሰው ችግሮች አሉት፣ የአንተን እንጀምር እንፍታ",
|
||||||
"write_your_challenge": "ችግርህን ጻፍ…",
|
"write_your_challenge": "ችግርህን ጻፍ…",
|
||||||
"topic_interest": "በጣም የሚስቡህ ርዕሶች የትኞቹ ናቸው?",
|
"topic_interest": "በጣም የሚስቡህ ርዕሶች የትኞቹ ናቸው?",
|
||||||
"favourite_topic": "የምትወዳቸው ርዕሶች አስደሳች እና ከሕይወትህ ጋር የተዛመዱ ትምህርቶችን ለመፍጠር ይረዱናል።",
|
"favourite_topic":
|
||||||
|
"የምትወዳቸው ርዕሶች አስደሳች እና ከሕይወትህ ጋር የተዛመዱ ትምህርቶችን ለመፍጠር ይረዱናል።",
|
||||||
"your_interest": "ፍላጎትህን ጻፍ…",
|
"your_interest": "ፍላጎትህን ጻፍ…",
|
||||||
"want_quick_assessment": "የእንግሊዝኛ ደረጃህን ለማወቅ ፈጣን ግምገማ ትፈልጋለህ?",
|
"want_quick_assessment": "የእንግሊዝኛ ደረጃህን ለማወቅ ፈጣን ግምገማ ትፈልጋለህ?",
|
||||||
"answer_quick_questions": "የእንግሊዝኛ ችሎታህን ለመረዳት ጥቂት ፈጣን ጥያቄዎችን መልስ።",
|
"answer_quick_questions": "የእንግሊዝኛ ችሎታህን ለመረዳት ጥቂት ፈጣን ጥያቄዎችን መልስ።",
|
||||||
|
|
@ -202,14 +205,16 @@ class CodegenLoader extends AssetLoader{
|
||||||
"finish_all_practice_lesson": "ይህን ልምምድ ለመውሰድ የቀድሞውን የትምህርት ልምምድ ያጠናቅቁ",
|
"finish_all_practice_lesson": "ይህን ልምምድ ለመውሰድ የቀድሞውን የትምህርት ልምምድ ያጠናቅቁ",
|
||||||
"finish_all_practice_module": "የሞጁሉን ልምምድ ለመውሰድ የትምህርት ልምምዶችን ያጠናቅቁ",
|
"finish_all_practice_module": "የሞጁሉን ልምምድ ለመውሰድ የትምህርት ልምምዶችን ያጠናቅቁ",
|
||||||
"finish_all_practice_course": "የኮርሱን ልምምድ ለመውሰድ የሞጁል ልምምዶችን ያጠናቅቁ",
|
"finish_all_practice_course": "የኮርሱን ልምምድ ለመውሰድ የሞጁል ልምምዶችን ያጠናቅቁ",
|
||||||
"finish_all_practice_previouse_module": "ይህን ልምምድ ለመውሰድ የቀድሞውን የሞጁል ልምምድ ያጠናቅቁ",
|
"finish_all_practice_previouse_module":
|
||||||
|
"ይህን ልምምድ ለመውሰድ የቀድሞውን የሞጁል ልምምድ ያጠናቅቁ",
|
||||||
"finish_all_practice_previouse_course": "ይህን ለመውሰድ የቀድሞውን የኮርስ ልምምድ ያጠናቅቁ",
|
"finish_all_practice_previouse_course": "ይህን ለመውሰድ የቀድሞውን የኮርስ ልምምድ ያጠናቅቁ",
|
||||||
"track_journey": "የትምህርት ጉዞዎን ይከታተሉ እና በጊዜ ሂደት ያሳዩትን እድገት ይመልከቱ።",
|
"track_journey": "የትምህርት ጉዞዎን ይከታተሉ እና በጊዜ ሂደት ያሳዩትን እድገት ይመልከቱ።",
|
||||||
"learn_english": "እንግሊዝኛ ይማሩ",
|
"learn_english": "እንግሊዝኛ ይማሩ",
|
||||||
"keep_momentum": "በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።",
|
"keep_momentum": "በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።",
|
||||||
"completed_practices": "የተጠናቀቁ ልምምዶች",
|
"completed_practices": "የተጠናቀቁ ልምምዶች",
|
||||||
"total_practices": "ጠቅላላ ልምምዶች",
|
"total_practices": "ጠቅላላ ልምምዶች",
|
||||||
"progress_percentage": "የእድገት መቶኛ"
|
"progress_percentage": "የእድገት መቶኛ",
|
||||||
|
"notifications": "ማሳወቂያዎች"
|
||||||
};
|
};
|
||||||
static const Map<String, dynamic> _en = {
|
static const Map<String, dynamic> _en = {
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
|
|
@ -229,13 +234,15 @@ static const Map<String,dynamic> _en = {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register_with_google": "Register with Google",
|
"register_with_google": "Register with Google",
|
||||||
"register_with_phone": "Register with phone number",
|
"register_with_phone": "Register with phone number",
|
||||||
"enter_phone_number": "Enter your phone number. We will send you a confirmation code there.",
|
"enter_phone_number":
|
||||||
|
"Enter your phone number. We will send you a confirmation code there.",
|
||||||
"login_with_email": "Login with email",
|
"login_with_email": "Login with email",
|
||||||
"create_password": "Create password",
|
"create_password": "Create password",
|
||||||
"confirm_password": "Confirm password",
|
"confirm_password": "Confirm password",
|
||||||
"eight_character_minimum": "8 characters minimum",
|
"eight_character_minimum": "8 characters minimum",
|
||||||
"password_match": "password match",
|
"password_match": "password match",
|
||||||
"sign_up_agreement": "By clicking ‘Sign Up’, you agree to our ‘Terms of Service’ and ‘Privacy Policy’",
|
"sign_up_agreement":
|
||||||
|
"By clicking ‘Sign Up’, you agree to our ‘Terms of Service’ and ‘Privacy Policy’",
|
||||||
"terms_of_services": "Terms of Service",
|
"terms_of_services": "Terms of Service",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"privacy_policy": "Privacy Policy",
|
"privacy_policy": "Privacy Policy",
|
||||||
|
|
@ -246,7 +253,8 @@ static const Map<String,dynamic> _en = {
|
||||||
"code_sent_to_email": "Code sent to your email",
|
"code_sent_to_email": "Code sent to your email",
|
||||||
"resend_code_in": "Resend code in",
|
"resend_code_in": "Resend code in",
|
||||||
"reset_password": "Reset Password",
|
"reset_password": "Reset Password",
|
||||||
"enter_email_reset_code": "Enter your email. We will send you a reset code.",
|
"enter_email_reset_code":
|
||||||
|
"Enter your email. We will send you a reset code.",
|
||||||
"please_wait": "Please wait",
|
"please_wait": "Please wait",
|
||||||
"reset_code_sent": "Reset code sent successfully",
|
"reset_code_sent": "Reset code sent successfully",
|
||||||
"reset_code": "Reset code",
|
"reset_code": "Reset code",
|
||||||
|
|
@ -287,7 +295,8 @@ static const Map<String,dynamic> _en = {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"you_are_speaking": "You're speaking",
|
"you_are_speaking": "You're speaking",
|
||||||
"practice_completed": "Practice completed!",
|
"practice_completed": "Practice completed!",
|
||||||
"great_improvement": "You sound more confident this time, great improvement",
|
"great_improvement":
|
||||||
|
"You sound more confident this time, great improvement",
|
||||||
"practice_again": "Practice again",
|
"practice_again": "Practice again",
|
||||||
"conversation_review": "Conversation review",
|
"conversation_review": "Conversation review",
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
|
|
@ -334,7 +343,8 @@ static const Map<String,dynamic> _en = {
|
||||||
"call_our_support": "Call our support team between 9 AM - 6 PM",
|
"call_our_support": "Call our support team between 9 AM - 6 PM",
|
||||||
"tap_to_call": "Tap to call",
|
"tap_to_call": "Tap to call",
|
||||||
"join_telegram": "Join Yimaru Academy on Telegram",
|
"join_telegram": "Join Yimaru Academy on Telegram",
|
||||||
"connect_with_support_team": "Connect with our support team instantly on Telegram for quick assistance and community updates",
|
"connect_with_support_team":
|
||||||
|
"Connect with our support team instantly on Telegram for quick assistance and community updates",
|
||||||
"open_in_telegram": "Open in Telegram",
|
"open_in_telegram": "Open in Telegram",
|
||||||
"search_for": "Search for",
|
"search_for": "Search for",
|
||||||
"current_level": "Current Level",
|
"current_level": "Current Level",
|
||||||
|
|
@ -342,18 +352,22 @@ static const Map<String,dynamic> _en = {
|
||||||
"no_practice_available": "No practice available!",
|
"no_practice_available": "No practice available!",
|
||||||
"begin_module_practice": "Begin Module Practice",
|
"begin_module_practice": "Begin Module Practice",
|
||||||
"lets_practice_lesson": "Let’s Practice",
|
"lets_practice_lesson": "Let’s Practice",
|
||||||
"lets_quickly_review": "Let’s quickly review what you’ve learned in this module!",
|
"lets_quickly_review":
|
||||||
|
"Let’s quickly review what you’ve learned in this module!",
|
||||||
"lets_practice_module": "Let's practice what you just learnt!",
|
"lets_practice_module": "Let's practice what you just learnt!",
|
||||||
"ask_you_few_actions": "I’ll ask you a few questions, and you can respond naturally.",
|
"ask_you_few_actions":
|
||||||
|
"I’ll ask you a few questions, and you can respond naturally.",
|
||||||
"begin_level_practice": "Begin Level Practice",
|
"begin_level_practice": "Begin Level Practice",
|
||||||
"lets_practice_course": "Let’s Practice Course",
|
"lets_practice_course": "Let’s Practice Course",
|
||||||
"lets_quick_review": "Let’s quickly review what you’ve learned in this level!",
|
"lets_quick_review":
|
||||||
|
"Let’s quickly review what you’ve learned in this level!",
|
||||||
"speaking": "is speaking...",
|
"speaking": "is speaking...",
|
||||||
"you_have_finished_practice": "You have finished your practice",
|
"you_have_finished_practice": "You have finished your practice",
|
||||||
"view_results": "View My Results",
|
"view_results": "View My Results",
|
||||||
"sample_answer": "Sample Answer",
|
"sample_answer": "Sample Answer",
|
||||||
"your_answer": "Your Answer",
|
"your_answer": "Your Answer",
|
||||||
"sound_confident": "You sound more confident this time - great improvement!",
|
"sound_confident":
|
||||||
|
"You sound more confident this time - great improvement!",
|
||||||
"you_have_completed": "Yay, you’ve completed",
|
"you_have_completed": "Yay, you’ve completed",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
|
|
@ -364,15 +378,20 @@ static const Map<String,dynamic> _en = {
|
||||||
"phone_must_start_with": "Phone number must start with 251",
|
"phone_must_start_with": "Phone number must start with 251",
|
||||||
"phone_must_be": "Phone number must be 12 digits",
|
"phone_must_be": "Phone number must be 12 digits",
|
||||||
"what_should_we_call_you": "What should we call you?",
|
"what_should_we_call_you": "What should we call you?",
|
||||||
"name_for_personalization": "We’ll use your name to personalize your learning journey.",
|
"name_for_personalization":
|
||||||
|
"We’ll use your name to personalize your learning journey.",
|
||||||
"choose_your_gender": "Choose your gender?",
|
"choose_your_gender": "Choose your gender?",
|
||||||
"gender_for_personalization": "We’ll personalize your learning experience based on your gender.",
|
"gender_for_personalization":
|
||||||
|
"We’ll personalize your learning experience based on your gender.",
|
||||||
"age_range": "Which age range are you in?",
|
"age_range": "Which age range are you in?",
|
||||||
"age_for_personalization": "We’ll personalize your learning experience based on your age.",
|
"age_for_personalization":
|
||||||
|
"We’ll personalize your learning experience based on your age.",
|
||||||
"educational_background": "What’s your current educational level?",
|
"educational_background": "What’s your current educational level?",
|
||||||
"education_for_personalization": "This helps us tailor your lessons to your experience.",
|
"education_for_personalization":
|
||||||
|
"This helps us tailor your lessons to your experience.",
|
||||||
"your_occupation": "What’s your occupation?",
|
"your_occupation": "What’s your occupation?",
|
||||||
"occupation_for_personalization": "We’ll personalize your learning experience based on your occupation.",
|
"occupation_for_personalization":
|
||||||
|
"We’ll personalize your learning experience based on your occupation.",
|
||||||
"location": "Where are you from?",
|
"location": "Where are you from?",
|
||||||
"select_country_region": "Select your country and region from the dropdown",
|
"select_country_region": "Select your country and region from the dropdown",
|
||||||
"select_country": "Select country",
|
"select_country": "Select country",
|
||||||
|
|
@ -384,10 +403,13 @@ static const Map<String,dynamic> _en = {
|
||||||
"evey_one_has_strugle": "Everyone has struggles, let’s start fixing yours",
|
"evey_one_has_strugle": "Everyone has struggles, let’s start fixing yours",
|
||||||
"write_your_challenge": "Write your challenge…",
|
"write_your_challenge": "Write your challenge…",
|
||||||
"topic_interest": "Which topics interest you most?",
|
"topic_interest": "Which topics interest you most?",
|
||||||
"favourite_topic": "Your favorite topics help us create fun, relatable lessons.",
|
"favourite_topic":
|
||||||
|
"Your favorite topics help us create fun, relatable lessons.",
|
||||||
"your_interest": "Write your interest…",
|
"your_interest": "Write your interest…",
|
||||||
"want_quick_assessment": "Want a quick assessment to know your English level?",
|
"want_quick_assessment":
|
||||||
"answer_quick_questions": "Answer a few quick questions to help us understand your English proficiency.",
|
"Want a quick assessment to know your English level?",
|
||||||
|
"answer_quick_questions":
|
||||||
|
"Answer a few quick questions to help us understand your English proficiency.",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"finish_level": "Finish Level",
|
"finish_level": "Finish Level",
|
||||||
"likely_speaker": "You’re likely speaker of",
|
"likely_speaker": "You’re likely speaker of",
|
||||||
|
|
@ -396,17 +418,27 @@ static const Map<String,dynamic> _en = {
|
||||||
"welcome_abroad": "Welcome aboard",
|
"welcome_abroad": "Welcome aboard",
|
||||||
"ready_to_explore": "You’re ready to explore your personalized lessons.",
|
"ready_to_explore": "You’re ready to explore your personalized lessons.",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"finish_all_practice_lesson": "Finish the previous lesson practice to take this practice",
|
"finish_all_practice_lesson":
|
||||||
"finish_all_practice_module": "Finish the lesson practices to take the Module Practice",
|
"Finish the previous lesson practice to take this practice",
|
||||||
"finish_all_practice_course": "Finish the Module practices to take the Course practice",
|
"finish_all_practice_module":
|
||||||
"finish_all_practice_previouse_module": "Finish the previous Module practice to take this practice",
|
"Finish the lesson practices to take the Module Practice",
|
||||||
"finish_all_practice_previouse_course": "Finish the previous course practice to take this",
|
"finish_all_practice_course":
|
||||||
"track_journey": "Track your learning journey and see your growth over time.",
|
"Finish the Module practices to take the Course practice",
|
||||||
|
"finish_all_practice_previouse_module":
|
||||||
|
"Finish the previous Module practice to take this practice",
|
||||||
|
"finish_all_practice_previouse_course":
|
||||||
|
"Finish the previous course practice to take this",
|
||||||
|
"track_journey":
|
||||||
|
"Track your learning journey and see your growth over time.",
|
||||||
"learn_english": "Learn English",
|
"learn_english": "Learn English",
|
||||||
"keep_momentum": "Great job! Keep the momentum.",
|
"keep_momentum": "Great job! Keep the momentum.",
|
||||||
"completed_practices": "Completed Practices",
|
"completed_practices": "Completed Practices",
|
||||||
"total_practices": "Total Practices",
|
"total_practices": "Total Practices",
|
||||||
"progress_percentage": "Progress Percentage"
|
"progress_percentage": "Progress Percentage",
|
||||||
|
"notifications": "Notifications"
|
||||||
|
};
|
||||||
|
static const Map<String, Map<String, dynamic>> mapLocales = {
|
||||||
|
"am": _am,
|
||||||
|
"en": _en
|
||||||
};
|
};
|
||||||
static const Map<String, Map<String,dynamic>> mapLocales = {"am": _am, "en": _en};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,8 @@ abstract class LocaleKeys {
|
||||||
static const educational_background = 'educational_background';
|
static const educational_background = 'educational_background';
|
||||||
static const education_for_personalization = 'education_for_personalization';
|
static const education_for_personalization = 'education_for_personalization';
|
||||||
static const your_occupation = 'your_occupation';
|
static const your_occupation = 'your_occupation';
|
||||||
static const occupation_for_personalization = 'occupation_for_personalization';
|
static const occupation_for_personalization =
|
||||||
|
'occupation_for_personalization';
|
||||||
static const location = 'location';
|
static const location = 'location';
|
||||||
static const select_country_region = 'select_country_region';
|
static const select_country_region = 'select_country_region';
|
||||||
static const select_country = 'select_country';
|
static const select_country = 'select_country';
|
||||||
|
|
@ -188,13 +189,15 @@ abstract class LocaleKeys {
|
||||||
static const finish_all_practice_lesson = 'finish_all_practice_lesson';
|
static const finish_all_practice_lesson = 'finish_all_practice_lesson';
|
||||||
static const finish_all_practice_module = 'finish_all_practice_module';
|
static const finish_all_practice_module = 'finish_all_practice_module';
|
||||||
static const finish_all_practice_course = 'finish_all_practice_course';
|
static const finish_all_practice_course = 'finish_all_practice_course';
|
||||||
static const finish_all_practice_previouse_module = 'finish_all_practice_previouse_module';
|
static const finish_all_practice_previouse_module =
|
||||||
static const finish_all_practice_previouse_course = 'finish_all_practice_previouse_course';
|
'finish_all_practice_previouse_module';
|
||||||
|
static const finish_all_practice_previouse_course =
|
||||||
|
'finish_all_practice_previouse_course';
|
||||||
static const track_journey = 'track_journey';
|
static const track_journey = 'track_journey';
|
||||||
static const learn_english = 'learn_english';
|
static const learn_english = 'learn_english';
|
||||||
static const keep_momentum = 'keep_momentum';
|
static const keep_momentum = 'keep_momentum';
|
||||||
static const completed_practices = 'completed_practices';
|
static const completed_practices = 'completed_practices';
|
||||||
static const total_practices = 'total_practices';
|
static const total_practices = 'total_practices';
|
||||||
static const progress_percentage = 'progress_percentage';
|
static const progress_percentage = 'progress_percentage';
|
||||||
|
static const notifications = 'notifications';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,12 @@ TextStyle style14LG400 = const TextStyle(
|
||||||
color: kcLightGrey,
|
color: kcLightGrey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style12W600 = const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: kcWhite,
|
||||||
|
fontWeight: FontWeight.w600
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style14MG400 = const TextStyle(
|
TextStyle style14MG400 = const TextStyle(
|
||||||
color: kcMediumGrey,
|
color: kcMediumGrey,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ class CourseView extends StackedView<CourseViewModel> {
|
||||||
Widget _buildAppBar(CourseViewModel viewModel) => ProfileAppBar(
|
Widget _buildAppBar(CourseViewModel viewModel) => ProfileAppBar(
|
||||||
name: viewModel.user?.firstName,
|
name: viewModel.user?.firstName,
|
||||||
profileImage: viewModel.user?.profilePicture,
|
profileImage: viewModel.user?.profilePicture,
|
||||||
|
unreadCount: viewModel.unreadCount.toString(),
|
||||||
|
onTap: () async => await viewModel.navigateToNotification(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCategoryColumnWrapper(CourseViewModel viewModel) =>
|
Widget _buildCategoryColumnWrapper(CourseViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../../../app/app.locator.dart';
|
||||||
import '../../../app/app.router.dart';
|
import '../../../app/app.router.dart';
|
||||||
import '../../../models/user.dart';
|
import '../../../models/user.dart';
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../../services/authentication_service.dart';
|
||||||
|
import '../../../services/in_app_notification_service.dart';
|
||||||
|
|
||||||
class CourseViewModel extends ReactiveViewModel {
|
class CourseViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
|
|
@ -13,15 +14,23 @@ class CourseViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
|
final _inAppNotificationService = locator<InAppNotificationService>();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices =>
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
[_authenticationService];
|
[_authenticationService,_inAppNotificationService];
|
||||||
|
|
||||||
// Current user
|
// Current user
|
||||||
User? get _user => _authenticationService.user;
|
User? get _user => _authenticationService.user;
|
||||||
|
|
||||||
User? get user => _user;
|
User? get user => _user;
|
||||||
|
|
||||||
|
// Notification count
|
||||||
|
int get _unreadCount => _inAppNotificationService.unreadCount;
|
||||||
|
|
||||||
|
int get unreadCount => _unreadCount;
|
||||||
|
|
||||||
// Course
|
// Course
|
||||||
final List<Map<String, dynamic>> _courses = [
|
final List<Map<String, dynamic>> _courses = [
|
||||||
{
|
{
|
||||||
|
|
@ -41,6 +50,11 @@ class CourseViewModel extends ReactiveViewModel {
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
Future<void> navigateToNotification() async =>
|
||||||
|
await _navigationService.navigateToNotificationView();
|
||||||
|
|
||||||
Future<void> navigateToCourseCatalog() async =>
|
Future<void> navigateToCourseCatalog() async =>
|
||||||
await _navigationService.navigateToCourseCatalogView();
|
await _navigationService.navigateToCourseCatalogView();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_carousel_widget/flutter_carousel_widget.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/views/failure/screens/first_failure_screen.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/failure/screens/second_failure_screen.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/failure/screens/third_failure_screen.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
|
|
@ -26,94 +30,38 @@ class FailureView extends StackedView<FailureViewModel> {
|
||||||
_buildScaffoldWrapper();
|
_buildScaffoldWrapper();
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper() => Scaffold(
|
Widget _buildScaffoldWrapper() => Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
body: _buildScaffold(),
|
body: _buildStartupScreens(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold() => Stack(
|
Widget _buildStartupScreens() => FlutterCarousel(
|
||||||
children: _buildScaffoldChildren(),
|
options: FlutterCarouselOptions(
|
||||||
);
|
autoPlay: true,
|
||||||
|
viewportFraction: 1,
|
||||||
List<Widget> _buildScaffoldChildren() => [
|
showIndicator: false,
|
||||||
_buildBackground(),
|
|
||||||
_buildColumn(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget _buildBackground() => Image.asset(
|
|
||||||
'assets/images/loading.png',
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
width: double.maxFinite,
|
|
||||||
height: double.maxFinite,
|
height: double.maxFinite,
|
||||||
|
),
|
||||||
|
items: _buildScreens(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumn() => Column(
|
List<Widget> _buildScreens() => [
|
||||||
mainAxisSize: MainAxisSize.max,
|
_buildFirstFailure(),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
_buildSecondFailure(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
_buildThirdFailure(),
|
||||||
children: _buildColumnChildren(),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildColumnChildren() =>
|
|
||||||
[_buildIconWrapper(), _buildSafeWrapper()];
|
|
||||||
|
|
||||||
Widget _buildIconWrapper() => Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 100),
|
|
||||||
child: _buildIcon(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg', height: 50);
|
|
||||||
|
|
||||||
Widget _buildSafeWrapper() => SafeArea(child: _buildBottomSectionWrapper());
|
|
||||||
|
|
||||||
Widget _buildBottomSectionWrapper() => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 50),
|
|
||||||
child: _buildBottomSectionColumn(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildBottomSectionColumn() => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: _buildBottomSectionChildren(),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildBottomSectionChildren() => [
|
|
||||||
_buildLoadingTextWrapper(),
|
|
||||||
verticalSpaceSmall,
|
|
||||||
_buildRetryButtonWrapper()
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildLoadingTextWrapper() => Row(
|
Widget _buildFirstFailure() => FirstFailureScreen(
|
||||||
mainAxisSize: MainAxisSize.min,
|
label: label,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: _buildLoadingTextChildren(),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildLoadingTextChildren() => [
|
|
||||||
_buildLoadingText(),
|
|
||||||
horizontalSpaceSmall,
|
|
||||||
_buildIndicatorWrapper(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget _buildLoadingText() =>
|
|
||||||
Text('$label ...', style: const TextStyle(color: kcWhite, fontSize: 16));
|
|
||||||
|
|
||||||
Widget _buildIndicatorWrapper() => SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: _buildIndicator(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildIndicator() =>
|
|
||||||
const CustomCircularProgressIndicator(color: kcWhite);
|
|
||||||
|
|
||||||
Widget _buildRetryButtonWrapper() => GestureDetector(
|
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: _buildRetryButton(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildRetryButton() => Text(
|
Widget _buildSecondFailure() => SecondFailureScreen(
|
||||||
'Retry',
|
label: label,
|
||||||
style: style16W600.copyWith(
|
onTap: onTap,
|
||||||
fontStyle: FontStyle.italic, decoration: TextDecoration.underline),
|
);
|
||||||
|
|
||||||
|
Widget _buildThirdFailure() => ThirdFailureScreen(
|
||||||
|
label: label,
|
||||||
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
165
lib/ui/views/failure/screens/first_failure_screen.dart
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.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/views/failure/failure_viewmodel.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/startup/startup_viewmodel.dart';
|
||||||
|
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
||||||
|
|
||||||
|
import '../../../common/translations/locale_keys.g.dart';
|
||||||
|
import '../../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
|
||||||
|
class FirstFailureScreen extends ViewModelWidget<FailureViewModel> {
|
||||||
|
final String label;
|
||||||
|
final GestureTapCallback onTap;
|
||||||
|
|
||||||
|
const FirstFailureScreen({super.key,required this.onTap,required this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, FailureViewModel viewModel) =>
|
||||||
|
_buildScaffoldWrapper();
|
||||||
|
|
||||||
|
Widget _buildScaffoldWrapper( ) => Scaffold(
|
||||||
|
backgroundColor: kcPrimaryColor,
|
||||||
|
body: _buildScaffoldPadding(),
|
||||||
|
);
|
||||||
|
Widget _buildScaffoldPadding( ) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildScaffold(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: _buildScaffoldChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildScaffoldChildren( ) =>
|
||||||
|
[_buildUpperColumn(), _buildLowerColumnWrapper()];
|
||||||
|
|
||||||
|
Widget _buildUpperColumn() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildUpperColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
|
|
||||||
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
|
'assets/icons/logo_white.svg',
|
||||||
|
height: 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumnWrapper( ) => Expanded(
|
||||||
|
child: _buildLowerColumn(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumn( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildLowerColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLowerColumnChildren( ) => [
|
||||||
|
_buildTitle(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildImageWrapper(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildSafeWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: 'እንግሊዝኛ\n',
|
||||||
|
style: style25W600,
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'በማንኛውም',
|
||||||
|
style: style25W400,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ሰዓት ',
|
||||||
|
style: style25W600,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'ይማሩ!',
|
||||||
|
style: style25W400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
|
||||||
|
|
||||||
|
Widget _buildImageClipper() => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
child: _buildImage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_1.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSafeWrapper( ) =>
|
||||||
|
SafeArea(child: _buildContinueButtonWrapper());
|
||||||
|
|
||||||
|
Widget _buildContinueButtonWrapper( ) => Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _buildLoadingTextContainer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildLoadingTextContainer() => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 50),
|
||||||
|
child: _buildLoadingTextWrapper(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLoadingTextWrapper() => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: _buildLoadingTextChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLoadingTextChildren() => [
|
||||||
|
_buildLoadingText(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildIndicatorWrapper(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildRetryButtonWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildLoadingText() =>
|
||||||
|
Text('$label...', style: style16W600);
|
||||||
|
|
||||||
|
Widget _buildIndicatorWrapper() => SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: _buildIndicator(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIndicator() =>
|
||||||
|
const CustomCircularProgressIndicator(color: kcWhite);
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildRetryButtonWrapper() => GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: _buildRetryButton(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildRetryButton() => Text(
|
||||||
|
'Retry',
|
||||||
|
style: style16W600.copyWith(
|
||||||
|
fontStyle: FontStyle.italic, decoration: TextDecoration.underline),
|
||||||
|
);
|
||||||
|
}
|
||||||
161
lib/ui/views/failure/screens/second_failure_screen.dart
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.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/views/failure/failure_viewmodel.dart';
|
||||||
|
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
||||||
|
|
||||||
|
import '../../../common/translations/locale_keys.g.dart';
|
||||||
|
import '../../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
|
||||||
|
class SecondFailureScreen extends ViewModelWidget<FailureViewModel> {
|
||||||
|
final String label;
|
||||||
|
final GestureTapCallback onTap;
|
||||||
|
|
||||||
|
const SecondFailureScreen(
|
||||||
|
{super.key, required this.onTap, required this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, FailureViewModel viewModel) =>
|
||||||
|
_buildScaffoldWrapper();
|
||||||
|
|
||||||
|
Widget _buildScaffoldWrapper() => Scaffold(
|
||||||
|
backgroundColor: Colors.amber,
|
||||||
|
body: _buildScaffoldPadding(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffoldPadding() => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildScaffold(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: _buildScaffoldChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildScaffoldChildren() =>
|
||||||
|
[_buildUpperColumn(), _buildLowerColumnWrapper()];
|
||||||
|
|
||||||
|
Widget _buildUpperColumn() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildUpperColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
|
|
||||||
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
|
'assets/icons/logo_purple.svg',
|
||||||
|
height: 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumnWrapper() => Expanded(
|
||||||
|
child: _buildLowerColumn(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumn() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildLowerColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLowerColumnChildren() => [
|
||||||
|
_buildTitle(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildImageWrapper(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildSafeWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: 'እንግሊዝኛ\n',
|
||||||
|
style: style25P600,
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'በማንኛውም',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' እድሜ ',
|
||||||
|
style: style25P600,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'ይማሩ!',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
|
||||||
|
|
||||||
|
Widget _buildImageClipper() => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
child: _buildImage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_2.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSafeWrapper() => SafeArea(child: _buildContinueButtonWrapper());
|
||||||
|
|
||||||
|
Widget _buildContinueButtonWrapper() => Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _buildLoadingTextContainer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLoadingTextContainer() => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 50),
|
||||||
|
child: _buildLoadingTextWrapper(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLoadingTextWrapper() => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: _buildLoadingTextChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLoadingTextChildren() => [
|
||||||
|
_buildLoadingText(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildIndicatorWrapper(),
|
||||||
|
verticalSpaceSmall,
|
||||||
|
_buildRetryButtonWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildLoadingText() => Text('$label...', style: style16P600);
|
||||||
|
|
||||||
|
Widget _buildIndicatorWrapper() => SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: _buildIndicator(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIndicator() =>
|
||||||
|
const CustomCircularProgressIndicator(color: kcPrimaryColor);
|
||||||
|
|
||||||
|
Widget _buildRetryButtonWrapper() => GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: _buildRetryButton(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildRetryButton() => Text(
|
||||||
|
'Retry',
|
||||||
|
style: style16P600.copyWith(
|
||||||
|
fontStyle: FontStyle.italic, decoration: TextDecoration.underline),
|
||||||
|
);
|
||||||
|
}
|
||||||
165
lib/ui/views/failure/screens/third_failure_screen.dart
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.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/views/failure/failure_viewmodel.dart';
|
||||||
|
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
||||||
|
|
||||||
|
import '../../../common/translations/locale_keys.g.dart';
|
||||||
|
import '../../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
|
||||||
|
class ThirdFailureScreen extends ViewModelWidget<FailureViewModel> {
|
||||||
|
final String label;
|
||||||
|
final GestureTapCallback onTap;
|
||||||
|
|
||||||
|
const ThirdFailureScreen(
|
||||||
|
{super.key, required this.onTap, required this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, FailureViewModel viewModel) =>
|
||||||
|
_buildScaffoldWrapper();
|
||||||
|
|
||||||
|
Widget _buildScaffoldWrapper( ) => Scaffold(
|
||||||
|
backgroundColor:kcWhite,
|
||||||
|
body: _buildScaffoldPadding(),
|
||||||
|
);
|
||||||
|
Widget _buildScaffoldPadding( ) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildScaffold(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: _buildScaffoldChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildScaffoldChildren( ) =>
|
||||||
|
[_buildUpperColumn(), _buildLowerColumnWrapper()];
|
||||||
|
|
||||||
|
Widget _buildUpperColumn() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildUpperColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
|
|
||||||
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
|
'assets/icons/logo_purple.svg',
|
||||||
|
height: 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumnWrapper( ) => Expanded(
|
||||||
|
child: _buildLowerColumn(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumn( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildLowerColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLowerColumnChildren( ) => [
|
||||||
|
_buildTitle(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildImageWrapper(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildSafeWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: 'እንግሊዝኛ\n',
|
||||||
|
style: style25P600,
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'በማንኛውም',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ቦታ ',
|
||||||
|
style: style25P600,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'ይማሩ!',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
|
||||||
|
|
||||||
|
Widget _buildImageClipper() => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
child: _buildImage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_3.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSafeWrapper( ) =>
|
||||||
|
SafeArea(child: _buildContinueButtonWrapper());
|
||||||
|
|
||||||
|
Widget _buildContinueButtonWrapper( ) => Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _buildLoadingTextContainer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildLoadingTextContainer() => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 50),
|
||||||
|
child: _buildLoadingTextWrapper(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLoadingTextWrapper() => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: _buildLoadingTextChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLoadingTextChildren() => [
|
||||||
|
_buildLoadingText(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildIndicatorWrapper(),
|
||||||
|
horizontalSpaceSmall,_buildRetryButtonWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildLoadingText() =>
|
||||||
|
Text('$label...', style: style16P600);
|
||||||
|
|
||||||
|
Widget _buildIndicatorWrapper() => SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: _buildIndicator(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIndicator() =>
|
||||||
|
const CustomCircularProgressIndicator(color: kcPrimaryColor);
|
||||||
|
|
||||||
|
Widget _buildRetryButtonWrapper() => GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: _buildRetryButton(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildRetryButton() => Text(
|
||||||
|
'Retry',
|
||||||
|
style: style16P600.copyWith(
|
||||||
|
fontStyle: FontStyle.italic, decoration: TextDecoration.underline),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -4,6 +4,8 @@ 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/enmus.dart';
|
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course/course_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_catalog/course_catalog_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
|
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
|
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
|
||||||
|
|
@ -17,6 +19,8 @@ class HomeView extends StackedView<HomeViewModel> {
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(HomeViewModel viewModel) async {
|
void onViewModelReady(HomeViewModel viewModel) async {
|
||||||
await viewModel.inAppUpdate();
|
await viewModel.inAppUpdate();
|
||||||
|
await viewModel.getUnreadNotifications();
|
||||||
|
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +84,7 @@ class HomeView extends StackedView<HomeViewModel> {
|
||||||
case 0:
|
case 0:
|
||||||
return const LearnProgramView();
|
return const LearnProgramView();
|
||||||
case 1:
|
case 1:
|
||||||
return const ComingSoon();
|
return const CourseView();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return const ProfileView();
|
return const ProfileView();
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../../services/authentication_service.dart';
|
||||||
|
import '../../../services/in_app_notification_service.dart';
|
||||||
import '../../../services/in_app_update_service.dart';
|
import '../../../services/in_app_update_service.dart';
|
||||||
|
|
||||||
class HomeViewModel extends ReactiveViewModel {
|
class HomeViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
|
||||||
|
|
||||||
final _bottomSheetService = locator<BottomSheetService>();
|
final _bottomSheetService = locator<BottomSheetService>();
|
||||||
|
|
||||||
|
|
@ -22,9 +22,11 @@ class HomeViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
|
final _inAppNotificationService = locator<InAppNotificationService>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices =>
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
[_authenticationService];
|
[_authenticationService,_inAppNotificationService];
|
||||||
|
|
||||||
// Current user
|
// Current user
|
||||||
User? get _user => _authenticationService.user;
|
User? get _user => _authenticationService.user;
|
||||||
|
|
@ -55,8 +57,6 @@ class HomeViewModel extends ReactiveViewModel {
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Remote api calls
|
// Remote api calls
|
||||||
|
|
||||||
// In-app update
|
// In-app update
|
||||||
|
|
@ -65,4 +65,12 @@ class HomeViewModel extends ReactiveViewModel {
|
||||||
await _inAppUpdateService.checkForUpdate();
|
await _inAppUpdateService.checkForUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Unread notifications
|
||||||
|
Future<void> getUnreadNotifications() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
await _inAppNotificationService.getUnreadNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'package:flutter_carousel_widget/flutter_carousel_widget.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/views/landing/screens/first_landing_screen.dart';
|
import 'package:yimaru_app/ui/views/landing/screens/first_landing_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/landing/screens/fourth_landing_screen.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart';
|
import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart';
|
import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart';
|
||||||
|
|
||||||
|
|
@ -44,17 +43,15 @@ class LandingView extends StackedView<LandingViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildScreens() => [
|
List<Widget> _buildScreens() => [
|
||||||
_buildFirstWelcome(),
|
_buildFirstLanding(),
|
||||||
_buildSecondWelcome(),
|
_buildSecondLanding(),
|
||||||
_buildThirdWelcome(),
|
_buildThirdLanding(),
|
||||||
_buildFourthWelcome()
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildFirstWelcome() => const FirstLandingScreen();
|
Widget _buildFirstLanding() => const FirstLandingScreen();
|
||||||
|
|
||||||
Widget _buildSecondWelcome() => const SecondLandingScreen();
|
Widget _buildSecondLanding() => const SecondLandingScreen();
|
||||||
|
|
||||||
Widget _buildThirdWelcome() => const ThirdLandingScreen();
|
Widget _buildThirdLanding() => const ThirdLandingScreen();
|
||||||
|
|
||||||
Widget _buildFourthWelcome() => const FourthLandingScreen();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
'assets/icons/logo.svg',
|
'assets/icons/logo_white.svg',
|
||||||
height: 25,
|
height: 25,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildImage() => Image.asset(
|
Widget _buildImage() => Image.asset(
|
||||||
'assets/images/landing_1.jpg',
|
'assets/images/landing_1.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -126,10 +126,10 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
Widget _buildContinueButton(LandingViewModel viewModel) =>
|
Widget _buildContinueButton(LandingViewModel viewModel) =>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Next',
|
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
onTap: viewModel.next,
|
text: 'Get Started',
|
||||||
backgroundColor: kcWhite,
|
backgroundColor: kcWhite,
|
||||||
foregroundColor: kcPrimaryColor,
|
foregroundColor: kcPrimaryColor,
|
||||||
|
onTap: () async => await viewModel.setFirstTimeInstall(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_svg/svg.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/custom_elevated_button.dart';
|
|
||||||
|
|
||||||
import '../../../widgets/custom_circular_progress_indicator.dart';
|
|
||||||
import '../landing_viewmodel.dart';
|
|
||||||
|
|
||||||
class FourthLandingScreen extends ViewModelWidget<LandingViewModel> {
|
|
||||||
const FourthLandingScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, LandingViewModel viewModel) =>
|
|
||||||
_buildScaffoldWrapper(viewModel);
|
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold(
|
|
||||||
backgroundColor: Colors.amber,
|
|
||||||
body: _buildScaffoldPadding(viewModel),
|
|
||||||
);
|
|
||||||
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
||||||
child: _buildScaffold(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildScaffold(LandingViewModel viewModel) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: _buildScaffoldChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
|
|
||||||
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
|
|
||||||
|
|
||||||
Widget _buildUpperColumn() => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: _buildUpperColumnChildren(),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren() =>
|
|
||||||
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
|
||||||
|
|
||||||
Widget _buildIconWrapper() => Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: _buildIcon(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
|
||||||
'assets/icons/logo.svg',
|
|
||||||
color: kcPrimaryColor,
|
|
||||||
height: 25,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
|
|
||||||
child: _buildLowerColumn(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildLowerColumn(LandingViewModel viewModel) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: _buildLowerColumnChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [
|
|
||||||
_buildTitle(),
|
|
||||||
verticalSpaceMedium,
|
|
||||||
_buildImageWrapper(),
|
|
||||||
verticalSpaceMedium,
|
|
||||||
_buildSafeWrapper(viewModel)
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget _buildTitle() => Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
text: 'እንግሊዝኛ\n',
|
|
||||||
style: style25P600,
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: 'በማንኛውም',
|
|
||||||
style: style25P400,
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: ' እድሜ ',
|
|
||||||
style: style25P600,
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: 'ይማሩ!',
|
|
||||||
style: style25P400,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
|
|
||||||
|
|
||||||
Widget _buildImageClipper() => ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
child: _buildImage(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildImage() => Image.asset(
|
|
||||||
'assets/images/landing_2.jpg',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
|
|
||||||
SafeArea(child: _buildContinueButtonWrapper(viewModel));
|
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: _buildButtonContainer(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildButtonContainer(LandingViewModel viewModel) => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 50),
|
|
||||||
child: _buildContinueButtonState(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildContinueButtonState(LandingViewModel viewModel) =>
|
|
||||||
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);
|
|
||||||
|
|
||||||
Widget _buildIndicator() =>
|
|
||||||
const CustomCircularProgressIndicator(color: kcWhite);
|
|
||||||
|
|
||||||
Widget _buildContinueButton(LandingViewModel viewModel) =>
|
|
||||||
CustomElevatedButton(
|
|
||||||
height: 55,
|
|
||||||
borderRadius: 25,
|
|
||||||
text: 'Get Started',
|
|
||||||
foregroundColor: kcWhite,
|
|
||||||
backgroundColor: kcPrimaryColor,
|
|
||||||
onTap: () async => await viewModel.setFirstTimeInstall(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -49,7 +49,7 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
'assets/icons/logo.svg',
|
'assets/icons/logo_purple.svg',
|
||||||
color: kcPrimaryColor,
|
color: kcPrimaryColor,
|
||||||
height: 25,
|
height: 25,
|
||||||
);
|
);
|
||||||
|
|
@ -101,7 +101,7 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildImage() => Image.asset(
|
Widget _buildImage() => Image.asset(
|
||||||
'assets/images/landing_2.jpg',
|
'assets/images/landing_2.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -127,10 +127,10 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
Widget _buildContinueButton(LandingViewModel viewModel) =>
|
Widget _buildContinueButton(LandingViewModel viewModel) =>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Next',
|
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
onTap: viewModel.next,
|
text: 'Get Started',
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
|
onTap: () async => await viewModel.setFirstTimeInstall(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
'assets/icons/logo.svg',
|
'assets/icons/logo_purple.svg',
|
||||||
color: kcPrimaryColor,
|
color: kcPrimaryColor,
|
||||||
height: 25,
|
height: 25,
|
||||||
);
|
);
|
||||||
|
|
@ -101,7 +101,7 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildImage() => Image.asset(
|
Widget _buildImage() => Image.asset(
|
||||||
'assets/images/landing_3.jpg',
|
'assets/images/landing_3.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -127,10 +127,10 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
Widget _buildContinueButton(LandingViewModel viewModel) =>
|
Widget _buildContinueButton(LandingViewModel viewModel) =>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Next',
|
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
onTap: viewModel.next,
|
text: 'Get Started',
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
|
onTap: () async => await viewModel.setFirstTimeInstall(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ class LanguageViewModel extends ReactiveViewModel {
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,16 @@ import 'learn_course_viewmodel.dart';
|
||||||
|
|
||||||
class LearnCourseView extends StackedView<LearnCourseViewModel> {
|
class LearnCourseView extends StackedView<LearnCourseViewModel> {
|
||||||
final int id;
|
final int id;
|
||||||
|
final bool first;
|
||||||
|
|
||||||
const LearnCourseView({Key? key, required this.id}) : super(key: key);
|
const LearnCourseView({Key? key, required this.id, required this.first})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
Future<void> _onPractice(
|
Future<void> _onPractice(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required LearnCourse course,
|
required LearnCourse course,
|
||||||
required LearnCourseViewModel viewModel}) async {
|
required LearnCourseViewModel viewModel}) async {
|
||||||
if (course.access?.completedCount == course.access?.totalCount) {
|
if (course.access?.isCompleted ?? false) {
|
||||||
await viewModel.navigateToLearnPractice(
|
await viewModel.navigateToLearnPractice(
|
||||||
id: course.id ?? 0, level: course.name ?? '');
|
id: course.id ?? 0, level: course.name ?? '');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -150,8 +152,8 @@ class LearnCourseView extends StackedView<LearnCourseViewModel> {
|
||||||
context: context,
|
context: context,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
course: viewModel.courses[index]),
|
course: viewModel.courses[index]),
|
||||||
onViewTap: () async =>
|
onViewTap: () async => await viewModel.navigateToLearnModule(
|
||||||
await viewModel.navigateToLearnModule(viewModel.courses[index]),
|
first: first && index == 0, course: viewModel.courses[index]),
|
||||||
),
|
),
|
||||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,10 @@ class LearnCourseViewModel extends ReactiveViewModel {
|
||||||
Future<void> navigateToLearnSubscription() async =>
|
Future<void> navigateToLearnSubscription() async =>
|
||||||
await _navigationService.navigateToLearnSubscriptionView();
|
await _navigationService.navigateToLearnSubscriptionView();
|
||||||
|
|
||||||
Future<void> navigateToLearnModule(LearnCourse course) async =>
|
Future<void> navigateToLearnModule(
|
||||||
_navigationService.navigateToLearnModuleView(course: course);
|
{required bool first, required LearnCourse course}) async =>
|
||||||
|
_navigationService.navigateToLearnModuleView(
|
||||||
|
first: first, course: course);
|
||||||
|
|
||||||
Future<void> navigateToLearnPractice(
|
Future<void> navigateToLearnPractice(
|
||||||
{required int id, required String level}) async =>
|
{required int id, required String level}) async =>
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,17 @@ import '../../widgets/small_app_bar.dart';
|
||||||
import 'learn_lesson_viewmodel.dart';
|
import 'learn_lesson_viewmodel.dart';
|
||||||
|
|
||||||
class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
||||||
|
final bool first;
|
||||||
final LearnModule module;
|
final LearnModule module;
|
||||||
|
|
||||||
const LearnLessonView({Key? key, required this.module}) : super(key: key);
|
const LearnLessonView({Key? key, required this.first, required this.module})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
Future<void> _onPractice(
|
Future<void> _onPractice(
|
||||||
{required int index,
|
{required int index,
|
||||||
required LearnLesson lesson,
|
required LearnLesson lesson,
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required LearnLessonViewModel viewModel}) async {
|
required LearnLessonViewModel viewModel}) async {
|
||||||
/* if (lesson.access?.isAccessible ?? false) {
|
|
||||||
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
|
||||||
} else {
|
|
||||||
await _showSheet(context: context, viewModel: viewModel);
|
|
||||||
}*/
|
|
||||||
if (index > 1) {
|
|
||||||
if (viewModel.user?.subscriptionStatus?.toLowerCase() == 'active') {
|
if (viewModel.user?.subscriptionStatus?.toLowerCase() == 'active') {
|
||||||
if (lesson.access?.isAccessible ?? false) {
|
if (lesson.access?.isAccessible ?? false) {
|
||||||
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
||||||
|
|
@ -39,10 +35,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
||||||
await _showSheet(context: context, viewModel: viewModel);
|
await _showSheet(context: context, viewModel: viewModel);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await viewModel.navigateToLearnSubscription();
|
if (first && index < 3) {
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (lesson.access?.isAccessible ?? false) {
|
|
||||||
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
||||||
} else {
|
} else {
|
||||||
await _showSheet(context: context, viewModel: viewModel);
|
await _showSheet(context: context, viewModel: viewModel);
|
||||||
|
|
@ -224,6 +217,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
index: index,
|
index: index,
|
||||||
|
first: first && index < 3,
|
||||||
lesson: viewModel.lessons[index],
|
lesson: viewModel.lessons[index],
|
||||||
last: index == viewModel.lessons.length - 1,
|
last: index == viewModel.lessons.length - 1,
|
||||||
onPracticeTap: () async => await _onPractice(
|
onPracticeTap: () async => await _onPractice(
|
||||||
|
|
@ -244,12 +238,14 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
required bool last,
|
required bool last,
|
||||||
required int index,
|
required int index,
|
||||||
|
required bool first,
|
||||||
required LearnLesson lesson,
|
required LearnLesson lesson,
|
||||||
required GestureTapCallback? onLessonTap,
|
required GestureTapCallback? onLessonTap,
|
||||||
required GestureTapCallback? onPracticeTap,
|
required GestureTapCallback? onPracticeTap,
|
||||||
}) =>
|
}) =>
|
||||||
LearnLessonTile(
|
LearnLessonTile(
|
||||||
last: last,
|
last: last,
|
||||||
|
first: first,
|
||||||
index: index,
|
index: index,
|
||||||
lesson: lesson,
|
lesson: lesson,
|
||||||
onLessonTap: onLessonTap,
|
onLessonTap: onLessonTap,
|
||||||
|
|
|
||||||
|
|
@ -31,21 +31,8 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
||||||
Future<void> _onPractice(
|
Future<void> _onPractice(
|
||||||
{required LearnLesson lesson,
|
{required LearnLesson lesson,
|
||||||
required LearnLessonDetailViewModel viewModel}) async {
|
required LearnLessonDetailViewModel viewModel}) async {
|
||||||
/* await viewModel.pause();
|
|
||||||
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
|
||||||
*/
|
|
||||||
if (index > 1) {
|
|
||||||
if (viewModel.user?.subscriptionStatus?.toLowerCase() == 'active') {
|
|
||||||
await viewModel.pause();
|
await viewModel.pause();
|
||||||
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
||||||
} else {
|
|
||||||
await viewModel.pause();
|
|
||||||
await viewModel.navigateToLearnSubscription();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await viewModel.pause();
|
|
||||||
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -16,27 +16,26 @@ import '../../widgets/small_app_bar.dart';
|
||||||
import 'learn_module_viewmodel.dart';
|
import 'learn_module_viewmodel.dart';
|
||||||
|
|
||||||
class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
||||||
|
final bool first;
|
||||||
final LearnCourse course;
|
final LearnCourse course;
|
||||||
|
|
||||||
const LearnModuleView({Key? key, required this.course}) : super(key: key);
|
const LearnModuleView({Key? key, required this.first, required this.course})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
Future<void> _onPractice(
|
Future<void> _onPractice(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required LearnModule module,
|
required LearnModule module,
|
||||||
required LearnModuleViewModel viewModel}) async {
|
required LearnModuleViewModel viewModel}) async {
|
||||||
if (module.access?.completedCount == module.access?.totalCount) {
|
if (module.access?.isCompleted ?? false ) {
|
||||||
await viewModel.navigateToLearnPractice(
|
await viewModel.navigateToLearnPractice(
|
||||||
id: module.id ?? 0, module: module.name ?? '');
|
id: module.id ?? 0, module: module.name ?? '');
|
||||||
} else {
|
} else {
|
||||||
if (module.access?.isAccessible ?? false) {
|
if (module.access?.isAccessible ?? false) {
|
||||||
print('Accessible');
|
|
||||||
await _showSheet(
|
await _showSheet(
|
||||||
context: context,
|
context: context,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
practice: PracticeReason.module);
|
practice: PracticeReason.module);
|
||||||
} else {
|
} else {
|
||||||
print('Inaccessible');
|
|
||||||
|
|
||||||
await _showSheet(
|
await _showSheet(
|
||||||
context: context,
|
context: context,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
|
|
@ -201,8 +200,8 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
module: viewModel.modules[index],
|
module: viewModel.modules[index],
|
||||||
),
|
),
|
||||||
onModuleTap: () async =>
|
onModuleTap: () async => await viewModel.navigateToLearnLesson(
|
||||||
await viewModel.navigateToLearnLesson(viewModel.modules[index]),
|
first: first && index == 0, module: viewModel.modules[index]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,10 @@ class LearnModuleViewModel extends ReactiveViewModel {
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToLearnLesson(LearnModule module) async =>
|
Future<void> navigateToLearnLesson(
|
||||||
await _navigationService.navigateToLearnLessonView(module: module);
|
{required bool first, required LearnModule module}) async =>
|
||||||
|
await _navigationService.navigateToLearnLessonView(
|
||||||
|
first: first, module: module);
|
||||||
|
|
||||||
Future<void> navigateToLearnPractice(
|
Future<void> navigateToLearnPractice(
|
||||||
{required int id, required String module}) async =>
|
{required int id, required String module}) async =>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:yimaru_app/services/voice_recorder_service.dart';
|
||||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
import '../../../app/app.router.dart';
|
||||||
import '../../../models/learn_question.dart';
|
import '../../../models/learn_question.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/audio_player_service.dart';
|
import '../../../services/audio_player_service.dart';
|
||||||
|
|
@ -264,6 +265,9 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
Future<void> navigateToLearnSubscription() async =>
|
||||||
|
await _navigationService.navigateToLearnSubscriptionView();
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
// Refresh url
|
// Refresh url
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,14 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
||||||
required this.subtitle,
|
required this.subtitle,
|
||||||
required this.practice});
|
required this.practice});
|
||||||
|
|
||||||
|
Future<void> _practice(LearnPracticeViewModel viewModel) async {
|
||||||
|
if (viewModel.user?.subscriptionStatus?.toLowerCase() == 'active') {
|
||||||
|
viewModel.goTo(1);
|
||||||
|
} else {
|
||||||
|
await viewModel.navigateToLearnSubscription();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
|
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
|
||||||
await viewModel.stopRecording();
|
await viewModel.stopRecording();
|
||||||
viewModel.pop();
|
viewModel.pop();
|
||||||
|
|
@ -207,7 +215,7 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
||||||
text: label,
|
text: label,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
onTap: () => viewModel.goTo(1),
|
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
|
onTap: () async => await _practice(viewModel),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ class LearnProgramView extends StackedView<LearnProgramViewModel> {
|
||||||
Widget _buildAppBar(LearnProgramViewModel viewModel) => ProfileAppBar(
|
Widget _buildAppBar(LearnProgramViewModel viewModel) => ProfileAppBar(
|
||||||
name: viewModel.user?.firstName,
|
name: viewModel.user?.firstName,
|
||||||
profileImage: viewModel.user?.profilePicture,
|
profileImage: viewModel.user?.profilePicture,
|
||||||
|
unreadCount: viewModel.unreadCount.toString(),
|
||||||
|
onTap: () async => await viewModel.navigateToNotification(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgramsColumnWrapper(LearnProgramViewModel viewModel) =>
|
Widget _buildProgramsColumnWrapper(LearnProgramViewModel viewModel) =>
|
||||||
|
|
@ -87,8 +89,8 @@ class LearnProgramView extends StackedView<LearnProgramViewModel> {
|
||||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
program: viewModel.learnPrograms[index],
|
program: viewModel.learnPrograms[index],
|
||||||
onTap: () async => await viewModel
|
onTap: () async => await viewModel.navigateToLearnCourse(
|
||||||
.navigateToLearnCourse(viewModel.learnPrograms[index].id ?? 0),
|
first: index == 0, id: viewModel.learnPrograms[index].id ?? 0),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:yimaru_app/models/learn_program.dart';
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../models/user.dart';
|
import '../../../models/user.dart';
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../../services/authentication_service.dart';
|
||||||
|
import '../../../services/in_app_notification_service.dart';
|
||||||
import '../../../services/learn_service.dart';
|
import '../../../services/learn_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
@ -21,23 +22,34 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
|
final _inAppNotificationService = locator<InAppNotificationService>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices =>
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
[_learnService, _authenticationService];
|
[_learnService, _authenticationService,_inAppNotificationService];
|
||||||
|
|
||||||
// Current user
|
// Current user
|
||||||
User? get _user => _authenticationService.user;
|
User? get _user => _authenticationService.user;
|
||||||
|
|
||||||
User? get user => _user;
|
User? get user => _user;
|
||||||
|
|
||||||
|
// Notification count
|
||||||
|
int get _unreadCount => _inAppNotificationService.unreadCount;
|
||||||
|
|
||||||
|
int get unreadCount => _unreadCount;
|
||||||
|
|
||||||
// Learn programs
|
// Learn programs
|
||||||
List<LearnProgram> get _learnPrograms => _learnService.programs;
|
List<LearnProgram> get _learnPrograms => _learnService.programs;
|
||||||
|
|
||||||
List<LearnProgram> get learnPrograms => _learnPrograms;
|
List<LearnProgram> get learnPrograms => _learnPrograms;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
Future<void> navigateToLearnCourse(int id) async =>
|
Future<void> navigateToNotification() async =>
|
||||||
_navigationService.navigateToLearnCourseView(id: id);
|
await _navigationService.navigateToNotificationView();
|
||||||
|
|
||||||
|
Future<void> navigateToLearnCourse(
|
||||||
|
{required int id, required bool first}) async =>
|
||||||
|
_navigationService.navigateToLearnCourseView(id: id, first: first);
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
|
|
|
||||||
112
lib/ui/views/notification/notification_view.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/ui/widgets/notification_card.dart';
|
||||||
|
|
||||||
|
import '../../../models/in_app_notification.dart';
|
||||||
|
import '../../common/app_colors.dart';
|
||||||
|
import '../../common/enmus.dart';
|
||||||
|
import '../../common/translations/locale_keys.g.dart';
|
||||||
|
import '../../common/ui_helpers.dart';
|
||||||
|
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
import '../../widgets/small_app_bar.dart';
|
||||||
|
import 'notification_viewmodel.dart';
|
||||||
|
|
||||||
|
class NotificationView extends StackedView<NotificationViewModel> {
|
||||||
|
const NotificationView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onViewModelReady(NotificationViewModel viewModel) async {
|
||||||
|
await viewModel.getAllNotifications();
|
||||||
|
await viewModel.markNotificationsRead();
|
||||||
|
super.onViewModelReady(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NotificationViewModel viewModelBuilder(BuildContext context) =>
|
||||||
|
NotificationViewModel();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget builder(
|
||||||
|
BuildContext context,
|
||||||
|
NotificationViewModel viewModel,
|
||||||
|
Widget? child,
|
||||||
|
) =>
|
||||||
|
_buildScaffoldWrapper(viewModel);
|
||||||
|
|
||||||
|
Widget _buildScaffoldWrapper(NotificationViewModel viewModel) => Scaffold(
|
||||||
|
backgroundColor: kcBackgroundColor,
|
||||||
|
body: _buildScaffoldContainer(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffoldContainer(NotificationViewModel viewModel) => Container(
|
||||||
|
decoration: bgDecoration,
|
||||||
|
child: _buildScaffold(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold(NotificationViewModel viewModel) =>
|
||||||
|
SafeArea(child: _buildBodyWrapper(viewModel));
|
||||||
|
|
||||||
|
Widget _buildBodyWrapper(NotificationViewModel viewModel) =>
|
||||||
|
_buildBody(viewModel);
|
||||||
|
|
||||||
|
Widget _buildBody(NotificationViewModel viewModel) => _buildColumn(viewModel);
|
||||||
|
|
||||||
|
Widget _buildColumn(NotificationViewModel viewModel) => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _buildColumnChildren(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildColumnChildren(NotificationViewModel viewModel) => [
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildAppBarWrapper(viewModel),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildNotificationsColumnWrapper(viewModel)
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildAppBarWrapper(NotificationViewModel viewModel) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildAppbar(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildAppbar(NotificationViewModel viewModel) => SmallAppBar(
|
||||||
|
showBackButton: true,
|
||||||
|
onPop: viewModel.pop,
|
||||||
|
title: LocaleKeys.notifications.tr(),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildNotificationsColumnWrapper(NotificationViewModel viewModel) =>
|
||||||
|
Expanded(child: _buildNotificationsColumnScrollView(viewModel));
|
||||||
|
|
||||||
|
Widget _buildNotificationsColumnScrollView(NotificationViewModel viewModel) =>
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: _buildListViewBuilder(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildListViewBuilder(NotificationViewModel viewModel) =>
|
||||||
|
viewModel.busy(StateObjects.notifications)
|
||||||
|
? _buildProgressIndicator()
|
||||||
|
: _buildListView(viewModel);
|
||||||
|
|
||||||
|
Widget _buildProgressIndicator() => const Center(
|
||||||
|
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildListView(NotificationViewModel viewModel) => ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: viewModel.notifications.length,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||||
|
itemBuilder: (context, index) => _buildCard(
|
||||||
|
notification: viewModel.notifications[index],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildCard({
|
||||||
|
required InAppNotification notification,
|
||||||
|
}) =>
|
||||||
|
NotificationCard(notification: notification);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
72
lib/ui/views/notification/notification_viewmodel.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
|
import '../../../app/app.locator.dart';
|
||||||
|
import '../../../models/in_app_notification.dart';
|
||||||
|
import '../../../services/in_app_notification_service.dart';
|
||||||
|
import '../../../services/localization_service.dart';
|
||||||
|
import '../../../services/status_checker_service.dart';
|
||||||
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
|
class NotificationViewModel extends ReactiveViewModel {
|
||||||
|
// Dependency injection
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
|
final _localizationService = locator<LocalizationService>();
|
||||||
|
|
||||||
|
final _inAppNotificationService = locator<InAppNotificationService>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
|
[_localizationService, _inAppNotificationService];
|
||||||
|
|
||||||
|
// Languages
|
||||||
|
Map<String, dynamic> get _selectedLanguage =>
|
||||||
|
_localizationService.selectedLanguage;
|
||||||
|
|
||||||
|
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
List<InAppNotification> get _notifications =>
|
||||||
|
_inAppNotificationService.notifications;
|
||||||
|
|
||||||
|
List<InAppNotification> get notifications => _notifications;
|
||||||
|
|
||||||
|
// Notification count
|
||||||
|
int get _unreadCount => _inAppNotificationService.unreadCount;
|
||||||
|
|
||||||
|
int get unreadCount => _unreadCount;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
// Remote api call
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
Future<void> getAllNotifications() async =>
|
||||||
|
await runBusyFuture(_getAllNotifications(),
|
||||||
|
busyObject: StateObjects.notifications);
|
||||||
|
|
||||||
|
Future<void> _getAllNotifications() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
await _inAppNotificationService.getAllNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> getUnreadNotifications() async =>
|
||||||
|
await runBusyFuture(_getUnreadNotifications());
|
||||||
|
|
||||||
|
Future<void> _getUnreadNotifications() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
await _inAppNotificationService.getUnreadNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> markNotificationsRead() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
await _inAppNotificationService.markNotificationRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -205,8 +205,8 @@ class OnboardingViewModel extends ReactiveViewModel
|
||||||
_selectedCountry = value;
|
_selectedCountry = value;
|
||||||
if (value?.code?.toLowerCase().trim() == 'et') {
|
if (value?.code?.toLowerCase().trim() == 'et') {
|
||||||
_dropdownRegion = true;
|
_dropdownRegion = true;
|
||||||
_selectedRegion = _regions
|
_selectedRegion = _regions.firstWhere(
|
||||||
.firstWhere((e) => e.code?.toLowerCase().trim().contains('addis_ababa') ?? false);
|
(e) => e.code?.toLowerCase().trim().contains('addis_ababa') ?? false);
|
||||||
} else {
|
} else {
|
||||||
_dropdownRegion = false;
|
_dropdownRegion = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ class PaymentView extends StackedView<PaymentViewModel> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(PaymentViewModel viewModel) async {
|
void onViewModelReady(PaymentViewModel viewModel) async {
|
||||||
await viewModel.createLearnSubscriptionRequest(phone: phone,subscription: subscription);
|
await viewModel.createLearnSubscriptionRequest(
|
||||||
|
phone: phone, subscription: subscription);
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,18 +45,22 @@ class PaymentViewModel extends ReactiveViewModel {
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
// Learn subscription
|
// Learn subscription
|
||||||
Future<void> createLearnSubscriptionRequest({required String phone,required LearnSubscription subscription}) async =>
|
Future<void> createLearnSubscriptionRequest(
|
||||||
await runBusyFuture(_createLearnSubscriptionRequest(phone: phone,subscription: subscription),
|
{required String phone,
|
||||||
|
required LearnSubscription subscription}) async =>
|
||||||
|
await runBusyFuture(
|
||||||
|
_createLearnSubscriptionRequest(
|
||||||
|
phone: phone, subscription: subscription),
|
||||||
busyObject: StateObjects.learnSubscription);
|
busyObject: StateObjects.learnSubscription);
|
||||||
|
|
||||||
Future<void> _createLearnSubscriptionRequest({required String phone,required LearnSubscription subscription}) async {
|
Future<void> _createLearnSubscriptionRequest(
|
||||||
|
{required String phone, required LearnSubscription subscription}) async {
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
Map<String, dynamic> data = {
|
Map<String, dynamic> data = {
|
||||||
'provider': 'CHAPA',
|
'provider': 'CHAPA',
|
||||||
'phone': '251$phone',
|
'phone': '251$phone',
|
||||||
'email': 'test@gmail.com',
|
'email': 'test@gmail.com',
|
||||||
'plan_id': subscription.id,
|
'plan_id': subscription.id,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Map<String, dynamic> response =
|
Map<String, dynamic> response =
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.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/notification_icon.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/profile_card.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/profile_image.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
|
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
|
||||||
|
|
@ -89,8 +90,9 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
required ProfileViewModel viewModel}) =>
|
required ProfileViewModel viewModel}) =>
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
|
verticalSpaceSmall,
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildNotificationIconWrapper(),
|
_buildNotificationIconWrapper(viewModel),
|
||||||
_buildProfileSection(context: context, viewModel: viewModel),
|
_buildProfileSection(context: context, viewModel: viewModel),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildViewProfileButton(viewModel),
|
_buildViewProfileButton(viewModel),
|
||||||
|
|
@ -102,12 +104,10 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildNotificationIconWrapper() =>
|
Widget _buildNotificationIconWrapper(ProfileViewModel viewModel) =>
|
||||||
Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon());
|
NotificationIcon(
|
||||||
|
count: viewModel.unreadCount.toString(),
|
||||||
Widget _buildNotificationIcon() => const Icon(
|
onTap: () async => await viewModel.navigateToNotification(),
|
||||||
Icons.notifications_none,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProfileSection(
|
Widget _buildProfileSection(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import '../../../models/user.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../../services/authentication_service.dart';
|
||||||
import '../../../services/google_auth_service.dart';
|
import '../../../services/google_auth_service.dart';
|
||||||
|
import '../../../services/in_app_notification_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
|
||||||
|
|
@ -23,21 +24,26 @@ class ProfileViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
final _googleAuthService = locator<GoogleAuthService>();
|
|
||||||
|
|
||||||
final _imagePickerService = locator<ImagePickerService>();
|
final _imagePickerService = locator<ImagePickerService>();
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
|
final _inAppNotificationService = locator<InAppNotificationService>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices =>
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
[_authenticationService];
|
[_authenticationService,_inAppNotificationService];
|
||||||
|
|
||||||
// Current user
|
// Current user
|
||||||
User? get _user => _authenticationService.user;
|
User? get _user => _authenticationService.user;
|
||||||
|
|
||||||
User? get user => _user;
|
User? get user => _user;
|
||||||
|
|
||||||
|
// Notification count
|
||||||
|
int get _unreadCount => _inAppNotificationService.unreadCount;
|
||||||
|
|
||||||
|
int get unreadCount => _unreadCount;
|
||||||
|
|
||||||
// Image picker
|
// Image picker
|
||||||
Future<void> openCamera() async =>
|
Future<void> openCamera() async =>
|
||||||
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
|
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
|
||||||
|
|
@ -80,21 +86,24 @@ class ProfileViewModel extends ReactiveViewModel {
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToProfileDetail() async =>
|
Future<void> navigateToSupport() async =>
|
||||||
await _navigationService.navigateToProfileDetailView();
|
await _navigationService.navigateToSupportView();
|
||||||
|
|
||||||
Future<void> navigateToDownloads() async =>
|
|
||||||
await _navigationService.navigateToDownloadsView();
|
|
||||||
|
|
||||||
Future<void> navigateToProgress() async =>
|
Future<void> navigateToProgress() async =>
|
||||||
await _navigationService.navigateToProgressView();
|
await _navigationService.navigateToProgressView();
|
||||||
|
|
||||||
|
Future<void> navigateToDownloads() async =>
|
||||||
|
await _navigationService.navigateToDownloadsView();
|
||||||
|
|
||||||
|
Future<void> navigateToNotification() async =>
|
||||||
|
await _navigationService.navigateToNotificationView();
|
||||||
|
|
||||||
|
Future<void> navigateToProfileDetail() async =>
|
||||||
|
await _navigationService.navigateToProfileDetailView();
|
||||||
|
|
||||||
Future<void> navigateToAccountPrivacy() async =>
|
Future<void> navigateToAccountPrivacy() async =>
|
||||||
await _navigationService.navigateToAccountPrivacyView();
|
await _navigationService.navigateToAccountPrivacyView();
|
||||||
|
|
||||||
Future<void> navigateToSupport() async =>
|
|
||||||
await _navigationService.navigateToSupportView();
|
|
||||||
|
|
||||||
Future<void> navigateToLogin() async =>
|
Future<void> navigateToLogin() async =>
|
||||||
await _navigationService.clearStackAndShow(Routes.loginView);
|
await _navigationService.clearStackAndShow(Routes.loginView);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,8 @@ class ProfileDetailViewModel extends ReactiveViewModel
|
||||||
|
|
||||||
void setEthiopianRegion(String region) {
|
void setEthiopianRegion(String region) {
|
||||||
_dropdownRegion = true;
|
_dropdownRegion = true;
|
||||||
_selectedRegion =
|
_selectedRegion = _regions
|
||||||
_regions.firstWhere((r) => r.code?.toLowerCase() == region.toLowerCase());
|
.firstWhere((r) => r.code?.toLowerCase() == region.toLowerCase());
|
||||||
|
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ class ProgressViewModel extends ReactiveViewModel {
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
List<ListenableServiceMixin> get listenableServices => [_learnService];
|
||||||
|
|
||||||
|
|
||||||
// Total practice count
|
// Total practice count
|
||||||
int get _totalCount => _learnService.totalCount;
|
int get _totalCount => _learnService.totalCount;
|
||||||
|
|
||||||
|
|
@ -33,7 +32,6 @@ class ProgressViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
int get totalProgress => _totalProgress;
|
int get totalProgress => _totalProgress;
|
||||||
|
|
||||||
|
|
||||||
// Courses
|
// Courses
|
||||||
final List<Map<String, dynamic>> _courses = [
|
final List<Map<String, dynamic>> _courses = [
|
||||||
{
|
{
|
||||||
|
|
@ -53,7 +51,8 @@ class ProgressViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
// Learning progress
|
// Learning progress
|
||||||
|
|
||||||
Future<void> getProgressSummary() async => runBusyFuture(_getProgressSummary(),
|
Future<void> getProgressSummary() async =>
|
||||||
|
runBusyFuture(_getProgressSummary(),
|
||||||
busyObject: StateObjects.progressSummary);
|
busyObject: StateObjects.progressSummary);
|
||||||
|
|
||||||
Future<void> _getProgressSummary() async {
|
Future<void> _getProgressSummary() async {
|
||||||
|
|
|
||||||
151
lib/ui/views/startup/screens/first_startup_screen.dart
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.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/views/startup/startup_viewmodel.dart';
|
||||||
|
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
||||||
|
|
||||||
|
import '../../../common/translations/locale_keys.g.dart';
|
||||||
|
import '../../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
|
||||||
|
class FirstStartupScreen extends ViewModelWidget<StartupViewModel> {
|
||||||
|
final String? label;
|
||||||
|
|
||||||
|
const FirstStartupScreen({super.key,this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, StartupViewModel viewModel) =>
|
||||||
|
_buildScaffoldWrapper();
|
||||||
|
|
||||||
|
Widget _buildScaffoldWrapper( ) => Scaffold(
|
||||||
|
backgroundColor: kcPrimaryColor,
|
||||||
|
body: _buildScaffoldPadding(),
|
||||||
|
);
|
||||||
|
Widget _buildScaffoldPadding( ) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildScaffold(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: _buildScaffoldChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildScaffoldChildren( ) =>
|
||||||
|
[_buildUpperColumn(), _buildLowerColumnWrapper()];
|
||||||
|
|
||||||
|
Widget _buildUpperColumn() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildUpperColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
|
|
||||||
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
|
'assets/icons/logo_white.svg',
|
||||||
|
height: 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumnWrapper( ) => Expanded(
|
||||||
|
child: _buildLowerColumn(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumn( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildLowerColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLowerColumnChildren( ) => [
|
||||||
|
_buildTitle(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildImageWrapper(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildSafeWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: 'እንግሊዝኛ\n',
|
||||||
|
style: style25W600,
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'በማንኛውም',
|
||||||
|
style: style25W400,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ሰዓት ',
|
||||||
|
style: style25W600,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'ይማሩ!',
|
||||||
|
style: style25W400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
|
||||||
|
|
||||||
|
Widget _buildImageClipper() => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
child: _buildImage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_1.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSafeWrapper( ) =>
|
||||||
|
SafeArea(child: _buildContinueButtonWrapper());
|
||||||
|
|
||||||
|
Widget _buildContinueButtonWrapper( ) => Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _buildLoadingTextContainer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildLoadingTextContainer() => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 50),
|
||||||
|
child: _buildLoadingTextWrapper(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLoadingTextWrapper() => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: _buildLoadingTextChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLoadingTextChildren() => [
|
||||||
|
_buildLoadingText(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildIndicatorWrapper(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildLoadingText() =>
|
||||||
|
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16W600);
|
||||||
|
|
||||||
|
Widget _buildIndicatorWrapper() => SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: _buildIndicator(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIndicator() =>
|
||||||
|
const CustomCircularProgressIndicator(color: kcWhite);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
153
lib/ui/views/startup/screens/second_startup_screen.dart
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.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/custom_elevated_button.dart';
|
||||||
|
|
||||||
|
import '../../../common/translations/locale_keys.g.dart';
|
||||||
|
import '../../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
import '../startup_viewmodel.dart';
|
||||||
|
|
||||||
|
class SecondStartupScreen extends ViewModelWidget<StartupViewModel> {
|
||||||
|
final String? label;
|
||||||
|
|
||||||
|
const SecondStartupScreen({super.key,this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, StartupViewModel viewModel) =>
|
||||||
|
_buildScaffoldWrapper();
|
||||||
|
|
||||||
|
Widget _buildScaffoldWrapper( ) => Scaffold(
|
||||||
|
backgroundColor: Colors.amber,
|
||||||
|
body: _buildScaffoldPadding(),
|
||||||
|
);
|
||||||
|
Widget _buildScaffoldPadding( ) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildScaffold(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: _buildScaffoldChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildScaffoldChildren( ) =>
|
||||||
|
[_buildUpperColumn(), _buildLowerColumnWrapper()];
|
||||||
|
|
||||||
|
Widget _buildUpperColumn() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildUpperColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
|
|
||||||
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
|
'assets/icons/logo_purple.svg',
|
||||||
|
height: 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumnWrapper( ) => Expanded(
|
||||||
|
child: _buildLowerColumn(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumn( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildLowerColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLowerColumnChildren( ) => [
|
||||||
|
_buildTitle(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildImageWrapper(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildSafeWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: 'እንግሊዝኛ\n',
|
||||||
|
style: style25P600,
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'በማንኛውም',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' እድሜ ',
|
||||||
|
style: style25P600,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'ይማሩ!',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
|
||||||
|
|
||||||
|
Widget _buildImageClipper() => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
child: _buildImage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_2.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSafeWrapper( ) =>
|
||||||
|
SafeArea(child: _buildContinueButtonWrapper());
|
||||||
|
|
||||||
|
Widget _buildContinueButtonWrapper( ) => Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _buildLoadingTextContainer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildLoadingTextContainer() => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 50),
|
||||||
|
child: _buildLoadingTextWrapper(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLoadingTextWrapper() => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: _buildLoadingTextChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLoadingTextChildren() => [
|
||||||
|
_buildLoadingText(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildIndicatorWrapper(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildLoadingText() =>
|
||||||
|
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16P600);
|
||||||
|
|
||||||
|
Widget _buildIndicatorWrapper() => SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: _buildIndicator(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIndicator() =>
|
||||||
|
const CustomCircularProgressIndicator(color: kcPrimaryColor);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
153
lib/ui/views/startup/screens/third_startup_screen.dart
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.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/custom_elevated_button.dart';
|
||||||
|
|
||||||
|
import '../../../common/translations/locale_keys.g.dart';
|
||||||
|
import '../../../widgets/custom_circular_progress_indicator.dart';
|
||||||
|
import '../startup_viewmodel.dart';
|
||||||
|
|
||||||
|
class ThirdStartupScreen extends ViewModelWidget<StartupViewModel> {
|
||||||
|
final String? label;
|
||||||
|
|
||||||
|
const ThirdStartupScreen({super.key,this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, StartupViewModel viewModel) =>
|
||||||
|
_buildScaffoldWrapper();
|
||||||
|
|
||||||
|
Widget _buildScaffoldWrapper( ) => Scaffold(
|
||||||
|
backgroundColor:kcWhite,
|
||||||
|
body: _buildScaffoldPadding(),
|
||||||
|
);
|
||||||
|
Widget _buildScaffoldPadding( ) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildScaffold(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: _buildScaffoldChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildScaffoldChildren( ) =>
|
||||||
|
[_buildUpperColumn(), _buildLowerColumnWrapper()];
|
||||||
|
|
||||||
|
Widget _buildUpperColumn() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildUpperColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
|
|
||||||
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
|
'assets/icons/logo_purple.svg',
|
||||||
|
height: 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumnWrapper( ) => Expanded(
|
||||||
|
child: _buildLowerColumn(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLowerColumn( ) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: _buildLowerColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLowerColumnChildren( ) => [
|
||||||
|
_buildTitle(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildImageWrapper(),
|
||||||
|
verticalSpaceMedium,
|
||||||
|
_buildSafeWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: 'እንግሊዝኛ\n',
|
||||||
|
style: style25P600,
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'በማንኛውም',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ቦታ ',
|
||||||
|
style: style25P600,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'ይማሩ!',
|
||||||
|
style: style25P400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
|
||||||
|
|
||||||
|
Widget _buildImageClipper() => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
child: _buildImage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_3.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSafeWrapper( ) =>
|
||||||
|
SafeArea(child: _buildContinueButtonWrapper());
|
||||||
|
|
||||||
|
Widget _buildContinueButtonWrapper( ) => Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _buildLoadingTextContainer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildLoadingTextContainer() => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 50),
|
||||||
|
child: _buildLoadingTextWrapper(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildLoadingTextWrapper() => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: _buildLoadingTextChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildLoadingTextChildren() => [
|
||||||
|
_buildLoadingText(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildIndicatorWrapper(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildLoadingText() =>
|
||||||
|
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16P600);
|
||||||
|
|
||||||
|
Widget _buildIndicatorWrapper() => SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: _buildIndicator(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIndicator() =>
|
||||||
|
const CustomCircularProgressIndicator(color: kcPrimaryColor);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter_carousel_widget/flutter_carousel_widget.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/views/startup/screens/first_startup_screen.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/startup/screens/second_startup_screen.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/startup/screens/third_startup_screen.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
|
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
|
@ -13,6 +17,7 @@ import 'startup_viewmodel.dart';
|
||||||
|
|
||||||
class StartupView extends StackedView<StartupViewModel> {
|
class StartupView extends StackedView<StartupViewModel> {
|
||||||
final String? label;
|
final String? label;
|
||||||
|
|
||||||
const StartupView({Key? key, this.label}) : super(key: key);
|
const StartupView({Key? key, this.label}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -24,83 +29,36 @@ class StartupView extends StackedView<StartupViewModel> {
|
||||||
_buildScaffoldWrapper(viewModel);
|
_buildScaffoldWrapper(viewModel);
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(StartupViewModel viewModel) => Scaffold(
|
Widget _buildScaffoldWrapper(StartupViewModel viewModel) => Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
body: _buildScaffoldState(viewModel),
|
body: _buildStartupScreens(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffoldState(StartupViewModel viewModel) =>
|
Widget _buildStartupScreens(StartupViewModel viewModel) => FlutterCarousel(
|
||||||
viewModel.busy(StateObjects.startupView)
|
options: FlutterCarouselOptions(
|
||||||
? _buildStartUpView()
|
autoPlay: true,
|
||||||
: _buildScaffold();
|
viewportFraction: 1,
|
||||||
|
showIndicator: false,
|
||||||
Widget _buildStartUpView() =>
|
|
||||||
StartupView(label: LocaleKeys.checking_user_info.tr());
|
|
||||||
|
|
||||||
Widget _buildScaffold() => Stack(
|
|
||||||
children: _buildScaffoldChildren(),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildScaffoldChildren() => [
|
|
||||||
_buildBackground(),
|
|
||||||
_buildColumn(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget _buildBackground() => Image.asset(
|
|
||||||
'assets/images/loading.png',
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
width: double.maxFinite,
|
|
||||||
height: double.maxFinite,
|
height: double.maxFinite,
|
||||||
|
),
|
||||||
|
items: _buildScreens(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumn() => Column(
|
List<Widget> _buildScreens() => [
|
||||||
mainAxisSize: MainAxisSize.max,
|
_buildFirstStartup(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
_buildSecondStartup(),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
_buildThirdWelcome(),
|
||||||
children: _buildUpperColumnChildren(),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren() =>
|
|
||||||
[_buildIconWrapper(), _buildSafeWrapper()];
|
|
||||||
|
|
||||||
Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer());
|
|
||||||
|
|
||||||
Widget _buildLoadingTextContainer() => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 50),
|
|
||||||
child: _buildLoadingTextWrapper(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildLoadingTextWrapper() => Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: _buildLoadingTextChildren(),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildLoadingTextChildren() => [
|
|
||||||
_buildLoadingText(),
|
|
||||||
horizontalSpaceSmall,
|
|
||||||
_buildIndicatorWrapper(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildLoadingText() =>
|
Widget _buildFirstStartup() => FirstStartupScreen(
|
||||||
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16W600);
|
label: label,
|
||||||
|
|
||||||
Widget _buildIndicatorWrapper() => SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: _buildIndicator(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildIndicator() =>
|
Widget _buildSecondStartup() => SecondStartupScreen(
|
||||||
const CustomCircularProgressIndicator(color: kcWhite);
|
label: label,
|
||||||
|
|
||||||
Widget _buildIconWrapper() => Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 120),
|
|
||||||
child: _buildIcon(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildThirdWelcome() => ThirdStartupScreen(
|
||||||
'assets/icons/logo.svg',
|
label: label,
|
||||||
height: 50,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import '../../../app/app.router.dart';
|
||||||
import '../../../models/user.dart';
|
import '../../../models/user.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/image_downloader_service.dart';
|
import '../../../services/image_downloader_service.dart';
|
||||||
|
import '../../../services/in_app_notification_service.dart';
|
||||||
import '../../../services/localization_service.dart';
|
import '../../../services/localization_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
@ -28,6 +29,7 @@ class StartupViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
final _imageDownloaderService = locator<ImageDownloaderService>();
|
final _imageDownloaderService = locator<ImageDownloaderService>();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<ListenableServiceMixin> get listenableServices =>
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
[_onboardingService, _authenticationService];
|
[_onboardingService, _authenticationService];
|
||||||
|
|
@ -133,8 +135,6 @@ class StartupViewModel extends ReactiveViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote api call
|
|
||||||
|
|
||||||
// Onboarding fields
|
// Onboarding fields
|
||||||
Future<void> getOnboardingFields() async {
|
Future<void> getOnboardingFields() async {
|
||||||
bool response = await _onboardingService.getOnboardingFields();
|
bool response = await _onboardingService.getOnboardingFields();
|
||||||
|
|
@ -144,4 +144,6 @@ class StartupViewModel extends ReactiveViewModel {
|
||||||
await replaceWithFailure();
|
await replaceWithFailure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import 'custom_linear_progress_indicator.dart';
|
||||||
class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
final int index;
|
final int index;
|
||||||
final bool last;
|
final bool last;
|
||||||
|
final bool first;
|
||||||
final LearnLesson lesson;
|
final LearnLesson lesson;
|
||||||
final GestureTapCallback? onLessonTap;
|
final GestureTapCallback? onLessonTap;
|
||||||
final GestureTapCallback? onPracticeTap;
|
final GestureTapCallback? onPracticeTap;
|
||||||
|
|
@ -26,6 +27,7 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
this.onLessonTap,
|
this.onLessonTap,
|
||||||
this.onPracticeTap,
|
this.onPracticeTap,
|
||||||
required this.last,
|
required this.last,
|
||||||
|
required this.first,
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.lesson});
|
required this.lesson});
|
||||||
|
|
||||||
|
|
@ -35,7 +37,13 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
|
|
||||||
Widget _buildContainerWrapper(LearnLessonViewModel viewModel) =>
|
Widget _buildContainerWrapper(LearnLessonViewModel viewModel) =>
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: !(lesson.access?.isAccessible ?? false) ? onPracticeTap : null,
|
onTap: viewModel.user?.subscriptionStatus?.toLowerCase() == 'active'
|
||||||
|
? !(lesson.access?.isAccessible ?? false)
|
||||||
|
? onPracticeTap
|
||||||
|
: null
|
||||||
|
: !first
|
||||||
|
? onPracticeTap
|
||||||
|
: null,
|
||||||
child: _buildContainer(viewModel),
|
child: _buildContainer(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -56,7 +64,7 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
Widget _buildTileStack(LearnLessonViewModel viewModel) => Stack(
|
Widget _buildTileStack(LearnLessonViewModel viewModel) => Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildExpansionTile(viewModel),
|
_buildExpansionTile(viewModel),
|
||||||
_buildContainerShaderState()
|
_buildContainerShaderState(viewModel)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -70,7 +78,6 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
shape: Border.all(color: kcTransparent),
|
shape: Border.all(color: kcTransparent),
|
||||||
expandedAlignment: Alignment.centerLeft,
|
expandedAlignment: Alignment.centerLeft,
|
||||||
leading: _buildLeadingWrapper(viewModel),
|
leading: _buildLeadingWrapper(viewModel),
|
||||||
enabled: (lesson.access?.isAccessible ?? false),
|
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||||
tilePadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
|
tilePadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
|
||||||
|
|
@ -85,6 +92,7 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
collapsedBackgroundColor: (lesson.access?.isCompleted ?? false)
|
collapsedBackgroundColor: (lesson.access?.isCompleted ?? false)
|
||||||
? kcGreen.withOpacity(0.1)
|
? kcGreen.withOpacity(0.1)
|
||||||
: kcPrimaryColor.withOpacity(0.1),
|
: kcPrimaryColor.withOpacity(0.1),
|
||||||
|
enabled: first ? true : (lesson.access?.isAccessible ?? false),
|
||||||
children: _buildExpansionTileChildren(viewModel),
|
children: _buildExpansionTileChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -203,7 +211,12 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContainerShaderState() => !(lesson.access?.isAccessible ?? false)
|
Widget _buildContainerShaderState(LearnLessonViewModel viewModel) =>
|
||||||
|
viewModel.user?.subscriptionStatus?.toLowerCase() == 'active'
|
||||||
|
? !(lesson.access?.isAccessible ?? false)
|
||||||
|
? _buildContainerShader()
|
||||||
|
: Container()
|
||||||
|
: !first
|
||||||
? _buildContainerShader()
|
? _buildContainerShader()
|
||||||
: Container();
|
: Container();
|
||||||
|
|
||||||
|
|
|
||||||
72
lib/ui/widgets/notification_card.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yimaru_app/models/in_app_notification.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
|
class NotificationCard extends StatelessWidget {
|
||||||
|
final InAppNotification notification;
|
||||||
|
|
||||||
|
const NotificationCard({super.key, required this.notification});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _buildContainer();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildContainer() => Container(
|
||||||
|
height: 100,
|
||||||
|
width: double.maxFinite,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15,vertical: 15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: (notification.isRead ?? false)
|
||||||
|
? kcGreen.withOpacity(0.05)
|
||||||
|
: kcPrimaryColor.withOpacity(0.05),
|
||||||
|
border: Border.all(
|
||||||
|
color: (notification.isRead ?? false)
|
||||||
|
? kcGreen.withOpacity(0.1)
|
||||||
|
: kcPrimaryColor.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _buildRow(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildRow() => Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _buildRowChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildRowChildren()=> [
|
||||||
|
_buildIcon(),
|
||||||
|
horizontalSpaceSmall,
|
||||||
|
_buildColumnWrapper()
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildIcon() => const Icon(
|
||||||
|
Icons.notifications_none,
|
||||||
|
size: 35,
|
||||||
|
color: kcMediumGrey,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildColumnWrapper()=> Expanded(child: _buildColumn());
|
||||||
|
|
||||||
|
Widget _buildColumn() => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _buildColumnChildren(),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _buildColumnChildren() =>
|
||||||
|
[ _buildTitle(), _buildSubtitle()];
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text(
|
||||||
|
notification.payload?.headline ?? '',
|
||||||
|
style: style16DG600,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSubtitle() => Text(
|
||||||
|
notification.payload?.message ?? '',
|
||||||
|
maxLines: 2,
|
||||||
|
style: style14MG400,
|
||||||
|
);
|
||||||
|
}
|
||||||
34
lib/ui/widgets/notification_icon.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:badges/badges.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
|
import '../common/app_colors.dart';
|
||||||
|
class NotificationIcon extends StatelessWidget {
|
||||||
|
final String count;
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
const NotificationIcon({super.key,this.onTap,required this.count});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _buildNotificationIconWrapper();
|
||||||
|
|
||||||
|
|
||||||
|
Widget _buildNotificationIconWrapper() => Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: _buildNotificationButton());
|
||||||
|
|
||||||
|
Widget _buildNotificationButton() =>
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: _buildNotificationBadge(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildNotificationBadge()=> badges.Badge(
|
||||||
|
badgeContent: Text(count,style: style12W600,),
|
||||||
|
|
||||||
|
child: _buildNotificationIcon(),
|
||||||
|
);
|
||||||
|
Widget _buildNotificationIcon() => const Icon(
|
||||||
|
Icons.notifications_none,
|
||||||
|
color: kcDarkGrey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,13 +7,16 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
import '../common/app_colors.dart';
|
import '../common/app_colors.dart';
|
||||||
import '../common/translations/locale_keys.g.dart';
|
import '../common/translations/locale_keys.g.dart';
|
||||||
|
import 'notification_icon.dart';
|
||||||
|
|
||||||
class ProfileAppBar extends StatelessWidget {
|
class ProfileAppBar extends StatelessWidget {
|
||||||
final String? name;
|
final String? name;
|
||||||
|
final String unreadCount;
|
||||||
final String? profileImage;
|
final String? profileImage;
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
const ProfileAppBar(
|
const ProfileAppBar(
|
||||||
{super.key, required this.name, required this.profileImage});
|
{super.key, this.onTap, required this.name,required this.unreadCount, required this.profileImage});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _buildStack();
|
Widget build(BuildContext context) => _buildStack();
|
||||||
|
|
@ -91,11 +94,8 @@ class ProfileAppBar extends StatelessWidget {
|
||||||
style: style14DG400,
|
style: style14DG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildNotificationIconWrapper() =>
|
|
||||||
Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon());
|
|
||||||
|
|
||||||
Widget _buildNotificationIcon() => const Icon(
|
Widget _buildNotificationIconWrapper() =>
|
||||||
Icons.notifications_none,
|
NotificationIcon(count: unreadCount,onTap:onTap ,)
|
||||||
color: kcDarkGrey,
|
;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.3.0"
|
||||||
|
badges:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: badges
|
||||||
|
sha256: cf1c88fb3777df69ccd630b80de5267f54efa4a39381b0404a7c03d56cb7c041
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: yimaru_app
|
name: yimaru_app
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.1.33+35
|
version: 0.1.41+43
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -16,6 +16,7 @@ dependencies:
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
async: ^2.13.1
|
async: ^2.13.1
|
||||||
pinput: ^6.0.1
|
pinput: ^6.0.1
|
||||||
|
badges: ^3.2.0
|
||||||
stacked: ^3.4.0
|
stacked: ^3.4.0
|
||||||
iconsax: ^0.0.8
|
iconsax: ^0.0.8
|
||||||
chewie: ^1.13.0
|
chewie: ^1.13.0
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||||
import 'package:yimaru_app/services/google_auth_service.dart';
|
import 'package:yimaru_app/services/google_auth_service.dart';
|
||||||
import 'package:yimaru_app/services/image_downloader_service.dart';
|
import 'package:yimaru_app/services/image_downloader_service.dart';
|
||||||
import 'package:yimaru_app/services/notification_service.dart';
|
|
||||||
import 'package:yimaru_app/services/smart_auth_service.dart';
|
import 'package:yimaru_app/services/smart_auth_service.dart';
|
||||||
import 'package:yimaru_app/services/course_service.dart';
|
import 'package:yimaru_app/services/course_service.dart';
|
||||||
import 'package:yimaru_app/services/audio_player_service.dart';
|
import 'package:yimaru_app/services/audio_player_service.dart';
|
||||||
|
|
@ -23,6 +22,8 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
|
||||||
import 'package:yimaru_app/services/learn_service.dart';
|
import 'package:yimaru_app/services/learn_service.dart';
|
||||||
import 'package:yimaru_app/services/localization_service.dart';
|
import 'package:yimaru_app/services/localization_service.dart';
|
||||||
import 'package:yimaru_app/services/onboarding_service.dart';
|
import 'package:yimaru_app/services/onboarding_service.dart';
|
||||||
|
import 'package:yimaru_app/services/in_app_notification_service.dart';
|
||||||
|
import 'package:yimaru_app/services/push_notification_service.dart';
|
||||||
// @stacked-import
|
// @stacked-import
|
||||||
|
|
||||||
@GenerateMocks(
|
@GenerateMocks(
|
||||||
|
|
@ -57,6 +58,10 @@ import 'package:yimaru_app/services/onboarding_service.dart';
|
||||||
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
|
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
|
||||||
MockSpec<LocalizationService>(onMissingStub: OnMissingStub.returnDefault),
|
MockSpec<LocalizationService>(onMissingStub: OnMissingStub.returnDefault),
|
||||||
MockSpec<OnboardingService>(onMissingStub: OnMissingStub.returnDefault),
|
MockSpec<OnboardingService>(onMissingStub: OnMissingStub.returnDefault),
|
||||||
|
MockSpec<InAppNotificationService>(
|
||||||
|
onMissingStub: OnMissingStub.returnDefault),
|
||||||
|
MockSpec<PushNotificationService>(
|
||||||
|
onMissingStub: OnMissingStub.returnDefault),
|
||||||
// @stacked-mock-spec
|
// @stacked-mock-spec
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -88,6 +93,8 @@ void registerServices() {
|
||||||
getAndRegisterLearnService();
|
getAndRegisterLearnService();
|
||||||
getAndRegisterLocalizationService();
|
getAndRegisterLocalizationService();
|
||||||
getAndRegisterOnboardingService();
|
getAndRegisterOnboardingService();
|
||||||
|
getAndRegisterInAppNotificationService();
|
||||||
|
getAndRegisterPushNotificationService();
|
||||||
// @stacked-mock-register
|
// @stacked-mock-register
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,13 +215,6 @@ MockImageDownloaderService getAndRegisterImageDownloaderService() {
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
MockNotificationService getAndRegisterNotificationService() {
|
|
||||||
_removeRegistrationIfExists<NotificationService>();
|
|
||||||
final service = MockNotificationService();
|
|
||||||
locator.registerSingleton<NotificationService>(service);
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
|
|
||||||
MockSmartAuthService getAndRegisterSmartAuthService() {
|
MockSmartAuthService getAndRegisterSmartAuthService() {
|
||||||
_removeRegistrationIfExists<SmartAuthService>();
|
_removeRegistrationIfExists<SmartAuthService>();
|
||||||
final service = MockSmartAuthService();
|
final service = MockSmartAuthService();
|
||||||
|
|
@ -291,6 +291,20 @@ MockOnboardingService getAndRegisterOnboardingService() {
|
||||||
locator.registerSingleton<OnboardingService>(service);
|
locator.registerSingleton<OnboardingService>(service);
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MockInAppNotificationService getAndRegisterInAppNotificationService() {
|
||||||
|
_removeRegistrationIfExists<InAppNotificationService>();
|
||||||
|
final service = MockInAppNotificationService();
|
||||||
|
locator.registerSingleton<InAppNotificationService>(service);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
MockPushNotificationService getAndRegisterPushNotificationService() {
|
||||||
|
_removeRegistrationIfExists<PushNotificationService>();
|
||||||
|
final service = MockPushNotificationService();
|
||||||
|
locator.registerSingleton<PushNotificationService>(service);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
// @stacked-mock-create
|
// @stacked-mock-create
|
||||||
|
|
||||||
void _removeRegistrationIfExists<T extends Object>() {
|
void _removeRegistrationIfExists<T extends Object>() {
|
||||||
|
|
|
||||||
11
test/services/in_app_notification_service_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
|
||||||
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('InAppNotificationServiceTest -', () {
|
||||||
|
setUp(() => registerServices());
|
||||||
|
tearDown(() => locator.reset());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import '../helpers/test_helpers.dart';
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('NotificationServiceTest -', () {
|
group('PushNotificationServiceTest -', () {
|
||||||
setUp(() => registerServices());
|
setUp(() => registerServices());
|
||||||
tearDown(() => locator.reset());
|
tearDown(() => locator.reset());
|
||||||
});
|
});
|
||||||
11
test/viewmodels/notification_viewmodel_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
|
||||||
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('NotificationViewModel Tests -', () {
|
||||||
|
setUp(() => registerServices());
|
||||||
|
tearDown(() => locator.reset());
|
||||||
|
});
|
||||||
|
}
|
||||||