Compare commits

...

76 Commits

Author SHA1 Message Date
misha
cbeb91a9f0 fix: analyzer issue 2025-11-28 01:00:56 +03:00
misha
9ca09e8137 Remove unwanted file 2025-11-28 00:49:38 +03:00
misha
8917f54e7b feat(tests): fixed feed tests failing and added tests for my_diets, my_plans and recipes 2025-11-28 00:46:25 +03:00
misha
626f74d20d Merge remote-tracking branch 'upstream/main' into issue-791 2025-11-27 23:50:39 +03:00
misha
c6c42b0525 feat(test): repostory, cubit and widget tests for feed 2025-11-25 11:13:37 +03:00
misha
b83d91e7f2 feat(tests): feed service tests 2025-11-20 09:54:12 +03:00
e5029f581d Merge pull request 'Updated app version' (#780) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#780
2025-11-19 13:57:51 +00:00
misha
c69fe7f0a6 Updated app version 2025-11-19 16:57:17 +03:00
fc0e9b1063 Merge pull request 'Update proj version' (#778) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#778
2025-11-19 13:48:20 +00:00
misha
453f119317 Merge remote-tracking branch 'upstream/main' 2025-11-19 16:47:43 +03:00
misha
6d0bee4da8 set build type to prod 2025-11-19 16:46:18 +03:00
misha
04d4be38d4 Update proj version 2025-11-19 16:45:08 +03:00
fa22cf5d36 Merge pull request 'feat(personalDiets)' (#760) from misha/eatvio_app:issue-756 into main
Reviewed-on: eatvio/eatvio_app#760
2025-11-19 09:58:07 +00:00
91e8348015 Merge pull request 'feat(registration): fixed registration on android devices, feat(leaders): fixed diet creation loader' (#776) from misha/eatvio_app:issue-738 into main
Reviewed-on: eatvio/eatvio_app#776
2025-11-19 08:17:36 +00:00
misha
1b948b99e3 Merge remote-tracking branch 'upstream/main' into issue-738 2025-11-19 11:16:45 +03:00
misha
b6eac3c46f feat(registration): fixed registration on android devices, feat(leaders): fixed diet creation loader 2025-11-19 11:07:16 +03:00
mso7nueh
39775cd7a8 Merge pull request 'updated app version' (#774) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#774
2025-11-17 10:22:48 +00:00
Username1548
36e2f12e3f updated app version 2025-11-17 12:01:31 +03:00
mso7nueh
66d2533add Merge pull request 'fix: Issue-752' (#769) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#769
2025-11-14 10:34:42 +00:00
mso7nueh
425f16bfc9 fix(consultation): Issue-752 2025-11-13 19:17:28 +03:00
mso7nueh
c5e30d8f00 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-11-13 18:59:48 +03:00
mso7nueh
d0935fbaae feat: upd flutter 2025-11-13 18:58:56 +03:00
dda0aa7440 Merge pull request 'fix: tests fix' (#768) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#768
2025-11-13 08:21:48 +00:00
misha
c81327b895 fix: tests fix 2025-11-13 11:21:02 +03:00
732d667ac1 Merge pull request 'improvements: added loaders' (#766) from misha/eatvio_app:issue-580 into main
Reviewed-on: eatvio/eatvio_app#766
2025-11-13 07:13:00 +00:00
misha
ba9f4a3584 improvements: added loaders 2025-11-13 10:12:11 +03:00
mso7nueh
f63d901a55 Merge pull request 'feat(ci/cd): add coverage test' (#764) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#764
2025-11-12 11:03:23 +00:00
mso7nueh
9148a4c668 feat(ci/cd): add coverage test 2025-11-12 13:34:55 +03:00
mso7nueh
a3f0aa3e67 Merge pull request 'feat(oauth): Issue-691+180' (#762) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#762
2025-11-11 14:40:09 +00:00
mso7nueh
8a390c2a9b Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-11-11 17:38:23 +03:00
mso7nueh
92b37e77b1 fix(socail,diets): Issue-180+691 2025-11-11 17:38:15 +03:00
101c1e15af Merge pull request 'feat(registration)' (#739) from misha/eatvio_app:issue-738 into main
Reviewed-on: eatvio/eatvio_app#739
2025-11-11 07:24:37 +00:00
mso7nueh
172c05ac69 merge upstream 2025-11-10 21:16:17 +00:00
misha
4eecc458f8 fix: formatting 2025-11-10 18:59:03 +03:00
misha
4b781adb79 feat(registration): added api integration for short registration 2025-11-10 18:55:55 +03:00
misha
dc400ef5b7 feat(personalDiets): fixed notification and chat banner 2025-11-10 13:50:06 +03:00
mso7nueh
d1160ba35f feat(oauth): Issue-691 2025-11-07 01:38:00 +03:00
mso7nueh
a46ac5bb4c Merge pull request 'feat(diary_test): Issue-755' (#759) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#759
2025-11-06 19:05:03 +00:00
misha
992cb38cdd feat(personalDiets): fixed ui bugs and added new notification type for personal diet 2025-11-06 08:00:30 +03:00
mso7nueh
2a06a5476a feat(diary_test): Issue-755 2025-11-06 00:38:08 +03:00
mso7nueh
f3853b58d9 Merge pull request 'fix: Issue-757' (#758) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#758
2025-11-05 21:17:36 +00:00
mso7nueh
bbc99ec9f4 fix(settings): Issue-757 2025-11-05 23:55:48 +03:00
ff946a5938 Merge pull request 'feat(personalDiets)' (#561) from misha/eatvio_app:personalDiets into main
Reviewed-on: eatvio/eatvio_app#561
2025-10-31 10:27:20 +00:00
misha
a72c40d8ee Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-31 13:26:05 +03:00
misha
6ed7faed15 feat(personalDiets): addde base url from app config for consultation widget 2025-10-31 13:23:13 +03:00
mso7nueh
12f51d49ea fix(profile): subs screen and blog post padding 2025-10-31 12:59:02 +03:00
misha
16fccaea8e Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-31 12:41:36 +03:00
Username1548
a723673d23 Merge branch 'main' of http://gitea.office.local:3000/eatvio/eatvio_app 2025-10-31 12:39:49 +03:00
Username1548
89f697f7a6 changed app version 2025-10-31 12:38:02 +03:00
d883c9e19a Merge pull request 'feat(marketing)' (#745) from misha/eatvio_app:issue-689 into main
Reviewed-on: eatvio/eatvio_app#745
2025-10-31 09:10:18 +00:00
misha
884830f232 Merge remote-tracking branch 'upstream/main' into issue-689 2025-10-31 12:08:02 +03:00
mso7nueh
7b297f954e Merge pull request 'Issue-746' (#747) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#747
2025-10-31 08:59:06 +00:00
mso7nueh
dbd92a65ea feat(md): Issue-746 2025-10-31 00:20:21 +03:00
mso7nueh
f3af743487 merge upstream 2025-10-30 19:27:36 +00:00
Username1548
c95f1988ae commit to create new build 2025-10-30 17:13:58 +03:00
mso7nueh
35a5e6d6be merge upstream 2025-10-30 09:13:28 +00:00
mso7nueh
977b7150fe feat(md): Issue-746 2025-10-29 17:27:05 +03:00
5b81014e72 Merge pull request 'feat(InfoByOrder)' (#750) from misha/eatvio_app:issue-681 into main
Reviewed-on: eatvio/eatvio_app#750
2025-10-29 01:39:05 +00:00
misha
563d84ca12 feat(InfoByOrder): added user info to InfoByOrderScreen author card 2025-10-29 04:38:04 +03:00
06cafb4257 Merge pull request 'feat(profile)' (#749) from misha/eatvio_app:issue-742 into main
Reviewed-on: eatvio/eatvio_app#749
2025-10-29 01:02:36 +00:00
mso7nueh
8799375880 feat(md): Issue-746 2025-10-27 17:07:20 +03:00
mso7nueh
fb5a0d52bc feat(feed): mdToHtml 2025-10-24 18:55:01 +03:00
misha
2e3172bc45 Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-24 13:21:03 +03:00
misha
41313f09af feat(marketing): added marketing bottom sheet 2025-10-23 17:25:12 +03:00
misha
e147f6e182 feat(registration): finished layout for new registration screen 2025-10-22 15:27:02 +03:00
misha
2440204294 feat(registration): added base layout for new registration page, and a bit of state managment 2025-10-20 17:55:56 +03:00
misha
46841fb502 feat(personalDiets): fixed PlanState widget in order to show personal diet state 2025-10-02 13:49:58 +03:00
misha
8e72350152 Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-02 12:25:40 +03:00
Username1548
8c2cd5eedd Merge remote-tracking branch 'upstream/main' into personalDiets 2025-09-05 11:28:51 +03:00
Username1548
996f2ecd70 Merge branch 'main' into personalDiets 2025-07-16 17:04:21 +07:00
Username1548
19787f459e feat(personalDiet): fixed wrong path for attach diet method 2025-07-16 17:03:46 +07:00
Username1548
7e64b24a8c added layout for suggestions page 2025-07-15 21:49:47 +07:00
Username1548
a07d085ba7 feat(personalDiets): preapared chats for consultations with personal diets 2025-07-14 20:24:40 +07:00
Username1548
5616bedd25 feat(personalDiets): diets suggestions 2025-07-14 13:44:44 +07:00
Username1548
0a3d9e577d Merge branch 'main' into personalDiets 2025-07-14 12:17:37 +07:00
Username1548
55e3abec99 feat(personal diets): added personal diet option to plans 2025-07-14 12:15:26 +07:00
131 changed files with 12772 additions and 30048 deletions

View File

@@ -140,7 +140,21 @@ jobs:
run: flutter analyze
- name: Test
run: flutter test
run: flutter test --coverage
- name: Test coverage
run: |
awk -F: '
/^LF:/ { total += $2 }
/^LH:/ { covered += $2 }
END {
if (total == 0) {
print "Coverage: 0%"
} else {
printf "Coverage: %.2f%%\n", (covered / total) * 100
}
}
' coverage/lcov.info
- name: Build apk
run: |

View File

@@ -19,15 +19,15 @@ def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property(
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {

3
assets/icons/check.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="12" height="9" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.16229 6.8625L10.5185 0.50625C10.6685 0.35625 10.8435 0.28125 11.0435 0.28125C11.2435 0.28125 11.4185 0.35625 11.5685 0.50625C11.7185 0.65625 11.7935 0.834375 11.7935 1.04063C11.7935 1.24688 11.7185 1.425 11.5685 1.575L4.68729 8.475C4.53729 8.625 4.36229 8.7 4.16229 8.7C3.96229 8.7 3.78729 8.625 3.63729 8.475L0.412289 5.25C0.262289 5.1 0.190414 4.92188 0.196664 4.71563C0.202914 4.50938 0.281039 4.33125 0.431039 4.18125C0.581039 4.03125 0.759164 3.95625 0.965414 3.95625C1.17166 3.95625 1.34979 4.03125 1.49979 4.18125L4.16229 6.8625Z" fill="#16A249"/>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View File

@@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.2" width="20" height="20" rx="6" fill="#16A249"/>
<g clip-path="url(#clip0_1823_10218)">
<path d="M8.16229 12.8625L14.5185 6.50625C14.6685 6.35625 14.8435 6.28125 15.0435 6.28125C15.2435 6.28125 15.4185 6.35625 15.5685 6.50625C15.7185 6.65625 15.7935 6.83438 15.7935 7.04063C15.7935 7.24688 15.7185 7.425 15.5685 7.575L8.68729 14.475C8.53729 14.625 8.36229 14.7 8.16229 14.7C7.96229 14.7 7.78729 14.625 7.63729 14.475L4.41229 11.25C4.26229 11.1 4.19041 10.9219 4.19666 10.7156C4.20291 10.5094 4.28104 10.3313 4.43104 10.1813C4.58104 10.0313 4.75916 9.95625 4.96541 9.95625C5.17166 9.95625 5.34979 10.0313 5.49979 10.1813L8.16229 12.8625Z" fill="#16A249"/>
</g>
<defs>
<clipPath id="clip0_1823_10218">
<rect width="12" height="9" fill="white" transform="translate(4 6)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1846_342)">
<path d="M5.33333 16C3.85185 16 2.59259 15.4815 1.55556 14.4444C0.518518 13.4074 0 12.1481 0 10.6667V5.33333C0 3.85185 0.518518 2.59259 1.55556 1.55556C2.59259 0.518518 3.85185 0 5.33333 0H10.6667C12.1481 0 13.4074 0.518518 14.4444 1.55556C15.4815 2.59259 16 3.85185 16 5.33333V10.6667C16 12.1481 15.4815 13.4074 14.4444 14.4444C13.4074 15.4815 12.1481 16 10.6667 16H5.33333ZM5.33333 14.2222H10.6667C11.6444 14.2222 12.4815 13.8741 13.1778 13.1778C13.8741 12.4815 14.2222 11.6444 14.2222 10.6667V5.33333C14.2222 4.35556 13.8741 3.51852 13.1778 2.82222C12.4815 2.12593 11.6444 1.77778 10.6667 1.77778H5.33333C4.35556 1.77778 3.51852 2.12593 2.82222 2.82222C2.12593 3.51852 1.77778 4.35556 1.77778 5.33333V10.6667C1.77778 11.6444 2.12593 12.4815 2.82222 13.1778C3.51852 13.8741 4.35556 14.2222 5.33333 14.2222Z" fill="#D22121"/>
<rect x="4.81836" y="10.9749" width="8" height="1" rx="0.5" transform="rotate(-45 4.81836 10.9749)" fill="#D22121"/>
<rect x="5.52539" y="5.31799" width="8" height="1" rx="0.5" transform="rotate(45 5.52539 5.31799)" fill="#D22121"/>
</g>
<defs>
<clipPath id="clip0_1846_342">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

9
assets/icons/logo_2.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="89" height="90" viewBox="0 0 89 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.0119 0C65.9947 0 83 9.05026 83 29.6764C83 50.2792 65.9947 67 45.0119 67C24.0053 67 7 50.2792 7 29.6764C7 9.05026 24.0053 0 45.0119 0Z" fill="#43A6F2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.8844 43.5162C39.8585 43.5162 34.9274 42.5623 30.3282 40.7209C25.7291 38.8573 21.5803 36.1284 18.1428 32.7119C16.5544 31.1589 16.6493 28.6741 18.3087 27.2098C19.9919 25.7234 22.6234 25.8122 24.2118 27.3651C26.867 30.0052 30.0675 32.1129 33.6235 33.5327C37.1796 34.9748 40.9964 35.7069 44.8844 35.7069C48.7486 35.7069 52.5655 34.9748 56.1215 33.5327C59.6776 32.1129 62.878 30.0052 65.5332 27.3651C67.1216 25.8122 69.7531 25.7234 71.4363 27.2098C73.0958 28.6741 73.1906 31.1589 71.6022 32.7119C68.1647 36.1284 64.016 38.8573 59.4168 40.7209C54.8176 42.5623 49.8866 43.5162 44.8844 43.5162Z" fill="white"/>
<path d="M9.69721 80.8957L3.51318 88.877C2.93793 89.6257 2.28049 90 1.54087 90C0.513623 90 0 89.3382 0 88.0147V77.3663C0 75.7888 0.56841 75 1.70523 75C2.84205 75 3.41046 75.7888 3.41046 77.3663V84.2246L9.71775 76.0829C10.2793 75.361 10.882 75 11.5257 75C12.5803 75 13.1077 75.7487 13.1077 77.246V87.6337C13.1077 89.2112 12.5393 90 11.4024 90C10.2656 90 9.69721 89.2112 9.69721 87.6337V80.8957Z" fill="#323F4B"/>
<path d="M23.0017 78.2888C21.769 78.2888 21.0019 78.2553 20.7006 78.1885C19.7829 78.0013 19.3241 77.5201 19.3241 76.7447C19.3241 75.7019 20.1322 75.1805 21.7484 75.1805H27.6859C29.3021 75.1805 30.1102 75.7019 30.1102 76.7447C30.1102 77.5201 29.6514 78.0013 28.7337 78.1885C28.4324 78.2553 27.6654 78.2888 26.4327 78.2888V87.6337C26.4327 89.2112 25.8642 90 24.7274 90C23.5769 90 23.0017 89.2112 23.0017 87.6337V78.2888Z" fill="#323F4B"/>
<path d="M38.6564 89.8195C37.7935 89.8195 37.1909 89.6791 36.8485 89.3984C36.506 89.1043 36.3348 88.5428 36.3348 87.7139V77.5267C36.3348 76.6578 36.506 76.0495 36.8485 75.7019C37.1909 75.3543 37.8072 75.1805 38.6975 75.1805H41.4916C42.8476 75.1805 43.9981 75.4545 44.9432 76.0027C46.0937 76.6578 46.6689 77.6003 46.6689 78.8302C46.6689 80.4211 46.08 81.4973 44.9021 82.0588V82.119C45.7787 82.3864 46.4087 82.7807 46.7922 83.3021C47.1757 83.8102 47.3675 84.512 47.3675 85.4078C47.3675 86.8917 46.8401 88.0214 45.7855 88.7968C44.8541 89.4786 43.6009 89.8195 42.0258 89.8195H38.6564ZM39.7453 83.5027V86.9519H41.4505C42.1764 86.9519 42.7175 86.8583 43.0736 86.6711C43.5393 86.4171 43.7721 85.9626 43.7721 85.3075C43.7721 84.5455 43.5461 84.0374 43.0941 83.7834C42.7654 83.5963 42.197 83.5027 41.3889 83.5027H39.7453ZM39.7453 78.0281V80.8957H41.2245C41.8957 80.8957 42.3682 80.8155 42.6421 80.6551C43.0256 80.4412 43.2174 80.0401 43.2174 79.4519C43.2174 78.5027 42.5531 78.0281 41.2245 78.0281H39.7453Z" fill="#323F4B"/>
<path d="M64.1493 80.8957L57.9653 88.877C57.39 89.6257 56.7326 90 55.993 90C54.9657 90 54.4521 89.3382 54.4521 88.0147V77.3663C54.4521 75.7888 55.0205 75 56.1573 75C57.2941 75 57.8626 75.7888 57.8626 77.3663V84.2246L64.1698 76.0829C64.7314 75.361 65.3341 75 65.9778 75C67.0324 75 67.5598 75.7487 67.5598 77.246V87.6337C67.5598 89.2112 66.9913 90 65.8545 90C64.7177 90 64.1493 89.2112 64.1493 87.6337V80.8957Z" fill="#323F4B"/>
<path d="M74.6391 82.5201C74.6391 80.3275 75.2691 78.5428 76.5292 77.1658C77.8304 75.7219 79.5904 75 81.8093 75C84.0418 75 85.8087 75.7152 87.1099 77.1457C88.37 78.5227 89 80.3142 89 82.5201C89 84.7126 88.37 86.4973 87.1099 87.8743C85.795 89.2914 84.0281 90 81.8093 90C79.5904 90 77.8304 89.2848 76.5292 87.8543C75.2691 86.4906 74.6391 84.7126 74.6391 82.5201ZM78.255 82.5602C78.255 83.7634 78.5358 84.7594 79.0973 85.5481C79.7274 86.4706 80.6314 86.9318 81.8093 86.9318C82.9872 86.9318 83.898 86.4773 84.5417 85.5682C85.1033 84.766 85.3841 83.7634 85.3841 82.5602C85.3841 81.3168 85.117 80.2941 84.5828 79.492C83.9391 78.5428 83.0146 78.0682 81.8093 78.0682C80.604 78.0682 79.6863 78.5428 79.0563 79.492C78.5221 80.2941 78.255 81.3168 78.255 82.5602Z" fill="#323F4B"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

3
assets/icons/sucess.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.33333 16C3.85185 16 2.59259 15.4815 1.55556 14.4444C0.518519 13.4074 0 12.1481 0 10.6667V5.33333C0 3.85185 0.518519 2.59259 1.55556 1.55556C2.59259 0.518519 3.85185 0 5.33333 0H10.6667C12.1481 0 13.4074 0.518519 14.4444 1.55556C15.4815 2.59259 16 3.85185 16 5.33333V10.6667C16 12.1481 15.4815 13.4074 14.4444 14.4444C13.4074 15.4815 12.1481 16 10.6667 16H5.33333ZM7.11111 9.06667L5.77778 7.73333C5.61481 7.57037 5.40741 7.48889 5.15556 7.48889C4.9037 7.48889 4.6963 7.57037 4.53333 7.73333C4.37037 7.8963 4.28889 8.1037 4.28889 8.35556C4.28889 8.60741 4.37037 8.81481 4.53333 8.97778L6.48889 10.9333C6.66667 11.1111 6.87407 11.2 7.11111 11.2C7.34815 11.2 7.55556 11.1111 7.73333 10.9333L11.8222 6.84444C11.9852 6.68148 12.0667 6.47407 12.0667 6.22222C12.0667 5.97037 11.9852 5.76296 11.8222 5.6C11.6593 5.43704 11.4519 5.35556 11.2 5.35556C10.9481 5.35556 10.7407 5.43704 10.5778 5.6L7.11111 9.06667ZM5.33333 14.2222H10.6667C11.6444 14.2222 12.4815 13.8741 13.1778 13.1778C13.8741 12.4815 14.2222 11.6444 14.2222 10.6667V5.33333C14.2222 4.35556 13.8741 3.51852 13.1778 2.82222C12.4815 2.12593 11.6444 1.77778 10.6667 1.77778H5.33333C4.35556 1.77778 3.51852 2.12593 2.82222 2.82222C2.12593 3.51852 1.77778 4.35556 1.77778 5.33333V10.6667C1.77778 11.6444 2.12593 12.4815 2.82222 13.1778C3.51852 13.8741 4.35556 14.2222 5.33333 14.2222Z" fill="#16A249"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/logo_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 81;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -406,7 +406,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.2.3;
MARKETING_VERSION = 1.3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -539,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 81;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -557,7 +557,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.2.3;
MARKETING_VERSION = 1.3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -578,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 81;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -596,7 +596,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.2.3;
MARKETING_VERSION = 1.3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>81</string>
<string>83</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -0,0 +1,502 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
class MarkdownEditor extends StatefulWidget {
const MarkdownEditor({
super.key,
this.initialText,
this.hintText,
this.hasError = false,
this.minLines = 4,
this.maxLines = 6,
this.heightPreview = 120,
this.onChanged,
this.onToolbarVisibilityChanged,
this.showInlineToolbar = true,
});
final String? initialText;
final String? hintText;
final bool hasError;
final int minLines;
final int maxLines;
final double heightPreview;
final void Function(String value)? onChanged;
final void Function(bool visible)? onToolbarVisibilityChanged;
final bool showInlineToolbar;
@override
State<MarkdownEditor> createState() => MarkdownEditorState();
}
class MarkdownEditorState extends State<MarkdownEditor> {
late final TextEditingController _controller;
final FocusNode _focusNode = FocusNode();
bool _isPreview = false;
bool _enablePanel = false;
late final KeyboardVisibilityController _keyboardVisibilityController;
late final StreamSubscription<bool> _keyboardSubscription;
bool get isToolbarVisible => _enablePanel || _isPreview;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialText ?? '');
_keyboardVisibilityController = KeyboardVisibilityController();
_keyboardSubscription = _keyboardVisibilityController.onChange.listen((visible) {
setState(() {
_enablePanel = visible;
});
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
});
}
@override
void dispose() {
_keyboardSubscription.cancel();
_focusNode.dispose();
_controller.dispose();
super.dispose();
}
bool _hasFormatting(String prefix, String suffix) {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
return selectedText.startsWith(prefix) && selectedText.endsWith(suffix);
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(cursorPosition - prefix.length, cursorPosition);
final afterCursor = text.substring(cursorPosition, cursorPosition + suffix.length);
return beforeCursor == prefix && afterCursor == suffix;
}
}
return false;
}
void _removeFormatting(String prefix, String suffix) {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);
final newText = text.replaceRange(selection.start, selection.end, unformattedText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + unformattedText.length);
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(0, cursorPosition - prefix.length);
final afterCursor = text.substring(cursorPosition + suffix.length);
final newText = beforeCursor + afterCursor;
_controller.text = newText;
_controller.selection = TextSelection.collapsed(offset: cursorPosition - prefix.length);
}
}
widget.onChanged?.call(_controller.text);
}
void _applyMarkdownFormat(String prefix, String suffix) {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final formattedText = '$prefix$selectedText$suffix';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + formattedText.length);
} else {
final cursorPosition = selection.baseOffset;
final newText = '${text.substring(0, cursorPosition)}$prefix$suffix${text.substring(cursorPosition)}';
_controller.text = newText;
_controller.selection = TextSelection.collapsed(offset: cursorPosition + prefix.length);
}
widget.onChanged?.call(_controller.text);
}
void _applyHeading() {
if (_hasFormatting('# ', '')) {
_removeFormatting('# ', '');
} else {
_applyMarkdownFormat('# ', '');
}
}
void _applyBold() {
if (_hasFormatting('**', '**')) {
_removeFormatting('**', '**');
} else {
_applyMarkdownFormat('**', '**');
}
}
void _applyItalic() {
if (_hasFormatting('*', '*')) {
_removeFormatting('*', '*');
} else {
_applyMarkdownFormat('*', '*');
}
}
void _applyNumberedList() {
if (_hasFormatting('1. ', '')) {
_removeFormatting('1. ', '');
} else {
_applyMarkdownFormat('1. ', '');
}
}
void _applyBulletedList() {
if (_hasFormatting('- ', '')) {
_removeFormatting('- ', '');
} else {
_applyMarkdownFormat('- ', '');
}
}
void _applyLink() {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final linkRegex = RegExp(r'^\[(.*?)\]\((.*?)\)$');
if (linkRegex.hasMatch(selectedText)) {
final match = linkRegex.firstMatch(selectedText);
final linkText = match?.group(1) ?? selectedText;
final newText = text.replaceRange(selection.start, selection.end, linkText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + linkText.length);
} else {
final formattedText = '[$selectedText](url)';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start + selectedText.length + 2, extentOffset: selection.start + selectedText.length + 5);
}
} else {
_applyMarkdownFormat('[', '](url)');
}
widget.onChanged?.call(_controller.text);
}
void _applyHorizontalRule() {
final cursorPosition = _controller.selection.baseOffset;
final text = _controller.text;
final newText = '${text.substring(0, cursorPosition)}\n---\n${text.substring(cursorPosition)}';
_controller.text = newText;
_controller.selection = TextSelection.collapsed(offset: cursorPosition + 5);
widget.onChanged?.call(_controller.text);
}
void _togglePreview() {
setState(() {
_isPreview = !_isPreview;
});
if (!_isPreview) {
Future.delayed(const Duration(milliseconds: 100), () {
_focusNode.requestFocus();
});
} else {
_focusNode.unfocus();
}
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
}
Widget _mdButton(IconData icon, VoidCallback onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Icon(icon),
),
),
);
}
Widget _mdTextButton(String text, VoidCallback onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Center(
child: Text(
text,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.black87),
),
),
),
),
);
}
Widget _buildMarkdownPreview(BuildContext context) {
return Container(
height: widget.heightPreview,
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE0E0E0)),
),
child: SingleChildScrollView(child: _parseMarkdown(_controller.text)),
);
}
Widget buildToolbar(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: MediaQuery.of(context).size.width - 76,
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
),
child: Row(
children: [
const SizedBox(width: 8),
_mdTextButton('H', _applyHeading),
const SizedBox(width: 8),
_mdButton(Icons.format_bold, _applyBold),
const SizedBox(width: 8),
_mdButton(Icons.format_italic, _applyItalic),
const SizedBox(width: 8),
_mdButton(Icons.format_list_numbered, _applyNumberedList),
const SizedBox(width: 8),
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
const SizedBox(width: 8),
_mdButton(Icons.link, _applyLink),
const SizedBox(width: 8),
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
const SizedBox(width: 8),
],
),
),
const SizedBox(width: 8),
Container(
width: 52,
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
),
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
),
],
);
}
Widget _parseMarkdown(String text) {
if (text.isEmpty) {
return Text(widget.hintText ?? '');
}
final lines = text.split('\n');
final widgets = <Widget>[];
for (int i = 0; i < lines.length; i++) {
final line = lines[i];
if (line.trim().isEmpty) {
widgets.add(const SizedBox(height: 8));
continue;
}
if (line.startsWith('# ')) {
widgets.add(
Text(
line.substring(2),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('## ')) {
widgets.add(
Text(
line.substring(3),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('### ')) {
widgets.add(
Text(
line.substring(4),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('- ') || line.startsWith('* ')) {
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('• ', style: TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(2))),
],
),
),
);
} else if (RegExp(r'^\d+\. ').hasMatch(line)) {
final match = RegExp(r'^(\d+)\. ').firstMatch(line);
final number = match?.group(1) ?? '1';
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$number. ', style: const TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(number.length + 2))),
],
),
),
);
} else if (line.trim() == '---') {
widgets.add(const Divider(color: Colors.grey, thickness: 1));
} else {
widgets.add(_parseInlineMarkdown(line));
}
if (i < lines.length - 1) {
widgets.add(const SizedBox(height: 4));
}
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: widgets);
}
Widget _parseInlineMarkdown(String text) {
final spans = <TextSpan>[];
final regex = RegExp(r'(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))');
int lastEnd = 0;
for (final match in regex.allMatches(text)) {
if (match.start > lastEnd) {
spans.add(
TextSpan(
text: text.substring(lastEnd, match.start),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
final matchedText = match.group(0)!;
if (matchedText.startsWith('**') && matchedText.endsWith('**')) {
spans.add(
TextSpan(
text: matchedText.substring(2, matchedText.length - 2),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (matchedText.startsWith('*') && matchedText.endsWith('*')) {
spans.add(
TextSpan(
text: matchedText.substring(1, matchedText.length - 1),
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.black),
),
);
} else if (matchedText.startsWith('[') && matchedText.contains('](')) {
final linkMatch = RegExp(r'\[(.*?)\]\((.*?)\)').firstMatch(matchedText);
if (linkMatch != null) {
final linkText = linkMatch.group(1)!;
spans.add(
TextSpan(
text: linkText,
style: const TextStyle(fontSize: 16, color: Colors.blue, decoration: TextDecoration.underline),
),
);
}
}
lastEnd = match.end;
}
if (lastEnd < text.length) {
spans.add(
TextSpan(
text: text.substring(lastEnd),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
return RichText(text: TextSpan(children: spans));
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isPreview)
_buildMarkdownPreview(context)
else
CustomTextFormField(
controller: _controller,
focusNode: _focusNode,
maxLines: widget.maxLines,
minLines: widget.minLines,
hintText: widget.hintText,
hasError: widget.hasError,
onChanged: (value) => widget.onChanged?.call(value),
),
const SizedBox(height: 8),
if (widget.showInlineToolbar && (isToolbarVisible || !_isPreview))
Row(
children: [
Expanded(
child: Container(
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
],
),
child: Row(
children: [
const SizedBox(width: 8),
_mdTextButton('H', _applyHeading),
const SizedBox(width: 8),
_mdButton(Icons.format_bold, _applyBold),
const SizedBox(width: 8),
_mdButton(Icons.format_italic, _applyItalic),
const SizedBox(width: 8),
_mdButton(Icons.format_list_numbered, _applyNumberedList),
const SizedBox(width: 8),
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
const SizedBox(width: 8),
_mdButton(Icons.link, _applyLink),
const SizedBox(width: 8),
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
const SizedBox(width: 8),
],
),
),
),
const SizedBox(width: 8),
Container(
width: 52,
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
],
),
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
),
],
),
],
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -40,6 +41,7 @@ class AppConfig {
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
];
/// Делегаты локализации

View File

@@ -0,0 +1,29 @@
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MarketingCubit extends Cubit<MarketingCubitState> {
final Prefs prefs;
MarketingCubit(this.prefs) : super(const MarketingCubitState());
Future<bool> checkRegistrationDate() async {
try {
final milliseconds = prefs.getInt(PrefsKeys.registrationDate);
final marketingBottomSheetHasBeenShown = prefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown);
if (milliseconds == null || marketingBottomSheetHasBeenShown == true) {
return false;
}
final date = DateTime.fromMillisecondsSinceEpoch(milliseconds);
final today = DateTime.now();
final daysCount = today.difference(date).inDays;
final needToShowMarketingBottomSheet = daysCount >= 14;
if (needToShowMarketingBottomSheet) {
await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true);
}
return needToShowMarketingBottomSheet;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,3 @@
class MarketingCubitState {
const MarketingCubitState();
}

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
@@ -38,6 +39,7 @@ import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cub
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
@@ -46,6 +48,7 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
@@ -74,10 +77,11 @@ class CubitRegistrations {
static void _registerFactoryCubits() {
// Auth related cubits
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl()));
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
// Feed related cubits
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
@@ -130,7 +134,7 @@ class CubitRegistrations {
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl()));
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()));
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
@@ -143,5 +147,7 @@ class CubitRegistrations {
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
}
}

View File

@@ -23,12 +23,14 @@ import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.da
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:get_it/get_it.dart';
@@ -68,6 +70,8 @@ class RepositoryRegistrations {
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl()));
// Content repositories
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));

View File

@@ -25,12 +25,14 @@ import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
import 'package:eatvio_app/features/plans/services/plan_service.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:get_it/get_it.dart';
@@ -76,6 +78,8 @@ class ServiceRegistrations {
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl()));
// Content services
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));

View File

@@ -0,0 +1,11 @@
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
mixin RegistrationDateCacheMixin {
late final Prefs prefs;
Future<void> cacheRegistrationDate(DateTime date) async {
await prefs.setInt(PrefsKeys.registrationDate, date.millisecondsSinceEpoch);
}
}

View File

@@ -1 +1 @@
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed }
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart';
import 'package:go_router/go_router.dart';
/// Маршруты для аутентификации
@@ -26,6 +27,7 @@ class AuthRoutes {
},
routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
GoRoute(
path: RouteConstants.forgetPasswordScreen,
name: 'forgetPasswordScreen',

View File

@@ -29,6 +29,7 @@ import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
@@ -156,7 +157,7 @@ class OtherRoutes {
},
),
GoRoute(
path: RouteConstants.subsScreen,
path: '${RouteConstants.subsScreen}/:slug',
name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
),
@@ -194,7 +195,11 @@ class OtherRoutes {
),
// Other routes
GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String)),
GoRoute(
path: RouteConstants.glossaryScreen,
name: 'glossaryScreen',
builder: (context, state) => GlossaryScreen(passKey: state.extra as String),
),
GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
GoRoute(
path: RouteConstants.notificationsSettingsScreen,
@@ -215,7 +220,8 @@ class OtherRoutes {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
final customerSlug = data['customerSlug'] as String?;
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment, customerSlug: customerSlug);
},
),
GoRoute(
@@ -238,6 +244,11 @@ class OtherRoutes {
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
},
),
GoRoute(
path: PersonalDietSuggestionScreen.path,
name: PersonalDietSuggestionScreen.name,
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
),
];
}
}

View File

@@ -3,6 +3,7 @@ class RouteConstants {
// Auth routes
static const String authScreen = '/auth-screen';
static const String registrationScreen = 'registration-screen';
static const String registrationScreenV2 = 'registration-screen-v2';
static const String forgetPasswordScreen = 'forget-password-screen';
static const String scanQRCodeScreen = 'scan-qr-code-screen';
static const String verifyScreen = '/verify-screen';

View File

@@ -10,161 +10,76 @@ class CustomTextStyles {
static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
// Title text style
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
fontWeight: FontWeight.w600,
);
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(fontWeight: FontWeight.w600);
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900,
);
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900);
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900,
fontSize: 13,
);
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900, fontSize: 13);
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.whiteA700,
);
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.whiteA700);
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black900,
fontSize: 12,
);
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black900, fontSize: 12);
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.blueGray900,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.blueGray900, fontSize: 12);
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.gray600,
);
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.gray600);
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
color: theme.colorScheme.primary,
fontSize: 12,
);
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(color: theme.colorScheme.primary, fontSize: 12);
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
color: appTheme.black900,
fontSize: 12,
);
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(color: appTheme.black900, fontSize: 12);
// Label text style
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.gray60003,
);
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(color: appTheme.gray60003);
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700,
);
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(color: appTheme.whiteA700);
// Title text style
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
);
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(fontSize: 16);
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
fontSize: 17,
fontWeight: FontWeight.w900,
);
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(fontSize: 17, fontWeight: FontWeight.w900);
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.black900,
);
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(color: appTheme.black900);
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600,
);
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600);
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.whiteA700,
fontWeight: FontWeight.w600,
);
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(color: appTheme.whiteA700, fontWeight: FontWeight.w600);
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.blue800,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.blue800, fontWeight: FontWeight.w800);
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
);
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.black);
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
fontWeight: FontWeight.w700,
);
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w700);
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w800);
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
fontSize: 15,
fontWeight: FontWeight.w700,
);
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(fontSize: 15, fontWeight: FontWeight.w700);
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001,
);
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001);
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001, fontWeight: FontWeight.w800);
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.lightGreen300,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(color: appTheme.lightGreen300, fontWeight: FontWeight.w800);
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.red200,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(color: appTheme.red200, fontWeight: FontWeight.w800);
static TextStyle get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallSecondaryContainer =>
theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer, fontWeight: FontWeight.w800);
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer,
);
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer);
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
color: theme.colorScheme.onPrimary,
);
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(color: theme.colorScheme.onPrimary);
}
extension on TextStyle {
@@ -178,75 +93,32 @@ extension on TextStyle {
}
abstract class TextStyles {
static const headingXLSemibold = TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const headingXLSemibold = TextStyle(fontSize: 20, fontWeight: FontWeight.w600, height: 1.255);
static const headingFS40FW600 = TextStyle(
fontSize: 40,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const headingFS40FW600 = TextStyle(fontSize: 40, fontWeight: FontWeight.w600, height: 1.255);
static const textLSemibold = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const textLSemibold = TextStyle(fontSize: 18, fontWeight: FontWeight.w600, height: 1.255);
static const textLMedium = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textMSemibold = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const textLMedium = TextStyle(fontSize: 18, fontWeight: FontWeight.w500, height: 1.255);
static const textMMedium = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1,
);
static const textMSemibold = TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1.255);
static const textMMediumAuto = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textMMedium = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1);
static const textSSemibold = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const textMMediumAuto = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.255);
static const textSMedium = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textSSemibold = TextStyle(fontSize: 14, fontWeight: FontWeight.w600, height: 1.255);
static const textFS13FW400 = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.255,
);
static const textSMedium = TextStyle(fontSize: 14, fontWeight: FontWeight.w500, height: 1.255);
static const textXSSemibold = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textFS13FW400 = TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1.255);
static const textXSSemibold = TextStyle(fontSize: 12, fontWeight: FontWeight.w500, height: 1.255);
static const textFS10FW500 = TextStyle(fontSize: 10, fontWeight: FontWeight.w500, height: 1.255);
static const textFS24FW700 = TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.255);
static const textFS22FW600 = TextStyle(fontSize: 22, fontWeight: FontWeight.w600, height: 1.255);
static const textFS10FW500 = TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
height: 1.255,
);
}

View File

@@ -7,23 +7,17 @@ class ThemeHelper {
// The current app theme
final _appTheme = 'primary';
// A map of custom color themes supported by the app
final Map<String, PrimaryColors> _supportedCustomColor = {
'primary': PrimaryColors(),
};
// A map of custom color themes supported by the app
final Map<String, PrimaryColors> _supportedCustomColor = {'primary': PrimaryColors()};
// A map of color schemes supported by the app
final Map<String, ColorScheme> _supportedColorScheme = {
'primary': ColorSchemes.primaryColorScheme,
};
// A map of color schemes supported by the app
final Map<String, ColorScheme> _supportedColorScheme = {'primary': ColorSchemes.primaryColorScheme};
/// Returns the primary colors for the current theme.
PrimaryColors _getThemeColors() {
//throw exception to notify given theme is not found or not generated by the generator
if (!_supportedCustomColor.containsKey(_appTheme)) {
throw Exception(
"$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
);
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
}
//return theme from map
@@ -34,9 +28,7 @@ class ThemeHelper {
ThemeData _getThemeData() {
//throw exception to notify given theme is not found or not generated by the generator
if (!_supportedColorScheme.containsKey(_appTheme)) {
throw Exception(
"$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
);
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
}
//return theme from map
@@ -60,17 +52,9 @@ class ThemeHelper {
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
backgroundColor: Colors.transparent,
side: BorderSide(
color: colorScheme.primary,
width: 1,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
visualDensity: const VisualDensity(
vertical: -4,
horizontal: -4,
),
side: BorderSide(color: colorScheme.primary, width: 1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
padding: EdgeInsets.zero,
),
),
@@ -101,7 +85,7 @@ class PrimaryColors {
// Blue
Color get blue400 => const Color(0XFF43A6F2);
Color get blueA400 => const Color(0XFF2F80ED);
Color get blueA400 => const Color(0XFF2F80ED);
// BlueGray
Color get blueGray400 => const Color(0XFF7B8794);
@@ -141,12 +125,17 @@ class PrimaryColors {
Color get gray60003 => const Color(0XFF707070);
Color get gray60005 => const Color(0xFF6C7C93);
// Green
Color get greenA700 => const Color(0XFF019A3E);
Color get greenA70001 => const Color(0XFF00BE13);
Color get green => const Color(0XFF0BB450);
Color get darkGreen => const Color(0XFF16A249);
Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
// LightGreen
Color get lightGreen300 => const Color(0XFFB2DF8A);
@@ -180,6 +169,8 @@ class PrimaryColors {
Color get lightGray0 => const Color(0xFFF6F6F6);
Color get darkGrey => const Color(0xFF5B656F);
Color get lightGray00 => const Color(0xFFE9E9EA);
Color get dottedBorder => const Color(0xFFE6E6E6);
@@ -206,6 +197,8 @@ class PrimaryColors {
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
Color get grey7B => const Color(0xFF7B7B7B);
Color get greyD9 => const Color(0xFFD9D9D9);
}
PrimaryColors get appTheme => ThemeHelper().themeColor();

View File

@@ -3,16 +3,28 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class CustomDropDownButton<T> extends StatelessWidget {
const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
const CustomDropDownButton({
super.key,
required this.items,
required this.onChanged,
this.value,
this.hint,
this.selectedItemBuilder,
this.hasError = false,
this.isExpanded = false,
});
final T? value;
final List<DropdownMenuItem<T>> items;
final void Function(T?)? onChanged;
final Widget? hint;
final bool hasError;
final List<Widget> Function(BuildContext)? selectedItemBuilder;
final bool isExpanded;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField(
hint: hint,
isExpanded: isExpanded,
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
borderRadius: BorderRadius.circular(20),
dropdownColor: appTheme.lightGray0,
@@ -35,6 +47,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
initialValue: value,
items: items,
onChanged: onChanged,
selectedItemBuilder: selectedItemBuilder,
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:share_plus/share_plus.dart';
void showMarketingBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
child: const MarketingBottomSheet(),
),
);
}
class MarketingBottomSheet extends StatelessWidget {
const MarketingBottomSheet({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsetsGeometry.all(4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
height: 180,
width: double.infinity,
decoration: const BoxDecoration(
image: DecorationImage(image: AssetImage('assets/images/logo_bg.png'), fit: BoxFit.cover),
),
child: Center(child: SvgPicture.asset('assets/icons/logo_2.svg', height: 90)),
),
),
const SizedBox(height: 16),
Text(s.congratulations, style: TextStyles.textFS24FW700.copyWith(color: appTheme.blueA400)),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Container(
decoration: BoxDecoration(color: appTheme.lightGray0, borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
children: [
const Spacer(),
Container(
decoration: BoxDecoration(shape: BoxShape.circle, color: appTheme.blueA400),
padding: const EdgeInsets.all(8),
child: Text('14', style: TextStyles.textFS24FW700.copyWith(color: appTheme.whiteA700)),
),
const SizedBox(width: 12),
RichText(
text: TextSpan(
text: s.youWithUs,
style: TextStyles.textSMedium.copyWith(color: appTheme.gray60005),
children: [
TextSpan(
text: s.fourteenDays,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
],
),
),
const Spacer(),
],
),
),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
s.weAreLookingFor,
textAlign: TextAlign.center,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
),
Text(
s.letsInvite,
textAlign: TextAlign.center,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
child: CustomOutlinedButton(
text: s.invite,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
leftIcon: Padding(
padding: const EdgeInsets.only(right: 12),
child: Transform.rotate(
angle: 3.14,
child: SvgPicture.asset('assets/icons/share.svg', width: 14, height: 14, colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn)),
),
),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
SharePlus.instance.share(ShareParams(uri: Uri.parse('https://promo.eatvio.ru/')));
},
),
),
Text(
s.eatvioIsAHelthyHabbit,
textAlign: TextAlign.center,
style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400),
),
const SizedBox(height: 16),
],
),
);
}
}

View File

@@ -9,9 +9,10 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
class OptionWidget extends StatelessWidget {
const OptionWidget({super.key, required this.option, this.disableDuration = false});
const OptionWidget({super.key, required this.option, this.disableDuration = false, required this.duration});
final PacketOptions? option;
final bool disableDuration;
final int duration;
@override
Widget build(BuildContext context) {
if (option == null) {
@@ -34,8 +35,12 @@ class OptionWidget extends StatelessWidget {
),
const SizedBox(height: 12),
if (!disableDuration) ...[
const _OptionDurationWidget(duration: 7),
const SizedBox(height: 16),
_OptionDurationWidget(duration: duration),
SizedBox(height: option?.hasPersonalDiet ?? false ? 8 : 12),
],
if ((option?.hasPersonalDiet ?? false) && !disableDuration) ...[
const _OptionHasPersonalDietWidget(),
const SizedBox(height: 12),
],
const CustomDivider(),
const SizedBox(height: 16),
@@ -52,7 +57,6 @@ class OptionWidget extends StatelessWidget {
}
}
class _OptionDurationWidget extends StatelessWidget {
const _OptionDurationWidget({required this.duration});
final int duration;
@@ -92,3 +96,43 @@ class _OptionDurationWidget extends StatelessWidget {
);
}
}
class _OptionHasPersonalDietWidget extends StatelessWidget {
const _OptionHasPersonalDietWidget();
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: appTheme.transparentGreen),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: appTheme.green,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(100),
bottomRight: Radius.circular(100),
),
),
),
),
const SizedBox(
width: 12,
),
Text(
s.personalDietCreating,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(
width: 12,
),
],
),
);
}
}

View File

@@ -1,14 +1,18 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class OAuthCubit extends Cubit<OAuthState> {
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final AuthRepository authRepository;
OAuthCubit({required this.authRepository}) : super(const OAuthState());
OAuthCubit({required this.authRepository, required Prefs prefs}) : super(const OAuthState()) {
this.prefs = prefs;
}
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
try {
@@ -63,6 +67,7 @@ class OAuthCubit extends Cubit<OAuthState> {
clientId: clientId,
);
TokenStorage.saveTokenInStorage(token);
cacheRegistrationDate(DateTime.now());
emit(OAuthState.loaded(token: token));
} on DioException catch (e) {
emit(OAuthState.error(errorMessage: e.toString()));

View File

@@ -337,7 +337,7 @@ class _AuthScreenState extends State<AuthScreen> {
children: [
const Text('Нет аккаунта? '),
GestureDetector(
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
onTap: () => GoRouter.of(context).pushNamed('registrationScreenV2'),
child: Text(
'Зарегистрироваться',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
@@ -411,7 +411,7 @@ class _AuthScreenState extends State<AuthScreen> {
_isLoading = false;
});
},
label: state is AuthStateLoading
label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
@@ -426,8 +426,8 @@ class _AuthScreenState extends State<AuthScreen> {
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
backgroundColor: state is AuthStateLoading || _isLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading || _isLoading
? null
: () async {
await showDialog(
@@ -487,7 +487,7 @@ class _AuthScreenState extends State<AuthScreen> {
},
);
},
label: state is AuthStateLoading
label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),

View File

@@ -11,7 +11,9 @@ class BlogPostCubit extends Cubit<BlogPostState> {
Future<void> getBlogPost(int id) async {
try {
emit(state.copyWith(isLoading: true));
final postModel = await feedRepository.getBlogPost(id: id);
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) {
log(e.toString(), stackTrace: st);

View File

@@ -18,6 +18,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class BlogPostScreen extends StatefulWidget {
final int id;
@@ -231,10 +232,35 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Html(
data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'),
style: {"*": Style(color: Colors.black87)},
data: md.markdownToHtml(blogPostState.blogPostModel!.description.replaceAll('\n', '<br>')),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
const SizedBox(height: 8),

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
@@ -25,10 +26,7 @@ import 'package:permission_handler/permission_handler.dart';
class BlogPostCreateScreen extends StatefulWidget {
final FeedBlogPostModel? feedBlogPostModel;
const BlogPostCreateScreen({
super.key,
this.feedBlogPostModel,
});
const BlogPostCreateScreen({super.key, this.feedBlogPostModel});
@override
State<BlogPostCreateScreen> createState() => _BlogPostCreateScreenState();
@@ -43,6 +41,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
final descriptionController = TextEditingController();
final videoUrlController = TextEditingController();
bool isLoading = false;
bool enableMdPanel = false;
bool isPreviewMode = false;
final FocusNode descriptionFocusNode = FocusNode();
@override
void initState() {
@@ -51,6 +52,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
setState(() {
shrinkImage = visible;
enableMdPanel = visible;
});
});
if (widget.feedBlogPostModel != null) {
@@ -63,9 +65,326 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
@override
void dispose() {
keyboardSubscription.cancel();
descriptionFocusNode.dispose();
super.dispose();
}
bool _hasFormatting(String prefix, String suffix) {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
return selectedText.startsWith(prefix) && selectedText.endsWith(suffix);
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(cursorPosition - prefix.length, cursorPosition);
final afterCursor = text.substring(cursorPosition, cursorPosition + suffix.length);
return beforeCursor == prefix && afterCursor == suffix;
}
}
return false;
}
void _removeFormatting(String prefix, String suffix) {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);
final newText = text.replaceRange(selection.start, selection.end, unformattedText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + unformattedText.length);
descriptionController.selection = newSelection;
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(0, cursorPosition - prefix.length);
final afterCursor = text.substring(cursorPosition + suffix.length);
final newText = beforeCursor + afterCursor;
descriptionController.text = newText;
final newCursorPosition = cursorPosition - prefix.length;
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
}
}
}
void _applyMarkdownFormat(String prefix, String suffix) {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final formattedText = '$prefix$selectedText$suffix';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + formattedText.length);
descriptionController.selection = newSelection;
} else {
final cursorPosition = selection.baseOffset;
final newText = '${text.substring(0, cursorPosition)}$prefix$suffix${text.substring(cursorPosition)}';
descriptionController.text = newText;
final newCursorPosition = cursorPosition + prefix.length;
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
}
}
void _applyHeading() {
if (_hasFormatting('# ', '')) {
_removeFormatting('# ', '');
} else {
_applyMarkdownFormat('# ', '');
}
}
void _applyBold() {
if (_hasFormatting('**', '**')) {
_removeFormatting('**', '**');
} else {
_applyMarkdownFormat('**', '**');
}
}
void _applyItalic() {
if (_hasFormatting('*', '*')) {
_removeFormatting('*', '*');
} else {
_applyMarkdownFormat('*', '*');
}
}
void _applyNumberedList() {
if (_hasFormatting('1. ', '')) {
_removeFormatting('1. ', '');
} else {
_applyMarkdownFormat('1. ', '');
}
}
void _applyBulletedList() {
if (_hasFormatting('- ', '')) {
_removeFormatting('- ', '');
} else {
_applyMarkdownFormat('- ', '');
}
}
void _applyLink() {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final linkRegex = RegExp(r'^\[(.*?)\]\((.*?)\)$');
if (linkRegex.hasMatch(selectedText)) {
final match = linkRegex.firstMatch(selectedText);
final linkText = match?.group(1) ?? selectedText;
final newText = text.replaceRange(selection.start, selection.end, linkText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + linkText.length);
descriptionController.selection = newSelection;
} else {
final formattedText = '[$selectedText](url)';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start + selectedText.length + 2, extentOffset: selection.start + selectedText.length + 5);
descriptionController.selection = newSelection;
}
} else {
_applyMarkdownFormat('[', '](url)');
}
}
void _applyHorizontalRule() {
final cursorPosition = descriptionController.selection.baseOffset;
final text = descriptionController.text;
final newText = '${text.substring(0, cursorPosition)}\n---\n${text.substring(cursorPosition)}';
descriptionController.text = newText;
final newCursorPosition = cursorPosition + 5;
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
}
void _togglePreview() {
setState(() {
isPreviewMode = !isPreviewMode;
});
if (!isPreviewMode) {
Future.delayed(const Duration(milliseconds: 100), () {
descriptionFocusNode.requestFocus();
});
} else {
descriptionFocusNode.unfocus();
}
}
Widget _buildMarkdownPreview() {
return Container(
height: 120,
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE0E0E0)),
),
child: SingleChildScrollView(child: _parseMarkdown(descriptionController.text)),
);
}
Widget _parseMarkdown(String text) {
if (text.isEmpty) {
return const Text(
'Описание',
style: TextStyle(fontSize: 16, color: Color(0xFF323F4B), fontWeight: FontWeight.w500),
);
}
final lines = text.split('\n');
final widgets = <Widget>[];
for (int i = 0; i < lines.length; i++) {
final line = lines[i];
if (line.trim().isEmpty) {
widgets.add(const SizedBox(height: 8));
continue;
}
if (line.startsWith('# ')) {
widgets.add(
Text(
line.substring(2),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('## ')) {
widgets.add(
Text(
line.substring(3),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('### ')) {
widgets.add(
Text(
line.substring(4),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('- ') || line.startsWith('* ')) {
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('• ', style: TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(2))),
],
),
),
);
} else if (RegExp(r'^\d+\. ').hasMatch(line)) {
final match = RegExp(r'^(\d+)\. ').firstMatch(line);
final number = match?.group(1) ?? '1';
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$number. ', style: const TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(number.length + 2))),
],
),
),
);
} else if (line.trim() == '---') {
widgets.add(const Divider(color: Colors.grey, thickness: 1));
} else {
widgets.add(_parseInlineMarkdown(line));
}
if (i < lines.length - 1) {
widgets.add(const SizedBox(height: 4));
}
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: widgets);
}
Widget _parseInlineMarkdown(String text) {
final spans = <TextSpan>[];
final regex = RegExp(r'(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))');
int lastEnd = 0;
for (final match in regex.allMatches(text)) {
if (match.start > lastEnd) {
spans.add(
TextSpan(
text: text.substring(lastEnd, match.start),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
final matchedText = match.group(0)!;
if (matchedText.startsWith('**') && matchedText.endsWith('**')) {
spans.add(
TextSpan(
text: matchedText.substring(2, matchedText.length - 2),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (matchedText.startsWith('*') && matchedText.endsWith('*')) {
spans.add(
TextSpan(
text: matchedText.substring(1, matchedText.length - 1),
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.black),
),
);
} else if (matchedText.startsWith('[') && matchedText.contains('](')) {
final linkMatch = RegExp(r'\[(.*?)\]\((.*?)\)').firstMatch(matchedText);
if (linkMatch != null) {
final linkText = linkMatch.group(1)!;
spans.add(
TextSpan(
text: linkText,
style: const TextStyle(fontSize: 16, color: Colors.blue, decoration: TextDecoration.underline),
),
);
}
}
lastEnd = match.end;
}
if (lastEnd < text.length) {
spans.add(
TextSpan(
text: text.substring(lastEnd),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
return RichText(text: TextSpan(children: spans));
}
@override
Widget build(BuildContext context) {
return BlocProvider<BlogPostCreateCubit>(
@@ -103,13 +422,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
@@ -127,97 +440,102 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
);
},
);
}
Widget _mdButton(IconData icon, Function() onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Icon(icon),
),
),
);
}
Widget _mdTextButton(String text, Function() onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Center(
child: Text(
text,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.black87),
),
),
),
),
);
}
Scaffold _buildBlogPostCreate(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: SizedBox(
width: MediaQuery.of(context).size.width - 32,
child: FloatingActionButton.extended(
key: AppKeys.blogPostCreateScreenCreateButton,
heroTag: 'createBlogPost',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: appTheme.blueA400,
label: isLoading
? const CircularProgressIndicator()
: const Text(
'Создать',
style: TextStyle(
color: Colors.white,
floatingActionButton: enableMdPanel || isPreviewMode
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!isPreviewMode) ...[
Container(
width: MediaQuery.of(context).size.width - 76,
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: const Color(0x00000000).withValues(alpha: 0.2), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
],
),
child: Row(
children: [
const SizedBox(width: 8),
_mdTextButton('H', _applyHeading),
const SizedBox(width: 8),
_mdButton(Icons.format_bold, _applyBold),
const SizedBox(width: 8),
_mdButton(Icons.format_italic, _applyItalic),
const SizedBox(width: 8),
_mdButton(Icons.format_list_numbered, _applyNumberedList),
const SizedBox(width: 8),
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
const SizedBox(width: 8),
_mdButton(Icons.link, _applyLink),
const SizedBox(width: 8),
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
const SizedBox(width: 8),
],
),
),
const SizedBox(width: 8),
],
Container(
width: 52,
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0x00000000).withValues(alpha: 0.2), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
),
child: Row(children: [_mdButton(isPreviewMode ? Icons.edit : Icons.visibility, _togglePreview)]),
),
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
if (widget.feedBlogPostModel != null) {
final result = await blogPostCubit.updateBlogPost(
id: widget.feedBlogPostModel!.id,
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
oldImageUrl: widget.feedBlogPostModel!.image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!context.mounted) return;
showErrorDialog(context, title: S.current.could_not_update_blog_post);
} else {
if (!context.mounted) return;
context.pop();
}
} else {
final result = await blogPostCubit.createBlogPost(
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!context.mounted) return;
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
} else {
if (!context.mounted) return;
context.pop();
}
}
},
),
),
],
)
: null,
appBar: AppBar(),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
@@ -226,19 +544,19 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
const SizedBox(height: 16),
const Text('Заголовок'),
const SizedBox(height: 8),
CustomTextFormField(
key: AppKeys.blogPostCreateScreenTitleTextField,
controller: titleController,
),
CustomTextFormField(key: AppKeys.blogPostCreateScreenTitleTextField, controller: titleController),
const SizedBox(height: 16),
const Text('Описание'),
const SizedBox(height: 8),
CustomTextFormField(
key: AppKeys.blogPostCreateScreenContentTextField,
controller: descriptionController,
maxLines: 6,
minLines: 4,
),
isPreviewMode
? _buildMarkdownPreview()
: CustomTextFormField(
key: AppKeys.blogPostCreateScreenContentTextField,
controller: descriptionController,
focusNode: descriptionFocusNode,
maxLines: 6,
minLines: 4,
),
const SizedBox(height: 16),
if (videoUrlController.text.isEmpty) const Text('Изображение поста'),
if (videoUrlController.text.isEmpty) const SizedBox(height: 8),
@@ -247,17 +565,75 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
if (image == null && widget.feedBlogPostModel?.image == null) const Text('Ссылка на видео'),
if (image == null && widget.feedBlogPostModel?.image == null) const SizedBox(height: 8),
if (image == null && widget.feedBlogPostModel?.image == null)
CustomTextFormField(
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
controller: videoUrlController,
),
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
const SizedBox(height: 80),
_createBlogPostButton(),
],
),
),
);
}
Widget _createBlogPostButton() {
return InkWell(
onTap: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
if (widget.feedBlogPostModel != null) {
final result = await blogPostCubit.updateBlogPost(
id: widget.feedBlogPostModel!.id,
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
oldImageUrl: widget.feedBlogPostModel!.image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!mounted) return;
showErrorDialog(context, title: S.current.could_not_update_blog_post);
} else {
if (!mounted) return;
context.pop();
}
} else {
final result = await blogPostCubit.createBlogPost(
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!mounted) return;
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
} else {
if (!mounted) return;
context.pop();
}
}
},
borderRadius: BorderRadius.circular(16),
child: isLoading
? const Center(child: Loader())
: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
child: const Center(
child: Text('Создать', style: TextStyle(color: Colors.white)),
),
),
);
}
GestureDetector _buildAddPhoto() {
return GestureDetector(
onTap: () {
@@ -270,22 +646,14 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
height: shrinkImage ? 52 : 128,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 150),
decoration: AppDecoration.fillGray.copyWith(
borderRadius: BorderRadius.circular(16),
),
decoration: AppDecoration.fillGray.copyWith(borderRadius: BorderRadius.circular(16)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.photo_library,
color: appTheme.blueGray300,
),
Icon(Icons.photo_library, color: appTheme.blueGray300),
const SizedBox(width: 8),
Text(
"Добавить фото",
selectionColor: appTheme.blueA400,
),
Text("Добавить фото", selectionColor: appTheme.blueA400),
],
),
),
@@ -294,13 +662,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image(
image: FileImage(
File(
image!.path,
),
),
),
child: Image(image: FileImage(File(image!.path))),
),
),
if (widget.feedBlogPostModel?.image != null && image == null)
@@ -308,9 +670,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image(
image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.image}'),
),
child: Image(image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.image}')),
),
),
],

View File

@@ -114,13 +114,21 @@ class _ChatScreenState extends State<ChatScreen> {
ChosenPlanStateEnum.statePendingPayment,
ChosenPlanStateEnum.stateInCoordination,
].contains(chosenPlan?.chosenPlanState);
PacketOptions? chosenOption;
if (chosenPlan?.optionType == 'options1') {
chosenOption = chosenPlan?.plan.packet1Options;
} else if (chosenPlan?.optionType == 'options2') {
chosenOption = chosenPlan?.plan.packet2Options;
} else if (chosenPlan?.optionType == 'options3') {
chosenOption = chosenPlan?.plan.packet3Options;
}
final showAboutOrderButton =
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
BannerType? bannerType;
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
bannerType = null;
} else if (amIConsultant && !state.hasCard) {
} else if (amIConsultant && !state.hasCard && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
bannerType = BannerType.card;
} else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
bannerType = BannerType.date;
@@ -131,16 +139,10 @@ class _ChatScreenState extends State<ChatScreen> {
bannerType = BannerType.collectCharge;
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
bannerType = BannerType.finishPlan;
} else if (chosenOption?.hasPersonalDiet == true && amIConsultant && chosenPlan.chosenPlanState != ChosenPlanStateEnum.stateDone) {
bannerType = BannerType.setPersonalDiet;
}
PacketOptions? chosenOption;
if (chosenPlan?.optionType == 'options1') {
chosenOption = chosenPlan?.plan.packet1Options;
} else if (chosenPlan?.optionType == 'options2') {
chosenOption = chosenPlan?.plan.packet2Options;
} else if (chosenPlan?.optionType == 'options3') {
chosenOption = chosenPlan?.plan.packet3Options;
}
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
@@ -149,10 +151,7 @@ class _ChatScreenState extends State<ChatScreen> {
centerTitle: false,
title: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: state.chatRoom.users.receiver.slug,
);
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
},
child: Row(
children: [
@@ -320,6 +319,7 @@ class _ChatScreenState extends State<ChatScreen> {
chosenPlanId: chosenPlan?.id,
planName: chosenPlan?.plan.name,
planPrice: chosenOption?.price.toDouble(),
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
),
),
],
@@ -447,7 +447,7 @@ class _ChatScreenState extends State<ChatScreen> {
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
if (isBlocked) {
@@ -462,7 +462,7 @@ class _ChatScreenState extends State<ChatScreen> {
return;
}
}
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
chatRoomCubit.sendMessage(
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
@@ -497,6 +497,12 @@ class _ChatScreenState extends State<ChatScreen> {
left: 16,
child: AboutOrderButton(
onTap: () async {
if (amIConsultant) {
GoRouter.of(
context,
).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id, 'customerSlug': state.chatRoom.users.receiver.slug});
return;
}
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
},
),

View File

@@ -9,17 +9,28 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.dart';
class ChosePlanDateWidget extends StatefulWidget {
const ChosePlanDateWidget({super.key, required this.chatCubit});
const ChosePlanDateWidget({super.key, required this.chatCubit, required this.hasPersonalDiet});
final ChatRoomCubit chatCubit;
final bool hasPersonalDiet;
@override
State<ChosePlanDateWidget> createState() => _ChosePlanDateWidget();
}
class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
final startDate = [DateTime.now()];
late final List<DateTime> startDate;
bool visible = false;
@override
void initState() {
super.initState();
if (widget.hasPersonalDiet) {
startDate = [DateTime.now().add(const Duration(days: 2))];
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -27,64 +38,129 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
if (widget.chatCubit.state is ChatRoomStateLoaded) {
planName = (widget.chatCubit.state as ChatRoomStateLoaded).chatRoom.chosenPlan?.plan.name;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
const BottomSheetHandlerBar(),
const SizedBox(height: 12),
Text(
planName ?? '',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
return GestureDetector(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
const BottomSheetHandlerBar(),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
planName ?? '',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
),
if (widget.hasPersonalDiet) ...[
const SizedBox(
width: 8,
),
PortalTarget(
closeDuration: const Duration(milliseconds: 20),
anchor: const Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,
target: Alignment.bottomLeft,
),
portalFollower: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: visible ? appTheme.whiteA700 : Colors.transparent,
boxShadow: visible
? [
BoxShadow(
color: appTheme.black900.withValues(alpha: 0.15),
blurRadius: 4,
offset: const Offset(0, 4),
),
]
: null,
),
child: AnimatedDefaultTextStyle(
style: TextStyles.textMMediumAuto.copyWith(
color: visible ? appTheme.black : Colors.transparent,
),
duration: const Duration(milliseconds: 300),
child: Text(
s.chosenPlanStartDateDescr,
),
),
),
),
child: GestureDetector(
onTap: () {
setState(() {
visible = !visible;
});
},
child: SvgPicture.asset(
'assets/icons/question_mark.svg',
width: 22,
height: 22,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
)
]
],
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Text(
s.chose_plan_start_date_2,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Text(
s.chose_plan_start_date_2,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
),
),
const SizedBox(height: 12),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: CustomDivider(),
),
CalendarWidget(
startDate: startDate,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
child: CustomOutlinedButton(
onPressed: () async {
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
if (result) {
showSuccessToast('Дата установлена');
if (!context.mounted) return;
Navigator.pop(context);
} else {
showErrorToast('Не удалось установить дату');
if (!context.mounted) return;
Navigator.pop(context);
}
},
height: 60,
text: s.confirm,
buttonTextStyle: TextStyles.textLSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
const SizedBox(height: 12),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: CustomDivider(),
),
CalendarWidget(
startDate: startDate,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
child: CustomOutlinedButton(
onPressed: () async {
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
if (result) {
showSuccessToast('Дата установлена');
if (!context.mounted) return;
Navigator.pop(context);
} else {
showErrorToast('Не удалось установить дату');
if (!context.mounted) return;
Navigator.pop(context);
}
},
height: 60,
text: s.confirm,
buttonTextStyle: TextStyles.textLSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
),
],
],
),
);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'
import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -17,12 +18,12 @@ import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class NotificationBanner extends StatefulWidget {
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId});
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId, this.hasPersonalDiet = false});
final BannerType bannerType;
final double? planPrice;
final String? planName;
final int? chosenPlanId;
final bool hasPersonalDiet;
@override
State<NotificationBanner> createState() => _NotificationBannerState();
}
@@ -68,7 +69,8 @@ class _NotificationBannerState extends State<NotificationBanner> {
BannerType.date => [
SizedBox(
width: size.width * 0.6,
child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
child: Text(widget.hasPersonalDiet == true ? s.choose_plan_start_date_with_personal_diet : s.choose_plan_start_date,
style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
@@ -78,6 +80,7 @@ class _NotificationBannerState extends State<NotificationBanner> {
context: context,
builder: (context) => ChosePlanDateWidget(
chatCubit: cubit,
hasPersonalDiet: widget.hasPersonalDiet,
),
);
},
@@ -155,6 +158,20 @@ class _NotificationBannerState extends State<NotificationBanner> {
),
const SizedBox(height: 8),
],
BannerType.setPersonalDiet => [
SizedBox(
width: size.width * 0.6,
child: Text(s.createPersonalDiet, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(MyDietsScreen.name);
},
child: Text(s.goToMyDiets, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
},
),
),
@@ -242,4 +259,4 @@ class _StarIcon extends StatelessWidget {
}
}
enum BannerType { date, card, payment, collectCharge, finishPlan }
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet }

View File

@@ -22,6 +22,7 @@ abstract class ConsultationModel with _$ConsultationModel {
@JsonKey(name: 'end_date', fromJson: parseDate) required DateTime endDate,
@JsonKey(name: 'consultant_id') required int consultantId,
required List<DiaryModel> diaries,
@JsonKey(name: 'personal_diet_id') required int? personalDietId,
}) = _ConsultationModel;
factory ConsultationModel.fromJson(Map<String, dynamic> json) => _$ConsultationModelFromJson(json);

View File

@@ -63,25 +63,28 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.consultationsModel.userName,
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
widget.consultationsModel.planTitle,
style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
],
GestureDetector(
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: widget.consultationsModel.userSlug),
child: Row(
children: [
CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.consultationsModel.userName,
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
widget.consultationsModel.planTitle,
style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
],
),
),
const SizedBox(height: 12),
GestureDetector(

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -54,6 +55,8 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
@override
void initState() {
@@ -76,9 +79,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(
title: state.dietData.name ?? 'Диета',
),
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
);
});
} else if (state.drafted) {
@@ -87,15 +88,15 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
context.pop();
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(
title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках',
),
child: SuccessPublishedDietOrPlanBottomSheet(title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках'),
);
});
}
return Scaffold(
backgroundColor: appTheme.gray100,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
appBar: AppBar(
centerTitle: true,
title: Text(
@@ -124,34 +125,23 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
context.read<CreateDietCubit>().setName(value);
},
),
if (state.dietNameError != null) ...[
const SizedBox(
height: 4,
),
ValidationErrorWidget(error: state.dietNameError ?? ''),
],
if (state.dietNameError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietNameError ?? '')],
const SizedBox(height: 12),
SizedBox(
height: 132,
child: CustomTextFormField(
hasError: state.dietDescriptionError != null,
controller: descriptionController,
textAlignVertical: TextAlignVertical.top,
maxLines: null,
minLines: null,
expands: true,
hintText: s.enter_diet_description,
onChanged: (value) {
context.read<CreateDietCubit>().setDetails(value);
},
),
MarkdownEditor(
key: _mdKey,
initialText: descriptionController.text,
hintText: s.enter_diet_description,
hasError: state.dietDescriptionError != null,
minLines: 6,
maxLines: 8,
showInlineToolbar: false,
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
onChanged: (value) {
descriptionController.text = value;
context.read<CreateDietCubit>().setDetails(value);
},
),
if (state.dietDescriptionError != null) ...[
const SizedBox(
height: 4,
),
ValidationErrorWidget(error: state.dietDescriptionError ?? ''),
],
if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')],
const SizedBox(height: 12),
const DietDurationDropDownWidget(),
const SizedBox(height: 12),
@@ -171,9 +161,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
child: Center(
child: Text(
'${s.diet_plan} (${state.dietData.duration ?? 0} ${getDayForm(state.dietData.duration ?? 0, s)})',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
),
@@ -196,9 +184,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
onPressed: () async {
context.read<CreateDietCubit>().setDietState(DietState.draft);
if (widget.diet != null) {
final result = await context.read<CreateDietCubit>().updateDiet(
widget.diet!.id,
);
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');

View File

@@ -302,7 +302,10 @@ class CreateDietCubit extends Cubit<CreateDietState> {
Future<String?> createDiet() async {
emit(state.copyWith(isDietPublishing: true));
if (!_isFromValid()) return 'validation';
if (!_isFromValid()) {
emit(state.copyWith(isDietPublishing: false));
return 'validation';
}
try {
File? file;
if (state.dietData.imageUrl != null) {

View File

@@ -1,13 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/string_extentions.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
import 'package:eatvio_app/core/widgets/cpfc_container.dart';
@@ -36,20 +31,11 @@ class DietPreviewScreen extends StatefulWidget {
class _DietPreviewScreenState extends State<DietPreviewScreen> {
final ScrollController scrollController = ScrollController();
final textEditingController = TextEditingController();
bool isMoreOptionsVisible = false;
late final Uint8List imageData;
@override
void initState() {
super.initState();
imageData = base64.decode(widget.diet.image);
scrollController.addListener(() {
if (isMoreOptionsVisible) {
setState(() {
isMoreOptionsVisible = false;
});
}
});
}
@override
@@ -58,86 +44,6 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
return Scaffold(
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: BookmarkButton(
isFavorite: widget.diet.isFavorite,
onTap: () {},
width: 52,
height: 52,
size: 24,
shape: BoxShape.circle,
color: appTheme.gray50,
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: SizedBox(
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
hintText: s.comment.capitalize(),
hintStyle: TextStyles.textMMedium.copyWith(color: appTheme.lightGray600),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
height: 32,
width: 32,
child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
),
),
],
),
],
),
),
),
appBar: AppBar(
centerTitle: true,
title: Text(
@@ -148,15 +54,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
userSlug: widget.diet.author.authorSlug,
),
const SizedBox(width: 16),
],
actions: const [],
),
body: SingleChildScrollView(
controller: scrollController,

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -28,16 +29,21 @@ class CreatePlanScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<CreatePlanCubit>(),
child: const CreatePlanScreenContent(),
);
return BlocProvider(create: (context) => sl<CreatePlanCubit>(), child: const CreatePlanScreenContent());
}
}
class CreatePlanScreenContent extends StatelessWidget {
class CreatePlanScreenContent extends StatefulWidget {
const CreatePlanScreenContent({super.key});
@override
State<CreatePlanScreenContent> createState() => _CreatePlanScreenContentState();
}
class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -51,15 +57,14 @@ class CreatePlanScreenContent extends StatelessWidget {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(
title: state.planData.name ?? 'План',
isPlan: true,
),
child: SuccessPublishedDietOrPlanBottomSheet(title: state.planData.name ?? 'План', isPlan: true),
);
});
}
return Scaffold(
backgroundColor: appTheme.gray100,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
appBar: AppBar(
centerTitle: true,
title: Text(
@@ -104,10 +109,7 @@ class CreatePlanScreenContent extends StatelessWidget {
},
maxLength: 150,
),
if (state.validationState.titleHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
if (state.validationState.titleHasError) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.atLeast3Symbols)],
],
);
},
@@ -140,20 +142,17 @@ class CreatePlanScreenContent extends StatelessWidget {
builder: (context, state) {
return Column(
children: [
SizedBox(
height: 132,
child: CustomTextFormField(
hasError: state.validationState.descriptionHasError,
textAlignVertical: TextAlignVertical.top,
maxLength: 1000,
maxLines: null,
minLines: null,
expands: true,
hintText: s.plan_desc_example,
onChanged: (value) {
context.read<CreatePlanCubit>().setDetails(value);
},
),
MarkdownEditor(
key: _mdKey,
hintText: s.plan_desc_example,
hasError: state.validationState.descriptionHasError,
minLines: 6,
maxLines: 8,
showInlineToolbar: false,
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
onChanged: (value) {
context.read<CreatePlanCubit>().setDetails(value);
},
),
if (state.validationState.descriptionHasError) ...[
const SizedBox(height: 4),
@@ -253,10 +252,7 @@ class CreatePlanScreenContent extends StatelessWidget {
}
return const Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: Align(
alignment: Alignment.bottomCenter,
child: SelectExactOptionWidget(),
),
child: Align(alignment: Alignment.bottomCenter, child: SelectExactOptionWidget()),
);
},
),

View File

@@ -67,6 +67,13 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
validateSelectedOptionItems();
}
void togglePersonalizedDietOptionState() {
final options = [...state.planData.options];
options[state.selectedOptionIndex ?? 0] =
options[state.selectedOptionIndex ?? 0].copyWith(hasPersonalDiet: !options[state.selectedOptionIndex ?? 0].hasPersonalDiet);
emit(state.copyWith(planData: state.planData.copyWith(options: options)));
}
void setDuration(int index) {
emit(
state.copyWith(
@@ -418,18 +425,21 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[0].description,
'items': Map.fromIterable(state.planData.options[0].items),
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
},
'packet2_options': {
'short': state.planData.options[1].short,
'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[1].description,
'items': Map.fromIterable(state.planData.options[1].items),
'has_personal_diet': state.planData.options[1].hasPersonalDiet ? '1' : '0',
},
'packet3_options': {
'short': state.planData.options[2].short,
'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[2].description,
'items': Map.fromIterable(state.planData.options[2].items),
'has_personal_diet': state.planData.options[2].hasPersonalDiet ? '1' : '0',
},
});
} else {
@@ -440,6 +450,7 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[0].description,
'items': Map.fromIterable(state.planData.options[0].items),
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
},
});
}

View File

@@ -90,7 +90,7 @@ class _OptionWidgetState extends State<OptionWidget> {
2 => thirdOptionCustomerPriceController,
_ => secondOptionCustomerPriceController,
};
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
@@ -255,33 +255,31 @@ class _OptionWidgetState extends State<OptionWidget> {
),
const SizedBox(height: 16),
const CustomDivider(),
// const SizedBox(height: 16),
// Row(
// children: [
// Checkbox(
// value: isChecked,
// activeColor: appTheme.gray,
// checkColor: appTheme.gray,
// fillColor: const WidgetStatePropertyAll(Colors.transparent),
// side: WidgetStateBorderSide.resolveWith(
// (Set<WidgetState> states) {
// return BorderSide(width: 1, color: appTheme.gray);
// },
// ),
// onChanged: (bool? value) {
// setState(() {
// isChecked = value ?? false;
// });
// },
// ),
// Expanded(
// child: Text(
// s.option_personal_diet_desc,
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
// ),
// ),
// ],
// ),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: state.planData.options[selectedOptionIndex].hasPersonalDiet,
activeColor: appTheme.gray,
checkColor: appTheme.gray,
fillColor: const WidgetStatePropertyAll(Colors.transparent),
side: WidgetStateBorderSide.resolveWith(
(Set<WidgetState> states) {
return BorderSide(width: 1, color: appTheme.gray);
},
),
onChanged: (bool? value) {
context.read<CreatePlanCubit>().togglePersonalizedDietOptionState();
},
),
Expanded(
child: Text(
s.option_personal_diet_desc,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
],
),
],
),
);

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.da
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart';
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
@@ -30,7 +31,9 @@ class RecipeCreateScreen extends StatefulWidget {
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
late final CreateRecipeCubit createRecipeCubit;
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
bool _isRecipeBeingPublished = false;
@override
void initState() {
super.initState();
@@ -54,6 +57,8 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
} else {
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
appBar: AppBar(
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
centerTitle: true,
@@ -65,51 +70,66 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
child: Column(
spacing: 16,
children: [
InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit),
InfoAboutRecipeWidget(
createRecipeCubit: createRecipeCubit,
markdownEditorKey: _mdKey,
onMarkdownToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
),
const IngredientsWidget(),
StepsWidget(createRecipeCubit: createRecipeCubit),
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
builder: (context, state) {
return Builder(
builder: (context) {
return InkWell(
key: AppKeys.recipeCreateScreenPublishButton,
onTap: () async {
if (context.read<CreateRecipeCubit>().validateAll()) {
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
widget.recipe == null ? false : true,
widget.recipe?.slug,
return _isRecipeBeingPublished
? const Center(child: Loader())
: InkWell(
key: AppKeys.recipeCreateScreenPublishButton,
onTap: () async {
setState(() {
_isRecipeBeingPublished = true;
});
if (context.read<CreateRecipeCubit>().validateAll()) {
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
widget.recipe == null ? false : true,
widget.recipe?.slug,
);
setState(() {
_isRecipeBeingPublished = false;
});
if (isSuccess) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
);
});
} else {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
}
}
setState(() {
_isRecipeBeingPublished = false;
});
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: Text(
widget.recipe != null ? 'Обновить' : 'Опубликовать',
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
),
),
),
);
if (isSuccess) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
);
});
} else {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
}
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: Text(
widget.recipe != null ? 'Обновить' : 'Опубликовать',
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
),
),
),
);
},
);
},

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -16,8 +17,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class InfoAboutRecipeWidget extends StatefulWidget {
final CreateRecipeCubit createRecipeCubit;
final GlobalKey<MarkdownEditorState>? markdownEditorKey;
final void Function(bool visible)? onMarkdownToolbarVisibilityChanged;
const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit});
const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit, this.markdownEditorKey, this.onMarkdownToolbarVisibilityChanged});
@override
State<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
@@ -105,19 +108,24 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
builder: (context, state) {
return Column(
children: [
CustomTextFormField(
KeyedSubtree(
key: AppKeys.recipeCreateScreenDescriptionField,
child: MarkdownEditor(
key: widget.markdownEditorKey,
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
minLines: 5,
initialValue: state.entity.description,
maxLines: 5,
initialText: state.entity.description,
hasError: state.descriptionValidationError != null,
showInlineToolbar: false,
onToolbarVisibilityChanged: widget.onMarkdownToolbarVisibilityChanged,
onChanged: (value) {
widget.createRecipeCubit.replaceDescription(value);
if (state.descriptionValidationError != null) {
widget.createRecipeCubit.validateDescription();
}
},
),
),
if (state.descriptionValidationError != null) ...[
const SizedBox(height: 4),

View File

@@ -44,16 +44,24 @@ class UserDiaryScreen extends StatelessWidget {
}
}
class DiaryScreenContent extends StatelessWidget {
class DiaryScreenContent extends StatefulWidget {
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
final int? diaryId;
final int? currentPlanDay;
@override
State<DiaryScreenContent> createState() => _DiaryScreenContentState();
}
class _DiaryScreenContentState extends State<DiaryScreenContent> {
bool _isDiaryFinishing = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return MultiBlocProvider(
providers: [
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: diaryId)),
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId)),
BlocProvider(create: (context) => sl<ConsultantCommentsCubit>()),
],
child: BlocBuilder<DiaryCubit, DiaryState>(
@@ -63,13 +71,13 @@ class DiaryScreenContent extends StatelessWidget {
}
if (state is DiaryStateError) {
return Scaffold(
body: DataErrorWidget(onReload: () => context.read<DiaryCubit>().fetchDiaryV2(needLoading: false, diaryId: diaryId)),
body: DataErrorWidget(onReload: () => context.read<DiaryCubit>().fetchDiaryV2(needLoading: false, diaryId: widget.diaryId)),
);
}
return Scaffold(
backgroundColor: Colors.grey.shade200,
appBar: diaryId == null
appBar: widget.diaryId == null
? AppBar(toolbarHeight: 0)
: AppBar(
title: Text(
@@ -83,7 +91,7 @@ class DiaryScreenContent extends StatelessWidget {
physics: const BouncingScrollPhysics(),
child: Column(
children: [
if (diaryId == null) const CalendarWidget(),
if (widget.diaryId == null) const CalendarWidget(),
if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()],
if (state is DiaryStateLoaded)
WakeupAndRetireWidget(
@@ -116,7 +124,23 @@ class DiaryScreenContent extends StatelessWidget {
if (state is DiaryStateLoaded && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented)
Padding(
padding: const EdgeInsets.only(top: 12),
child: FinishDayButtonWidget(diaryDay: state.diary),
child: _isDiaryFinishing
? const Center(child: Loader())
: FinishDayButtonWidget(
onPressed: () async {
setState(() {
_isDiaryFinishing = true;
});
await context.read<DiaryCubit>().finishDiaryDay(
diaryId: state.diary.id,
wakeUp: state.diary.wakeup.toString(),
retire: state.diary.retire.toString(),
);
setState(() {
_isDiaryFinishing = false;
});
},
),
),
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[
const SizedBox(height: 16),

View File

@@ -1,13 +1,10 @@
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class FinishDayButtonWidget extends StatefulWidget {
final DiaryDayEntity diaryDay;
final Future<void> Function() onPressed;
const FinishDayButtonWidget({super.key, required this.diaryDay});
const FinishDayButtonWidget({super.key, required this.onPressed});
@override
State<FinishDayButtonWidget> createState() => _FinishDayButtonWidgetState();
@@ -17,11 +14,7 @@ class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => context.read<DiaryCubit>().finishDiaryDay(
diaryId: widget.diaryDay.id,
wakeUp: widget.diaryDay.wakeup.toString(),
retire: widget.diaryDay.retire.toString(),
),
onTap: widget.onPressed,
borderRadius: BorderRadius.circular(20),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 8),

View File

@@ -4,22 +4,24 @@ import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class AuthorCard extends StatelessWidget {
const AuthorCard({super.key, required this.author, required this.title, this.backgroundColor, this.titleStyle});
const AuthorCard({super.key, required this.author, required this.title, this.backgroundColor, this.titleStyle, this.customerProfile});
final Author author;
final String title;
final Color? backgroundColor;
final TextStyle? titleStyle;
final ProfileModel? customerProfile;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('profileScreen', extra: author.authorSlug);
GoRouter.of(context).pushNamed('profileScreen', extra: customerProfile != null ? customerProfile!.slug : author.authorSlug);
},
child: Container(
width: MediaQuery.sizeOf(context).width,
@@ -27,7 +29,7 @@ class AuthorCard extends StatelessWidget {
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 24),
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 24, right: 16),
child: Column(
children: [
ListTile(
@@ -39,24 +41,51 @@ class AuthorCard extends StatelessWidget {
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(
author.authorProfileIcon.contains('https') ? author.authorProfileIcon : sl<AppConfig>().baseUrl + author.authorProfileIcon,
customerProfile != null
? customerProfile!.profilePhotoUrl.contains('https')
? customerProfile!.profilePhotoUrl
: sl<AppConfig>().baseUrl + customerProfile!.profilePhotoUrl
: author.authorProfileIcon.contains('https')
? author.authorProfileIcon
: sl<AppConfig>().baseUrl + author.authorProfileIcon,
),
fit: BoxFit.cover,
),
),
),
subtitle: Text(author.authorName, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
subtitle: Text(
customerProfile != null ? customerProfile!.name : author.authorName,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
title: Text(title, style: titleStyle ?? TextStyles.textSSemibold.copyWith(color: appTheme.gray)),
),
Row(
children: [
Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
const SizedBox(width: 10),
if (author.authorPlaceInRating != null)
InfoBouble(
radius: 12,
backgroundColor: appTheme.whiteA700,
children: [Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
if (customerProfile == null) ...[
Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
const SizedBox(width: 10),
if (author.authorPlaceInRating != null)
InfoBouble(
radius: 12,
backgroundColor: appTheme.cardBackground,
children: [
Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400)),
],
),
] else
Expanded(
child: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(UserDetailsScreen.name, extra: customerProfile!.slug);
},
child: InfoBouble(
radius: 12,
backgroundColor: appTheme.cardBackground,
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('Анкета пользователя', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
),
),
),
],
),

View File

@@ -13,8 +13,15 @@ class CalendarWidget extends StatefulWidget {
class _CalendarWidgetState extends State<CalendarWidget> {
CalendarFormat _calendarFormat = CalendarFormat.week;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay = DateTime.now();
late DateTime _focusedDay;
DateTime? _selectedDay;
@override
void initState() {
super.initState();
_selectedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
_focusedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
}
@override
Widget build(BuildContext context) {
return Column(
@@ -23,7 +30,7 @@ class _CalendarWidgetState extends State<CalendarWidget> {
Padding(
padding: const EdgeInsets.only(top: 5),
child: TableCalendar(
firstDay: DateTime.now(),
firstDay: widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0],
lastDay: DateTime.utc(2030, 3, 14),
locale: 'ru_RU',
startingDayOfWeek: StartingDayOfWeek.monday,

View File

@@ -2,20 +2,25 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
class InfoBouble extends StatelessWidget {
const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100, this.padding});
const InfoBouble({
super.key,
required this.children,
this.backgroundColor,
this.radius = 100,
this.padding,
this.mainAxisAlignment = MainAxisAlignment.start,
});
final List<Widget> children;
final Color? backgroundColor;
final double radius;
final EdgeInsetsGeometry? padding;
final MainAxisAlignment mainAxisAlignment;
@override
Widget build(BuildContext context) {
return Container(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 12, vertical: 8.5),
decoration: BoxDecoration(
color: backgroundColor ?? appTheme.gray50,
borderRadius: BorderRadius.circular(radius),
),
child: Row(mainAxisSize: MainAxisSize.min, children: children),
decoration: BoxDecoration(color: backgroundColor ?? appTheme.gray50, borderRadius: BorderRadius.circular(radius)),
child: Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: mainAxisAlignment, children: children),
);
}
}

View File

@@ -67,11 +67,19 @@ abstract class PacketOptions with _$PacketOptions {
@JsonKey(name: "price") required dynamic price,
@JsonKey(name: "short") required String short,
@JsonKey(name: "description_md") required String description,
@JsonKey(name: "has_personal_diet", fromJson: _parseHasPersonalDietField) required bool hasPersonalDiet,
}) = _PacketOptions;
factory PacketOptions.fromJson(Map<String, dynamic> json) => _$PacketOptionsFromJson(json);
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '');
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '', hasPersonalDiet: false);
}
bool _parseHasPersonalDietField(dynamic json) {
if (json == null) return false;
if (json == '1') return true;
if (json == '0') return false;
return json as bool;
}
List<String> _parseItems(dynamic json) {

View File

@@ -1,9 +1,11 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/core/widgets/marketing_bottom_sheet.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
@@ -71,6 +73,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
void initState() {
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants);
_checkRegistrationDate(context);
super.initState();
}
@@ -78,6 +81,15 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
}
void _checkRegistrationDate(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final needToShowMarketingBottomSheet = await context.read<MarketingCubit>().checkRegistrationDate();
if (needToShowMarketingBottomSheet && context.mounted) {
showMarketingBottomSheet(context);
}
});
}
@override
void dispose() {
blogPostsScrollController.dispose();

View File

@@ -17,6 +17,7 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class BlogPostWidget extends StatefulWidget {
final int index;
@@ -57,10 +58,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: state.feedBlogPostList[widget.index].author.authorSlug,
);
GoRouter.of(context).pushNamed('profileScreen', extra: state.feedBlogPostList[widget.index].author.authorSlug);
},
child: CircleAvatar(
radius: 20,
@@ -126,14 +124,39 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
const SizedBox(height: 8),
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Html(
data:
(state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description)
.replaceAll('\n', '<br>'),
style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)},
data: md.markdownToHtml(
(state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
if (state.feedBlogPostList[widget.index].description.length > 200)

View File

@@ -17,15 +17,12 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class DietsWidget extends StatefulWidget {
class DietsWidget extends StatefulWidget {
final Diet diet;
final int index;
const DietsWidget({
super.key,
required this.diet,
required this.index,
});
const DietsWidget({super.key, required this.diet, required this.index});
@override
State<DietsWidget> createState() => _DietsWidgetState();
@@ -33,18 +30,11 @@ class DietsWidget extends StatefulWidget {
class _DietsWidgetState extends State<DietsWidget> {
bool isMoreOptionsVisible = false;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => GoRouter.of(context).pushNamed(
'aboutDietScreen',
extra: {
'slug': widget.diet.slug,
'index': widget.index,
'navigateToComments': false,
},
),
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': false}),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -104,12 +94,7 @@ class _DietsWidgetState extends State<DietsWidget> {
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
diet: widget.diet,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
type: ProductType.diet,
),
child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet),
),
),
],
@@ -120,26 +105,48 @@ class _DietsWidgetState extends State<DietsWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.diet.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.diet.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.diet.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.diet.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -149,12 +156,7 @@ class _DietsWidgetState extends State<DietsWidget> {
padding: EdgeInsets.only(left: 16, top: 8),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
],
@@ -172,18 +174,14 @@ class _DietsWidgetState extends State<DietsWidget> {
await context.read<DietsCubit>().likeDiet(widget.diet.id, widget.index);
},
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
icon: Icons.chat_bubble_outline,
text: widget.diet.totalComments.toString(),
onTap: () =>
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': true}),
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
onTap: () async {

View File

@@ -19,16 +19,13 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class PlanWidget extends StatefulWidget {
final PlanModel plan;
final int index;
const PlanWidget({
super.key,
required this.plan,
required this.index,
});
const PlanWidget({super.key, required this.plan, required this.index});
@override
State<PlanWidget> createState() => _PlanWidgetState();
@@ -41,14 +38,7 @@ class _PlanWidgetState extends State<PlanWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return InkWell(
onTap: () => GoRouter.of(context).pushNamed(
'aboutPlanScreen',
extra: {
'slug': widget.plan.slug,
'index': widget.index,
'navigateToComments': false,
},
),
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': false}),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -109,12 +99,7 @@ class _PlanWidgetState extends State<PlanWidget> {
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
plan: widget.plan,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
type: ProductType.plan,
),
child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan),
),
),
],
@@ -125,26 +110,43 @@ class _PlanWidgetState extends State<PlanWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.plan.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18),
),
child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18)),
),
if (widget.plan.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.plan.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
margin: Margins.zero,
color: appTheme.black,
fontSize: FontSize(16),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -153,12 +155,7 @@ class _PlanWidgetState extends State<PlanWidget> {
padding: EdgeInsets.only(left: 16, top: 8),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
],
@@ -167,10 +164,7 @@ class _PlanWidgetState extends State<PlanWidget> {
const SizedBox(height: 16),
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: widget.plan.author.authorSlug,
);
GoRouter.of(context).pushNamed('profileScreen', extra: widget.plan.author.authorSlug);
},
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
@@ -181,10 +175,7 @@ class _PlanWidgetState extends State<PlanWidget> {
height: 46,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon),
fit: BoxFit.cover,
),
image: DecorationImage(image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover),
),
),
subtitle: Row(
@@ -194,12 +185,7 @@ class _PlanWidgetState extends State<PlanWidget> {
backgroundColor: appTheme.gray50,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
children: [
Text(
'${widget.plan.author.authorPlaceInRating}-й в рейтинге',
style: TextStyles.textXSSemibold.copyWith(
color: appTheme.blueA400,
),
),
Text('${widget.plan.author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textXSSemibold.copyWith(color: appTheme.blueA400)),
],
),
const Spacer(),
@@ -212,21 +198,14 @@ class _PlanWidgetState extends State<PlanWidget> {
),
title: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
widget.plan.author.authorName,
style: TextStyles.textSSemibold.copyWith(
color: appTheme.black,
),
),
child: Text(widget.plan.author.authorName, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
),
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(
left: 16,
),
padding: const EdgeInsets.only(left: 16),
child: Row(
children: [
FeedIconButton(
@@ -238,20 +217,17 @@ class _PlanWidgetState extends State<PlanWidget> {
await context.read<PlansCubit>().likePlan(widget.plan.slug, widget.index);
},
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
icon: Icons.chat_bubble_outline,
text: widget.plan.totalComments.toString(),
onTap: () {
return GoRouter.of(context)
.pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
onTap: () {
return GoRouter.of(
context,
).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
},
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
onTap: () async {

View File

@@ -20,16 +20,13 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class RecipesWidget extends StatefulWidget {
final int index;
final Recipe recipe;
const RecipesWidget({
super.key,
required this.recipe,
required this.index,
});
const RecipesWidget({super.key, required this.recipe, required this.index});
@override
State<RecipesWidget> createState() => _RecipesWidgetState();
@@ -56,14 +53,8 @@ class _RecipesWidgetState extends State<RecipesWidget> {
}
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': widget.recipe.slug,
'index': widget.index,
'navigateToComments': false,
},
),
onTap: () =>
GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': false}),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -139,10 +130,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.recipe.title ?? '',
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.recipe.title ?? '', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.recipe.videoUrl != null && widget.recipe.videoUrl!.isNotEmpty) ...[
const SizedBox(height: 12),
@@ -152,10 +140,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
children: [
SvgPicture.asset('assets/icons/play_icon.svg', width: 16, height: 16),
const SizedBox(width: 8),
Text(
s.there_is_a_video,
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(s.there_is_a_video, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
),
@@ -165,17 +150,41 @@ class _RecipesWidgetState extends State<RecipesWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.recipe.description.length > 200
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
: widget.recipe.description.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.recipe.description.length > 200
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
: widget.recipe.description.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
color: appTheme.black,
margin: Margins.all(0),
fontFamily: TextStyles.textSMedium.fontFamily,
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -184,12 +193,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
padding: EdgeInsets.only(left: 16, top: 8),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
const SizedBox(height: 12),
@@ -211,14 +215,9 @@ class _RecipesWidgetState extends State<RecipesWidget> {
FeedIconButton(
icon: Icons.chat_bubble_outline,
text: widget.recipe.commentsCount.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': widget.recipe.slug,
'index': widget.index,
'navigateToComments': true,
},
),
onTap: () => GoRouter.of(
context,
).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': true}),
),
const SizedBox(width: 8),
FeedIconButton(
@@ -239,19 +238,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
},
),
const Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(
widget.recipe.viewsCount.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
),
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),
@@ -268,26 +259,17 @@ class _RecipesWidgetState extends State<RecipesWidget> {
Container _iconButton(String iconPath, [String? text]) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)),
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
iconPath,
),
SvgPicture.asset(iconPath),
if (text != null) const SizedBox(width: 8),
if (text != null)
Text(
text,
style: const TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500),
),
],
),

View File

@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.d
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -24,14 +25,12 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class MyDietWidget extends StatefulWidget {
final Diet diet;
const MyDietWidget({
super.key,
required this.diet,
});
const MyDietWidget({super.key, required this.diet});
@override
State<MyDietWidget> createState() => _MyDietWidgetState();
@@ -42,10 +41,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -78,26 +74,48 @@ class _MyDietWidgetState extends State<MyDietWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.diet.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.diet.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.diet.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.diet.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -129,6 +147,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
}
} else if (state.selectedStateIndex == 2) {
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: widget.diet.id);
} else {
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
}
@@ -230,11 +249,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/trash_can.svg',
width: 20,
height: 20,
),
leftIcon: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),

View File

@@ -18,6 +18,7 @@ import 'package:flutter_html/flutter_html.dart';
// Removed SVG icon usage after switching to text buttons
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class MyPlanWidget extends StatefulWidget {
final PlanModel plan;
@@ -43,7 +44,11 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width),
child: CachedNetworkImage(
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
),
Positioned(
@@ -68,16 +73,41 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),

View File

@@ -316,7 +316,7 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
type: type,
planName: json['plan_name'],
customerName: json['client_name'],
planId: json['plan_id'],
planId: json['chosen_plan_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
@@ -1695,6 +1695,35 @@ class DiaryReminderForUsersNotification implements BaseNotificationModel {
}
}
class PersonalDietIsDefinedToCustomerNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String dietName;
final int dietId;
PersonalDietIsDefinedToCustomerNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.dietName,
required this.dietId,
});
factory PersonalDietIsDefinedToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PersonalDietIsDefinedToCustomerNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
dietName: json['diet_name'] ?? '',
dietId: json['diet_id'],
);
}
}
enum NotificationType {
// Diet related
dietCancelled,
@@ -1726,6 +1755,7 @@ enum NotificationType {
userDailyReportCompleted,
chosenPlanFinishedToConsultant,
stayOnTrackToCustomer,
personalDietIsDefinedToCustomer,
// Comments
entityCommented,
@@ -1834,6 +1864,7 @@ enum NotificationType {
'chosen-plan-finished-to-consultant' => NotificationType.chosenPlanFinishedToConsultant,
'stay-on-track-to-customer' => NotificationType.stayOnTrackToCustomer,
'diary-reminder-for-users' => NotificationType.diaryReminderForUsers,
'personal-diet-is-defined-to-customer' => NotificationType.personalDietIsDefinedToCustomer,
// Legacy mapping
'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor,

View File

@@ -261,6 +261,12 @@ class NotificationsCubit extends Cubit<NotificationsState> {
createdAt: createdAt,
),
NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.personalDietIsDefinedToCustomer => PersonalDietIsDefinedToCustomerNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
@@ -113,7 +114,11 @@ class NotificationWidget extends StatelessWidget {
notification: notification as ProductStatusUpdatedForAuthorNotification,
),
DiaryReminderForUsersNotification() => _DiaryReminderForUsersWidget(notification: notification as DiaryReminderForUsersNotification),
PersonalDietIsDefinedToCustomerNotification() => _PersonalDietIsDefinedToCustomerWidget(
notification: notification as PersonalDietIsDefinedToCustomerNotification,
),
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
// TODO: Handle this case.
};
}
}
@@ -364,34 +369,38 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
// CardSettingsScreen
class _BaseNotificationWidget extends StatelessWidget {
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt});
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt, this.onTap});
final String notificationsText;
final bool isRead;
final DateTime createdAt;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
if (!isRead)
Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
width: 8,
height: 8,
return GestureDetector(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
],
),
const SizedBox(height: 4),
Text(
'${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}',
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
),
],
if (!isRead)
Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
width: 8,
height: 8,
),
],
),
const SizedBox(height: 4),
Text(
'${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}',
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
),
],
),
);
}
@@ -1053,3 +1062,17 @@ class _DiaryReminderForUsersWidget extends StatelessWidget {
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Напоминание о заполнении дневника', createdAt: notification.createdAt);
}
}
class _PersonalDietIsDefinedToCustomerWidget extends StatelessWidget {
const _PersonalDietIsDefinedToCustomerWidget({required this.notification});
final PersonalDietIsDefinedToCustomerNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
onTap: () => GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId, 'navigateToComments': false}),
notificationsText: 'Персональная диета "${notification.dietName}" установлена',
createdAt: notification.createdAt,
);
}
}

View File

@@ -407,6 +407,12 @@ class NotificationsService {
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.personalDietIsDefinedToCustomer => PersonalDietIsDefinedToCustomerNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
};
} catch (e) {

View File

@@ -1,12 +1,13 @@
import 'dart:developer';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class OrderInfoCubit extends Cubit<OrderInfoState> {
OrderInfoCubit({required this.chatRepository}) : super(OrderInfoState());
OrderInfoCubit({required this.chatRepository, required this.profileRepository}) : super(OrderInfoState());
final ChatRepository chatRepository;
final ProfileRepository profileRepository;
Future<String?> getPaymentLink() async {
try {
@@ -20,10 +21,14 @@ class OrderInfoCubit extends Cubit<OrderInfoState> {
}
}
Future<void> getChosenPlan(int id) async {
Future<void> getChosenPlan({required int id, String? customerSlug}) async {
try {
emit(state.copyWith(isLoading: true));
final chosenPlan = await chatRepository.getChosenPlan(id: id);
if (customerSlug != null) {
final profile = await profileRepository.getProfileBySlug(customerSlug);
emit(state.copyWith(customerProfile: profile));
}
emit(state.copyWith(isLoading: false, chosenPlan: chosenPlan));
} catch (e) {
log(e.toString());

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'order_info_state.freezed.dart';
@@ -9,6 +10,7 @@ abstract class OrderInfoState with _$OrderInfoState {
factory OrderInfoState({
@Default(true) bool isLoading,
@Default(null) ChosenPlanModel? chosenPlan,
@Default(null) ProfileModel? customerProfile,
}) = _OrderInfoState;
}

View File

@@ -24,31 +24,24 @@ import 'package:go_router/go_router.dart';
class InfoByOrderScreen extends StatelessWidget {
static const name = 'infoByOrderScreen';
static const path = '/info-by-order-screen';
const InfoByOrderScreen({super.key, this.successfulPayment, required this.planId});
const InfoByOrderScreen({super.key, this.successfulPayment, required this.planId, this.customerSlug});
final bool? successfulPayment;
final int planId;
final String? customerSlug;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<OrderInfoCubit>()..getChosenPlan(planId),
child: InfoByOrderScreenContent(
successfulPayment: successfulPayment,
planId: planId,
),
create: (context) => sl<OrderInfoCubit>()..getChosenPlan(id: planId, customerSlug: customerSlug),
child: InfoByOrderScreenContent(successfulPayment: successfulPayment, planId: planId, customerSlug: customerSlug),
);
}
}
class InfoByOrderScreenContent extends StatefulWidget {
static const name = 'infoByOrderScreen';
static const path = '/info-by-order-screen';
const InfoByOrderScreenContent({
super.key,
required this.planId,
required this.successfulPayment,
});
class InfoByOrderScreenContent extends StatefulWidget {
const InfoByOrderScreenContent({super.key, required this.planId, required this.successfulPayment, this.customerSlug});
final int planId;
final bool? successfulPayment;
final String? customerSlug;
@override
State<InfoByOrderScreenContent> createState() => _InfoByOrderScreenContentState();
}
@@ -64,39 +57,30 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
if (state.isLoading) {
return Scaffold(
appBar: AppBar(
title: Text(
s.infoAboutOrder,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
}),
),
body: const Center(
child: Loader(),
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
),
body: const Center(child: Loader()),
);
}
if (plan == null) {
return Scaffold(
appBar: AppBar(
title: Text(
s.infoAboutOrder,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
}),
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
),
body: Center(
child: Text(
s.sorry_we_did_not_find_the_plan,
style: TextStyles.textSSemibold.copyWith(color: appTheme.black),
),
child: Text(s.sorry_we_did_not_find_the_plan, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
),
);
}
@@ -122,58 +106,35 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
title: Text(
s.infoAboutOrder,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
title: Text(s.infoAboutOrder, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
}),
icon: const Icon(Icons.arrow_back),
onPressed: () {
GoRouter.of(context).pop();
},
),
),
body: SingleChildScrollView(
child: Column(
children: [
if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[
CollectChargeBanner(
chosenPlan: plan,
price: chosenOption?.price ?? 0,
)
],
if (widget.successfulPayment != null) ...[
const SizedBox(height: 8),
PaymentBanner(
successfulPayment: widget.successfulPayment ?? false,
),
],
if (plan.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[CollectChargeBanner(chosenPlan: plan, price: chosenOption?.price ?? 0)],
if (widget.successfulPayment != null) ...[const SizedBox(height: 8), PaymentBanner(successfulPayment: widget.successfulPayment ?? false)],
const SizedBox(height: 8),
AuthorCard(
author: plan.plan.author,
title: s.plan_seller,
title: state.customerProfile != null ? s.customer : s.plan_seller,
titleStyle: TextStyles.textSSemibold.copyWith(color: appTheme.gray),
backgroundColor: appTheme.whiteA700,
customerProfile: state.customerProfile,
),
const SizedBox(height: 8),
PlanProps(
planDuration: plan.plan.duration,
planPrice: chosenOption?.price.toDouble() ?? 0,
planStatus: planStatus),
PlanProps(planDuration: plan.plan.duration, planPrice: chosenOption?.price.toDouble() ?? 0, planStatus: planStatus),
const SizedBox(height: 8),
PlanInfo(
title: plan.plan.name,
description: plan.plan.details,
imagePath: plan.plan.image,
),
PlanInfo(title: plan.plan.name, description: plan.plan.details, imagePath: plan.plan.image),
const SizedBox(height: 8),
OptionWidget(
option: chosenOption,
disableDuration: true,
),
OptionWidget(option: chosenOption, disableDuration: true, duration: plan.plan.duration),
const SizedBox(height: 8),
PlanState(
state: plan.chosenPlanState,
),
PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,),
const SizedBox(height: 20),
if (widget.successfulPayment == null)
CustomOutlinedButton(
@@ -184,13 +145,10 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
onPressed: () async {
showModalBottomSheet(
context: context,
builder: (context) => CancelPlanBottomSheet(
enableForm: plan.chosenPlanState.isStateAfterPaidState(),
planId: plan.id,
),
builder: (context) => CancelPlanBottomSheet(enableForm: plan.chosenPlanState.isStateAfterPaidState(), planId: plan.id),
);
},
)
),
],
),
),

View File

@@ -11,11 +11,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class PaymentBanner extends StatefulWidget {
const PaymentBanner({
super.key,
required this.successfulPayment,
});
const PaymentBanner({super.key, required this.successfulPayment});
final bool successfulPayment;
@override
@@ -29,8 +25,9 @@ class _PaymentBannerState extends State<PaymentBanner> {
final s = S.of(context);
final size = MediaQuery.sizeOf(context);
return MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Stack(children: [
padding: const EdgeInsets.symmetric(vertical: 16),
child: Stack(
children: [
Positioned(
left: 0,
top: 0,
@@ -38,8 +35,9 @@ class _PaymentBannerState extends State<PaymentBanner> {
child: Container(
width: 4,
decoration: BoxDecoration(
color: widget.successfulPayment ? appTheme.blueA400 : appTheme.strongRed,
borderRadius: const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20))),
color: widget.successfulPayment ? appTheme.blueA400 : appTheme.strongRed,
borderRadius: const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20)),
),
),
),
Padding(
@@ -50,78 +48,65 @@ class _PaymentBannerState extends State<PaymentBanner> {
? [
SizedBox(
width: size.width * 0.6,
child: Text(
s.cunsutation_was_paid_successfully,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 8,
child: Text(s.cunsutation_was_paid_successfully, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 8),
SizedBox(
width: size.width * 0.6,
child: Text(
s.wait_for_start_day,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 16,
child: Text(s.wait_for_start_day, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black)),
),
const SizedBox(height: 16),
isLoading
? const Center(child: Loader())
: CustomOutlinedButton(
text: s.go_to_diary,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
GoRouter.of(context).pushReplacement('/diary-screen');
},
),
const SizedBox(height: 10),
isLoading
? const Center(child: Loader())
: CustomOutlinedButton(
text: s.back_to_chat,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () {
GoRouter.of(context).pop();
},
)
),
]
: [
SizedBox(
width: size.width * 0.6,
child: Text(
s.smth_went_wrong_payment_failed,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 8,
child: Text(s.smth_went_wrong_payment_failed, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 8),
SizedBox(
width: size.width * 0.6,
child: Text(
s.try_again_2,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 16,
child: Text(s.try_again_2, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black)),
),
const SizedBox(height: 16),
isLoading
? const Center(child: Loader())
: CustomOutlinedButton(
text: s.pay,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
final planId = context.read<OrderInfoCubit>().state.chosenPlan?.id;
setState(() {
@@ -133,16 +118,15 @@ class _PaymentBannerState extends State<PaymentBanner> {
});
if (paymentLink != null && planId != null) {
if (!context.mounted) return;
GoRouter.of(context).pushReplacement(
PaymentScreen.path,
extra: {'url': paymentLink, 'planId': planId},
);
GoRouter.of(context).pushReplacement(PaymentScreen.path, extra: {'url': paymentLink, 'planId': planId});
}
},
)
),
],
),
)
]));
),
],
),
);
}
}

View File

@@ -5,42 +5,65 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
class PlanState extends StatelessWidget {
PlanState({super.key, required this.state});
class PlanState extends StatefulWidget {
const PlanState({super.key, required this.state, required this.hasPersonalDiet});
final ChosenPlanStateEnum state;
final List<List<ChosenPlanStateEnum>> states = [
[
ChosenPlanStateEnum.stateInited,
ChosenPlanStateEnum.stateInCoordination,
],
[
ChosenPlanStateEnum.stateStartDateHasBeenSet,
ChosenPlanStateEnum.statePendingPayment,
],
[
ChosenPlanStateEnum.statePaid,
],
[
ChosenPlanStateEnum.stateStarted,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
],
[
ChosenPlanStateEnum.stateComplained,
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.stateChargeWasCollected,
]
];
final bool hasPersonalDiet;
@override
State<PlanState> createState() => _PlanStateState();
}
class _PlanStateState extends State<PlanState> {
late final List<List<ChosenPlanStateEnum>> states;
@override
void initState() {
super.initState();
if (widget.hasPersonalDiet) {
states = [
[ChosenPlanStateEnum.stateInited, ChosenPlanStateEnum.stateInCoordination],
[ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment],
[ChosenPlanStateEnum.statePaid],
[ChosenPlanStateEnum.personalDietSet],
[
ChosenPlanStateEnum.stateStarted,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
],
[ChosenPlanStateEnum.stateComplained, ChosenPlanStateEnum.stateDone, ChosenPlanStateEnum.stateChargeWasCollected],
];
} else {
states = [
[ChosenPlanStateEnum.stateInited, ChosenPlanStateEnum.stateInCoordination],
[ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment],
[ChosenPlanStateEnum.statePaid],
[
ChosenPlanStateEnum.stateStarted,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
],
[ChosenPlanStateEnum.stateComplained, ChosenPlanStateEnum.stateDone, ChosenPlanStateEnum.stateChargeWasCollected],
];
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
final titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.in_progress, s.done];
final List<String> titles;
if (widget.hasPersonalDiet) {
titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.personal_diet_set, s.in_progress, s.done];
} else {
titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.in_progress, s.done];
}
return MainBackgroundBouble(
padding: const EdgeInsets.all(16),
child: Column(
children: List.generate(states.length, (index) {
final color = index <= states.indexWhere((element) => element.contains(state)) ? appTheme.greenA700 : appTheme.lightGray00;
final color = index <= states.indexWhere((element) => element.contains(widget.state)) ? appTheme.greenA700 : appTheme.lightGray00;
final text = titles[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -51,29 +74,16 @@ class PlanState extends StatelessWidget {
width: 24,
height: 24,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: Center(
child: Icon(
Icons.check,
color: appTheme.whiteA700,
size: 14,
),
),
child: Center(child: Icon(Icons.check, color: appTheme.whiteA700, size: 14)),
),
const SizedBox(width: 12),
Text(
text,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
Text(text, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
],
),
Padding(
padding: const EdgeInsets.only(left: 12),
child: Container(
height: 12,
width: 1,
color: color,
),
)
child: Container(height: 12, width: 1, color: color),
),
],
);
}),

View File

@@ -0,0 +1,67 @@
import 'dart:developer';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalDietSuggestionCubit extends Cubit<PersonalDietSuggestionState> {
final DietRepository dietRepository;
final PersonalDietSuggestionRepository personalDietSuggestionService;
late final int dietId;
PersonalDietSuggestionCubit({required this.dietRepository, required this.personalDietSuggestionService}) : super(PersonalDietSuggestionState());
Future<void> setDietName(int dietId) async {
try {
emit(state.copyWith(isLoading: true, error: null));
final diet = await dietRepository.getDiet(dietId);
emit(state.copyWith(isLoading: false, dietName: diet.name));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
Future<void> getConsultationWithPersonalDiets(int dietId) async {
try {
this.dietId = dietId;
emit(state.copyWith(isLoading: true, error: null));
setDietName(dietId);
final consultations = await personalDietSuggestionService.getConsultationWithPersonalDiets();
final consultationWithDiet = consultations.where((consultation) => consultation.personalDietId != null).toList();
final consultationWithoutDiet = consultations.where((consultation) => consultation.personalDietId == null).toList();
emit(state.copyWith(
isLoading: false,
consultationsWithDiet: consultationWithDiet,
consultationsWithoutDiet: consultationWithoutDiet,
));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
Future<bool> attachDietToConsultation({
required int consultationId,
}) async {
try {
await personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
return false;
}
}
Future<bool> reattachDietToConsultation({
required int consultationId,
}) async {
try {
await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
return false;
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'personal_diet_suggestion_state.freezed.dart';
@freezed
abstract class PersonalDietSuggestionState with _$PersonalDietSuggestionState {
factory PersonalDietSuggestionState({
@Default([]) List<ConsultationModel> consultationsWithDiet,
@Default([]) List<ConsultationModel> consultationsWithoutDiet,
@Default('') String dietName,
@Default(false) bool isLoading,
@Default(null) String? error,
}) = _PersonalDietSuggestionState;
}

View File

@@ -0,0 +1,358 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ConsultationWidget extends StatelessWidget {
const ConsultationWidget({super.key, required this.consultationModel, required this.dietName});
final ConsultationModel consultationModel;
final String dietName;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: appTheme.whiteA700,
),
child: Column(
children: [
if (consultationModel.personalDietId == null)
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultationModel.userProfilePhotoUrl),
),
const SizedBox(width: 12),
Column(
mainAxisSize: MainAxisSize.min,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultationModel.userName,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
Text(
consultationModel.planTitle,
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
),
Text(
'Старт: ${consultationModel.userName}',
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
),
],
)
],
)
else
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultationModel.userProfilePhotoUrl),
),
const SizedBox(width: 12),
Column(
mainAxisSize: MainAxisSize.min,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultationModel.userName,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
Text(
consultationModel.planTitle,
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
),
],
)
],
),
const SizedBox(
height: 12,
),
if (consultationModel.personalDietId != null)
CustomOutlinedButton(
text: s.changeDiet,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.dividerColor)),
onPressed: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestDietBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
onTap: () async {
final result = await context.read<PersonalDietSuggestionCubit>().attachDietToConsultation(consultationId: consultationModel.id);
AppRouter.navigatorKey.currentContext?.pop();
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionSuccessBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
return;
}
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionFailureBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
},
),
);
},
)
else
CustomOutlinedButton(
text: s.suggestDiet,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestDietBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
onTap: () async {
AppRouter.navigatorKey.currentContext?.pop();
final result = await context.read<PersonalDietSuggestionCubit>().attachDietToConsultation(consultationId: consultationModel.id);
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionSuccessBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
return;
}
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionFailureBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
},
),
);
},
)
],
),
);
}
}
class SuggestDietBottomSheet extends StatefulWidget {
const SuggestDietBottomSheet({
super.key,
required this.userName,
required this.dietName,
required this.onTap,
});
final String userName;
final String dietName;
final Future<void> Function() onTap;
@override
State<SuggestDietBottomSheet> createState() => _SuggestDietBottomSheet();
}
class _SuggestDietBottomSheet extends State<SuggestDietBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
Text(
'Предложить пользователю ${widget.userName} «${widget.dietName}»',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
CustomOutlinedButton(
text: s.toOffer,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: widget.onTap,
),
const SizedBox(
height: 40,
),
],
),
);
}
}
class SuggestionSuccessBottomSheet extends StatefulWidget {
const SuggestionSuccessBottomSheet({
super.key,
required this.userName,
required this.dietName,
});
final String userName;
final String dietName;
@override
State<SuggestionSuccessBottomSheet> createState() => _SuggestionSuccessBottomSheet();
}
class _SuggestionSuccessBottomSheet extends State<SuggestionSuccessBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
Text(
'«${widget.dietName}» предложена пользователю ${widget.userName}',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
CustomOutlinedButton(
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
AppRouter.navigatorKey.currentContext?.pop();
},
),
const SizedBox(
height: 40,
),
],
),
);
}
}
class SuggestionFailureBottomSheet extends StatefulWidget {
const SuggestionFailureBottomSheet({
super.key,
required this.userName,
required this.dietName,
});
final String userName;
final String dietName;
@override
State<SuggestionFailureBottomSheet> createState() => _SuggestionFailureBottomSheet();
}
class _SuggestionFailureBottomSheet extends State<SuggestionFailureBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
Text(
'Не удалось предложить диету ${widget.dietName} пользователю ${widget.userName}',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
CustomOutlinedButton(
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
AppRouter.navigatorKey.currentContext?.pop();
},
),
const SizedBox(
height: 40,
),
],
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/consultation_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalDietSuggestionScreen extends StatelessWidget {
final int dietId;
const PersonalDietSuggestionScreen({super.key, required this.dietId});
static const path = '/personal-diet-suggestion';
static const name = 'personalDietSuggestion';
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<PersonalDietSuggestionCubit>()..getConsultationWithPersonalDiets(dietId),
child: const PersonalDietSuggestionScreenContent(),
);
}
}
class PersonalDietSuggestionScreenContent extends StatelessWidget {
const PersonalDietSuggestionScreenContent({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<PersonalDietSuggestionCubit, PersonalDietSuggestionState>(
builder: (context, state) {
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
'${s.dietSuggestion}\n«${state.dietName}»',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: state.isLoading
? const Center(child: Loader())
: SingleChildScrollView(
child: Column(
children: [
...List.generate(state.consultationsWithDiet.length, (index) {
return ConsultationWidget(
consultationModel: state.consultationsWithDiet[index],
dietName: state.dietName,
);
}),
const SizedBox(
height: 12,
),
...List.generate(state.consultationsWithoutDiet.length, (index) {
return ConsultationWidget(
consultationModel: state.consultationsWithoutDiet[index],
dietName: state.dietName,
);
})
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
class PersonalDietSuggestionRepository {
final PersonalDietSuggestionService personalDietSuggestionService;
PersonalDietSuggestionRepository({required this.personalDietSuggestionService});
Future<List<ConsultationModel>> getConsultationWithPersonalDiets() async{
try {
return (await personalDietSuggestionService.getConsultationWithPersonalDiets()).data;
} catch (e) {
rethrow;
}
}
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) {
try {
return personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
} catch (e) {
rethrow;
}
}
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) {
try {
return personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,36 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
class PersonalDietSuggestionService {
final Dio dio;
PersonalDietSuggestionService(this.dio);
Future<ConsultationPaginationModel> getConsultationWithPersonalDiets() async {
try {
final response = await dio.get('/api/consultations', queryParameters: {
'filters[plan][has_personal_diet][\$personal_diet]': '1',
'filters[last_state][\$notIn][]': ['done', 'charge_was_collected', 'complained', 'last_day_finished', 'canceled_by_the_initiator', 'inited'],
'sort[]': 'start_date:desc',
});
return ConsultationPaginationModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) async {
try {
await dio.post('/api/chosen_plans/$consultationId/attach_diet', data: {'diet_id': dietId});
} catch (e) {
rethrow;
}
}
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) async {
try {
await dio.post('/api/chosen_plans/$consultationId/re_attach_diet', data: {'diet_id': dietId});
} catch (e) {
rethrow;
}
}
}

View File

@@ -27,6 +27,8 @@ enum ChosenPlanStateEnum {
stateComplained,
@JsonValue('done')
stateDone,
@JsonValue('personal_diet_setted')
personalDietSet,
@JsonValue('charge_was_collected')
stateChargeWasCollected;
@@ -40,6 +42,7 @@ enum ChosenPlanStateEnum {
ChosenPlanStateEnum.stateLastDayFinished,
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.personalDietSet,
];
}
@@ -64,7 +67,8 @@ enum ChosenPlanStateEnum {
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
ChosenPlanStateEnum.stateComplained,
ChosenPlanStateEnum.stateDone
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.personalDietSet
];
}
}

View File

@@ -505,12 +505,15 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
switch (state.chosenOptionIndex) {
1 => OptionWidget(
option: state.planModel?.packet2Options,
duration: state.planModel?.duration ?? 0,
),
2 => OptionWidget(
option: state.planModel?.packet3Options,
duration: state.planModel?.duration ?? 0,
),
_ => OptionWidget(
option: state.planModel?.packet1Options,
duration: state.planModel?.duration ?? 0,
),
},
const SizedBox(

View File

@@ -16,17 +16,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:share_plus/share_plus.dart';
class ProfileBlogPostWidget extends StatefulWidget {
final int index;
final ProfileCubit profileCubit;
const ProfileBlogPostWidget({
super.key,
required this.index,
required this.profileCubit,
});
const ProfileBlogPostWidget({super.key, required this.index, required this.profileCubit});
@override
State<ProfileBlogPostWidget> createState() => _ProfileBlogPostWidgetState();
@@ -46,15 +43,9 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
totalLikes = state.blogPosts[widget.index].totalLikes;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.blogPosts[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.blogPosts[widget.index].id),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -80,19 +71,12 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
children: [
Text(
state.blogPosts[widget.index].author.authorName,
style: const TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500),
),
const SizedBox(height: 4),
Text(
'${calculateDifference(state.blogPosts[widget.index].createdAt)} в ${state.blogPosts[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.blogPosts[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(
height: 1,
color: Colors.black45,
),
style: const TextStyle(height: 1, color: Colors.black45),
),
],
),
@@ -114,11 +98,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.blogPosts[widget.index].title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: Colors.black87,
),
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.black87),
),
),
],
@@ -126,16 +106,38 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
if (state.blogPosts[widget.index].title.isNotEmpty && state.blogPosts[widget.index].description.isNotEmpty) const SizedBox(height: 8),
if (state.blogPosts[widget.index].description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Html(
data: state.blogPosts[widget.index].description.length > 200
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
state.blogPosts[widget.index].description.length > 200
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
fontSize: FontSize(16),
color: Colors.black87,
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -144,12 +146,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
if (state.blogPosts[widget.index].title.isNotEmpty || state.blogPosts[widget.index].description.isNotEmpty) const SizedBox(height: 8),
@@ -157,10 +154,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}'],
index: 0,
),
PhotoViewScreen(imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}'], index: 0),
);
},
child: Hero(
@@ -192,10 +186,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
isLiked
? await context.read<FeedCubit>().likePost(state.blogPosts[widget.index].id)
: await context.read<FeedCubit>().unlikedPost(state.blogPosts[widget.index].id);
widget.profileCubit.updateFeedBlogPostList(
state.blogPosts[widget.index],
isLiked ? false : true,
);
widget.profileCubit.updateFeedBlogPostList(state.blogPosts[widget.index], isLiked ? false : true);
},
),
const SizedBox(width: 8),
@@ -204,16 +195,10 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
'assets/icons/comment.svg',
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
appTheme.gray600,
BlendMode.srcIn,
),
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
),
text: state.blogPosts[widget.index].totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.blogPosts[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.blogPosts[widget.index].id),
),
const SizedBox(width: 8),
FeedIconButton(
@@ -234,19 +219,11 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
},
),
const Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(
state.blogPosts[widget.index].totalViews.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
),
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),

View File

@@ -15,15 +15,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:share_plus/share_plus.dart';
class MyRecipeWidget extends StatefulWidget {
final MyRecipeModel recipe;
const MyRecipeWidget({
super.key,
required this.recipe,
});
const MyRecipeWidget({super.key, required this.recipe});
@override
State<MyRecipeWidget> createState() => _MyRecipeWidgetState();
@@ -50,10 +48,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -61,13 +56,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
Stack(
children: [
InkWell(
onTap: () async => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': widget.recipe.slug,
'navigateToComments': false,
},
),
onTap: () async => GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
@@ -99,28 +88,48 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.recipe.title!,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.recipe.title!, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.recipe.description.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.recipe.description.length > 200
? '${widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.recipe.description.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.recipe.description.length > 200
? '${widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.recipe.description.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -192,26 +201,17 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
Container _iconButton(String iconPath, [String? text]) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)),
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
iconPath,
),
SvgPicture.asset(iconPath),
if (text != null) const SizedBox(width: 8),
if (text != null)
Text(
text,
style: const TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500),
),
],
),

View File

@@ -3,6 +3,8 @@ import 'dart:developer';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
@@ -12,10 +14,9 @@ import 'package:eatvio_app/features/registration/services/registration_analytics
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class RegistrationCubit extends Cubit<RegistrationState> {
class RegistrationCubit extends Cubit<RegistrationState> with RegistrationDateCacheMixin {
final RegistrationRepository registrationRepository;
RegistrationCubit({required this.registrationRepository})
RegistrationCubit({required this.registrationRepository, required Prefs prefs})
: super(
const RegistrationState(
sheet: UserRegistrationSheet(
@@ -47,7 +48,9 @@ class RegistrationCubit extends Cubit<RegistrationState> {
phone: '',
),
),
);
) {
this.prefs = prefs;
}
void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet));
@@ -59,6 +62,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
await TokenStorage.saveTokenInStorage(token);
await sl<FirebaseApi>().registerPushToken();
RegistrationAnalyticsService.reportRegistrationComplete();
cacheRegistrationDate(DateTime.now());
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
} catch (e, st) {
log(e.toString(), stackTrace: st);

View File

@@ -0,0 +1,238 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/models/registration_model.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gpassword/gpassword.dart';
class RegistrationCubitV2 extends Cubit<RegistrationStateV2> {
final RegistrationRepositoryV2 registrationRepository;
RegistrationCubitV2({required this.registrationRepository}) : super(RegistrationStateV2());
void setFirstName(String firstName) {
emit(state.copyWith(firstName: firstName));
validateFirstName();
}
void setLastName(String lastName) {
emit(state.copyWith(lastName: lastName));
validateLastName();
}
void setEmail(String email) {
emit(state.copyWith(email: email));
validateEmail();
}
void setPhone(String phone) {
emit(state.copyWith(phone: phone));
validatePhone();
}
void setBirthday(DateTime birthday) {
emit(state.copyWith(birthday: birthday));
validateBirthday();
}
void setGender(String gender) {
emit(state.copyWith(gender: gender));
validateGender();
}
void setWeight(int weight) {
emit(state.copyWith(weight: weight));
validateWeight();
}
void setHeight(int height) {
emit(state.copyWith(height: height));
validateHeight();
}
void setActivityCoefficient(double activityCoefficient) {
emit(state.copyWith(activityCoefficient: activityCoefficient));
validateActivityCoefficient();
}
void setPassword(String password) {
emit(state.copyWith(password: password));
}
bool validateFirstName() {
if (state.firstName.isEmpty) {
emit(state.copyWith(isFirstNameValid: false));
return false;
}
emit(state.copyWith(isFirstNameValid: true));
return true;
}
bool validateLastName() {
if (state.lastName.isEmpty) {
emit(state.copyWith(isLastNameValid: false));
return false;
}
emit(state.copyWith(isLastNameValid: true));
return true;
}
bool validateEmail() {
if (state.email.isEmpty) {
emit(state.copyWith(isEmailValid: false));
return false;
}
emit(state.copyWith(isEmailValid: true));
return true;
}
bool validatePhone() {
if (state.phone.isEmpty) {
emit(state.copyWith(isPhoneValid: false));
return false;
}
emit(state.copyWith(isPhoneValid: true));
return true;
}
bool validateGender() {
if (state.gender.isEmpty) {
emit(state.copyWith(isGenderValid: false));
return false;
}
emit(state.copyWith(isGenderValid: true));
return true;
}
bool validateWeight() {
if (state.weight == 0) {
emit(state.copyWith(isWeightValid: false));
return false;
}
emit(state.copyWith(isWeightValid: true));
return true;
}
bool validateHeight() {
if (state.height == 0) {
emit(state.copyWith(isHeightValid: false));
return false;
}
emit(state.copyWith(isHeightValid: true));
return true;
}
bool validateActivityCoefficient() {
if (state.activityCoefficient == 0) {
emit(state.copyWith(isActivityCoefficientValid: false));
return false;
}
emit(state.copyWith(isActivityCoefficientValid: true));
return true;
}
bool validateBirthday() {
if (state.birthday == null) {
emit(state.copyWith(isBirthdayValid: false));
return false;
}
emit(state.copyWith(isBirthdayValid: true));
return true;
}
void nextStep() {
emit(state.copyWith(step: 1));
}
void prevStep() {
emit(state.copyWith(step: 0));
}
bool validateAllFields() {
if (!validateFirstName() ||
!validateLastName() ||
!validateEmail() ||
!validatePhone() ||
!validateGender() ||
!validateWeight() ||
!validateHeight() ||
!validateActivityCoefficient() ||
!validateBirthday()) {
return false;
}
return true;
}
Future<(bool, bool)> register() async {
if (!validateAllFields()) {
return (false, true);
}
final birthday = state.birthday?.format('yyyy-MM-dd');
if (birthday == null) {
validateBirthday();
return (false, true);
}
final password = await _generatePassword();
final registrationModel = RegistrationModel(
firstName: state.firstName,
lastName: state.lastName,
email: state.email,
phone: state.phone,
gender: state.gender,
password: password,
confirmPassword: password,
weight: state.weight,
height: state.height,
activityCoefficient: state.activityCoefficient,
birthday: birthday,
);
bool didApiComplainOnValidation = false;
try {
final token = await registrationRepository.register(registrationModel: registrationModel);
if (token != null) {
TokenStorage.saveTokenInStorage(token);
return (true, false);
}
} on DioException catch (e) {
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
final phoneErrors = errors?['phone'] as List<dynamic>?;
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
if (emailErrors != null && emailErrors.isNotEmpty) {
if (emailErrors.contains('Некорректный формат email')) {
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Некорректный формат email'));
didApiComplainOnValidation = true;
}
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Такая Эл. почта уже существует.'));
didApiComplainOnValidation = true;
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
if (phoneErrors.contains('Такой phone уже существует.')) {
emit(state.copyWith(isPhoneValid: false, phoneDisplayError: 'Такой номер телефона уже существует.'));
didApiComplainOnValidation = true;
}
}
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
if (dateOfBirth.contains('Некорректный формат даты рождения')) {
emit(state.copyWith(isBirthdayValid: false, emailDisplayError: 'Некорректный формат даты рождения'));
didApiComplainOnValidation = true;
}
}
} catch (e) {
log(e.toString());
}
return (false, didApiComplainOnValidation);
}
Future<String> _generatePassword() async {
final generator = GPassword();
final password = generator.generate(passwordLength: 10);
emit(state.copyWith(password: password));
return password;
}
}

View File

@@ -0,0 +1,32 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'registration_state.freezed.dart';
@freezed
abstract class RegistrationStateV2 with _$RegistrationStateV2 {
factory RegistrationStateV2({
@Default('') String firstName,
@Default('') String lastName,
@Default('') String email,
@Default('') String phone,
@Default('') String gender,
@Default('') String password,
@Default(0) int weight,
@Default(0) int height,
@Default(1.2) double activityCoefficient,
DateTime? birthday,
@Default(true) bool isFirstNameValid,
@Default(true) bool isLastNameValid,
@Default(true) bool isEmailValid,
@Default(true) bool isPhoneValid,
@Default(true) bool isGenderValid,
@Default(true) bool isWeightValid,
@Default(true) bool isHeightValid,
@Default(true) bool isActivityCoefficientValid,
@Default(true) bool isBirthdayValid,
@Default(0) int step,
@Default('') String emailDisplayError,
@Default('') String phoneDisplayError,
@Default('') String dateOfBirthDisplayError,
}) = _RegistrationStateV2;
}

View File

@@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'registration_model.freezed.dart';
part 'registration_model.g.dart';
@freezed
abstract class RegistrationModel with _$RegistrationModel {
factory RegistrationModel({
@JsonKey(name: 'first_name') required String firstName,
@JsonKey(name: 'last_name') required String lastName,
@JsonKey(name: 'email') required String email,
@JsonKey(name: 'phone') required String phone,
@JsonKey(name: 'gender') required String gender,
@JsonKey(name: 'password') required String password,
@JsonKey(name: 'password_confirmation') required String confirmPassword,
@JsonKey(name: 'weight') required int weight,
@JsonKey(name: 'height') required int height,
@JsonKey(name: 'activity_coefficient') required double activityCoefficient,
@JsonKey(name: 'birthdate') required String birthday,
}) = _RegistrationModel;
factory RegistrationModel.fromJson(Map<String, dynamic> json) => _$RegistrationModelFromJson(json);
}

View File

@@ -0,0 +1,47 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/first_step_content.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/second_step_content.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/step_progress.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/step_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
class RegistrationV2Page extends StatelessWidget {
const RegistrationV2Page({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(create: (context) => sl<RegistrationCubitV2>(), child: RegistrationV2PageContent());
}
}
class RegistrationV2PageContent extends StatelessWidget {
RegistrationV2PageContent({super.key});
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(centerTitle: true, toolbarHeight: 45, title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 45), actions: const [StepWidget()]),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Column(
children: [
const SizedBox(height: 4),
const StepProgress(),
const SizedBox(height: 20),
if (state.step == 0) const Expanded(child: FirstStepContent()) else if (state.step == 1) const Expanded(child: SecondStepContent()),
],
);
},
),
),
);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/sex_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
class FirstStepContent extends StatefulWidget {
const FirstStepContent({super.key});
@override
State<FirstStepContent> createState() => _FirstStepContentState();
}
class _FirstStepContentState extends State<FirstStepContent> {
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final dateController = TextEditingController();
@override
void initState() {
super.initState();
firstNameController.text = context.read<RegistrationCubitV2>().state.firstName;
lastNameController.text = context.read<RegistrationCubitV2>().state.lastName;
emailController.text = context.read<RegistrationCubitV2>().state.email;
phoneController.text = context.read<RegistrationCubitV2>().state.phone;
dateController.text = context.read<RegistrationCubitV2>().state.birthday?.format('dd.MM.yyyy') ?? '';
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.first_name, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.your_name,
hasError: !state.isFirstNameValid,
controller: firstNameController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setFirstName(val);
},
),
if (!state.isFirstNameValid) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.first_name_validation_error)],
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.last_name, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.your_last_name,
hasError: !state.isLastNameValid,
controller: lastNameController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setLastName(val);
},
),
if (!state.isLastNameValid) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.last_name_error)],
],
),
),
],
),
const SizedBox(height: 12),
Text(s.email, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
CustomTextFormField(
hintText: s.type_email,
hasError: !state.isEmailValid,
controller: emailController,
onChanged: (val) {
context.read<RegistrationCubitV2>().setEmail(val);
},
),
if (!state.isEmailValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.emailDisplayError.isEmpty ? s.email_error : state.emailDisplayError),
],
const SizedBox(height: 12),
Text(s.phone, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
Column(
children: [
CustomTextFormField(
hintText: '+7 (999) 999 99 99',
controller: phoneController,
hasError: !state.isPhoneValid,
onChanged: (val) {
context.read<RegistrationCubitV2>().setPhone(val);
},
),
if (!state.isPhoneValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.phoneDisplayError.isEmpty ? s.phone_error : state.phoneDisplayError),
],
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.date_of_birth, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
textInputAction: TextInputAction.done,
hintText: '01.01.2000',
keyboardType: TextInputType.number,
inputFormatters: [maskFormatterForBirthday],
hasError: !state.isBirthdayValid,
controller: dateController,
onChanged: (val) {
final dateArray = val.split('.');
if (dateArray.length != 3) return;
final date = DateTime(int.parse(dateArray[2]), int.parse(dateArray[1]), int.parse(dateArray[0]));
context.read<RegistrationCubitV2>().setBirthday(date);
},
),
if (!state.isBirthdayValid) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: state.dateOfBirthDisplayError.isEmpty ? s.date_of_birth_error : state.dateOfBirthDisplayError),
],
],
),
),
const SizedBox(width: 12),
const Expanded(child: SexWidget()),
],
),
const Spacer(),
CustomOutlinedButton(
onPressed: () {
context.read<RegistrationCubitV2>().nextStep();
},
text: s.next,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
),
const SizedBox(height: 20),
],
);
},
);
}
}

View File

@@ -0,0 +1,77 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
class PasswordField extends StatefulWidget {
const PasswordField({super.key, required this.cubit});
final RegistrationCubitV2 cubit;
@override
State<PasswordField> createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool copied = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
bloc: widget.cubit,
builder: (context, state) {
return Column(
children: [
Container(
decoration: BoxDecoration(color: appTheme.lightGray0, borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.only(left: 24, right: 16, top: 16, bottom: 16),
width: double.infinity,
child: Row(
children: [
Text(state.password, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black)),
const Spacer(),
GestureDetector(
onTap: copied
? null
: () async {
Clipboard.setData(ClipboardData(text: state.password));
setState(() {
copied = true;
});
Future.delayed(const Duration(seconds: 3), () {
if (!context.mounted) return;
setState(() {
copied = false;
});
});
},
child: SvgPicture.asset(
copied ? 'assets/icons/check_filled.svg' : 'assets/icons/copy2.svg',
height: 20,
width: 20,
colorFilter: copied ? null : ColorFilter.mode(appTheme.black, BlendMode.srcIn),
),
),
],
),
),
if (copied) ...[
const SizedBox(height: 6),
Row(
children: [
SvgPicture.asset('assets/icons/check.svg', height: 10, width: 10),
const SizedBox(width: 4),
Text(s.copied2, style: TextStyles.textSMedium.copyWith(color: appTheme.darkGreen)),
],
),
],
SizedBox(height: copied ? 18 : 12),
],
);
},
);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PhisicalActivity extends StatefulWidget {
const PhisicalActivity({super.key});
@override
State<PhisicalActivity> createState() => _PhisicalActivityState();
}
class _PhisicalActivityState extends State<PhisicalActivity> {
final List<String> activityDropDownTitles = [];
final List<double> activityDropDownValues = [1.2, 1.375, 1.55, 1.7, 1.9];
@override
void didChangeDependencies() {
super.didChangeDependencies();
final s = S.of(context);
if (activityDropDownTitles.isEmpty) {
activityDropDownTitles.add(s.activity_first_value);
activityDropDownTitles.add(s.activity_second_value);
activityDropDownTitles.add(s.activity_third_value);
activityDropDownTitles.add(s.activity_fourth_value);
activityDropDownTitles.add(s.activity_fifth_value);
}
}
// String _getTitleFromValue(double value) {
// switch (value) {
// case 1.2:
// return activityDropDownTitles[0];
// case 1.375:
// return activityDropDownTitles[1];
// case 1.55:
// return activityDropDownTitles[2];
// case 1.7:
// return activityDropDownTitles[3];
// case 1.9:
// return activityDropDownTitles[4];
// default:
// return activityDropDownTitles[5];
// }
// }
@override
Widget build(BuildContext context) {
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomDropDownButton<double>(
isExpanded: true,
onChanged: (value) {
if (value != null) {
context.read<RegistrationCubitV2>().setActivityCoefficient(value);
}
},
items: activityDropDownTitles
.mapIndexed(
(index, value) => DropdownMenuItem<double>(
value: activityDropDownValues[index],
child: Text(activityDropDownTitles[index], style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
)
.toList(),
value: state.activityCoefficient == 0 ? null : state.activityCoefficient,
selectedItemBuilder: (context) {
return activityDropDownTitles.mapIndexed((index, value) => Text(activityDropDownTitles[index], overflow: TextOverflow.clip, maxLines: 1)).toList();
},
),
],
);
},
);
}
}

View File

@@ -0,0 +1,175 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/phisical_activity.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/show_successful_registration_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
class SecondStepContent extends StatefulWidget {
const SecondStepContent({super.key});
@override
State<SecondStepContent> createState() => _SecondStepContentState();
}
class _SecondStepContentState extends State<SecondStepContent> {
bool isLoading = false;
final weightController = TextEditingController();
final heightController = TextEditingController();
@override
void initState() {
super.initState();
heightController.text = context.read<RegistrationCubitV2>().state.height.toString();
weightController.text = context.read<RegistrationCubitV2>().state.weight.toString();
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.last_step, style: TextStyles.textFS24FW700.copyWith(color: appTheme.black)),
const SizedBox(height: 12),
Text(s.user_data_desc, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.height, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.type_height,
hasError: !state.isHeightValid,
controller: heightController,
textInputType: const TextInputType.numberWithOptions(),
onChanged: (val) {
context.read<RegistrationCubitV2>().setHeight(int.parse(val));
},
),
if (!state.isHeightValid) ValidationErrorWidget(error: s.height_error),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.weight, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
hintText: s.type_weight,
hasError: !state.isWeightValid,
controller: weightController,
textInputType: const TextInputType.numberWithOptions(),
onChanged: (val) {
context.read<RegistrationCubitV2>().setWeight(int.parse(val));
},
),
if (!state.isWeightValid) ValidationErrorWidget(error: s.weight_error),
],
),
),
],
),
const SizedBox(height: 12),
Text(s.phisical_activity, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const PhisicalActivity(),
const SizedBox(height: 12),
const Spacer(),
Row(
children: [
InkWell(
onTap: () {
context.read<RegistrationCubitV2>().prevStep();
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: 52,
decoration: BoxDecoration(color: appTheme.gray50, borderRadius: BorderRadius.circular(20)),
child: Center(child: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400, size: 24)),
),
),
const SizedBox(width: 4),
isLoading
? const Expanded(child: Center(child: Loader()))
: Expanded(
child: CustomOutlinedButton(
onPressed: () async {
setState(() {
isLoading = true;
});
final cubit = context.read<RegistrationCubitV2>();
final (commonResult, hasValidationError) = await cubit.register();
setState(() {
isLoading = false;
});
if (context.mounted && !hasValidationError) {
showRegistrationBottomSheet(context, cubit, commonResult);
}
},
text: s.register,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
],
),
const SizedBox(height: 12),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая “Зарегистрироваться”, Вы соглашаетесь \n',
style: const TextStyle(fontSize: 12, color: Colors.black),
children: [
const TextSpan(text: 'c '),
if (!hidePlans) ...[
TextSpan(
text: ' Публичной офертой',
style: TextStyle(fontSize: 12, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/oferta_bs.pdf')),
),
const TextSpan(text: ' и '),
],
TextSpan(
text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle(fontSize: 12, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/docs/privacy.pdf')),
),
],
),
),
),
const SizedBox(height: 30),
],
);
},
);
}
}

View File

@@ -0,0 +1,98 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SexWidget extends StatelessWidget {
const SexWidget({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.sex, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
InkWell(
onTap: () {
context.read<RegistrationCubitV2>().setGender('male');
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 60,
decoration: BoxDecoration(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
border: state.isGenderValid
? state.gender == 'male'
? Border.all(width: 2, color: appTheme.blueA400)
: null
: Border.all(width: 1, color: appTheme.red),
),
child: Center(
child: Text(
s.male_short,
style: TextStyle(
color: state.gender == 'male' ? appTheme.blueA400 : const Color(0xFF323F4B),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
),
],
),
),
const SizedBox(width: 4),
Expanded(
child: InkWell(
onTap: () {
context.read<RegistrationCubitV2>().setGender('female');
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 60,
decoration: BoxDecoration(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
border: state.isGenderValid
? state.gender == 'female'
? Border.all(width: 2, color: appTheme.blueA400)
: null
: Border.all(width: 1, color: appTheme.red),
),
child: Center(
child: Text(
s.female_short,
style: TextStyle(
color: state.gender == 'female' ? appTheme.blueA400 : const Color(0xFF323F4B),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
],
),
if (!state.isGenderValid) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.gender_error)],
],
);
},
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/presentation/widgets/password_field.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
void showRegistrationBottomSheet(BuildContext context, RegistrationCubitV2 cubit, bool isSuccess) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
child: RegistrationBottomSheet(cubit: cubit, isSuccess: isSuccess),
),
);
}
class RegistrationBottomSheet extends StatefulWidget {
const RegistrationBottomSheet({super.key, required this.cubit, required this.isSuccess});
final RegistrationCubitV2 cubit;
final bool isSuccess;
@override
State<RegistrationBottomSheet> createState() => _RegistrationBottomSheetState();
}
class _RegistrationBottomSheetState extends State<RegistrationBottomSheet> {
late bool isSuccess;
bool isLoading = false;
@override
void initState() {
super.initState();
isSuccess = widget.isSuccess;
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: isSuccess
? [
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
children: [
SvgPicture.asset('assets/icons/sucess.svg', height: 16, width: 16),
const SizedBox(width: 8),
Text(s.registration_completed, style: TextStyles.textFS22FW600.copyWith(color: appTheme.black)),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(s.acc_has_been_created, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.darkGrey)),
),
PasswordField(cubit: widget.cubit),
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Text(s.password_desc, style: TextStyles.textSMedium.copyWith(color: appTheme.darkGrey)),
),
CustomOutlinedButton(
text: s.login,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
},
),
const SizedBox(width: 20),
//button
]
: [
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
children: [
SvgPicture.asset('assets/icons/close_mark.svg', height: 16, width: 16),
const SizedBox(width: 8),
Text(s.signUpFailed, style: TextStyles.textFS22FW600.copyWith(color: appTheme.black)),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(s.error_desc, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.darkGrey)),
),
const SizedBox(width: 20),
],
),
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class StepProgress extends StatelessWidget {
const StepProgress({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return LinearProgressIndicator(
value: state.step == 0 ? 0.5 : 1,
borderRadius: BorderRadius.circular(6),
color: appTheme.blueA400,
backgroundColor: appTheme.greyD9,
minHeight: 8,
);
},
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class StepWidget extends StatelessWidget {
const StepWidget({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<RegistrationCubitV2, RegistrationStateV2>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Text(s.step_widget(state.step + 1), style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
);
},
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/registration_v2/models/registration_model.dart';
import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
class RegistrationRepositoryV2 {
final RegistrationServiceV2 registrationService;
RegistrationRepositoryV2({required this.registrationService});
Future<String?> register({required RegistrationModel registrationModel}) async {
try {
final response = await registrationService.register(model: registrationModel.toJson());
return response['token'];
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
part 'registration_service_v2.g.dart';
@RestApi()
abstract class RegistrationServiceV2 {
factory RegistrationServiceV2(Dio dio, {String baseUrl}) = _RegistrationServiceV2;
@POST('/api/register')
Future<Map<String, String>> register({
@Body() required Map<String, dynamic> model,
});
}

View File

@@ -0,0 +1,44 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_account_model.freezed.dart';
part 'oauth_account_model.g.dart';
@freezed
abstract class OAuthAccountModel with _$OAuthAccountModel {
const factory OAuthAccountModel({
@JsonKey(name: 'oauth_id') required String oauthId,
required String name,
required String email,
required String phone,
@JsonKey(name: 'linked_at') required DateTime linkedAt,
}) = _OAuthAccountModel;
factory OAuthAccountModel.fromJson(Map<String, dynamic> json) => _$OAuthAccountModelFromJson(json);
}
@freezed
abstract class OAuthAccountsResponseModel with _$OAuthAccountsResponseModel {
const factory OAuthAccountsResponseModel({required bool success, required OAuthAccountsDataModel data}) = _OAuthAccountsResponseModel;
factory OAuthAccountsResponseModel.fromJson(Map<String, dynamic> json) => _$OAuthAccountsResponseModelFromJson(json);
}
@freezed
abstract class OAuthAccountsDataModel with _$OAuthAccountsDataModel {
const factory OAuthAccountsDataModel({
@JsonKey(fromJson: _oauthAccountFromJson) OAuthAccountModel? vk,
@JsonKey(fromJson: _oauthAccountFromJson) OAuthAccountModel? yandex,
}) = _OAuthAccountsDataModel;
factory OAuthAccountsDataModel.fromJson(Map<String, dynamic> json) => _$OAuthAccountsDataModelFromJson(json);
}
// Custom converter to handle both empty array [] and object
OAuthAccountModel? _oauthAccountFromJson(dynamic json) {
if (json == null) return null;
if (json is List && json.isEmpty) return null;
if (json is Map<String, dynamic>) {
return OAuthAccountModel.fromJson(json);
}
return null;
}

View File

@@ -0,0 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_attach_request.freezed.dart';
part 'oauth_attach_request.g.dart';
@freezed
abstract class OAuthAttachRequest with _$OAuthAttachRequest {
const factory OAuthAttachRequest({
required String provider,
@JsonKey(name: 'client_id') String? clientId,
@JsonKey(name: 'access_token') required String accessToken,
@JsonKey(name: 'refresh_token') required String refreshToken,
}) = _OAuthAttachRequest;
factory OAuthAttachRequest.fromJson(Map<String, dynamic> json) => _$OAuthAttachRequestFromJson(json);
}

View File

@@ -4,6 +4,8 @@ import 'package:eatvio_app/features/consultations/repositories/consultations_rep
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
@@ -26,6 +28,7 @@ class SettingsCubit extends Cubit<SettingsState> {
final profile = await profileRepository.getMyProfile();
late final ConsultationRequestModel? consultantInfo;
late final bool hasActiveConsultation;
late final OAuthAccountsDataModel? oauthAccounts;
try {
if (profile.phoneConfirmationState.contains('confirmed')) {
@@ -43,6 +46,13 @@ class SettingsCubit extends Cubit<SettingsState> {
hasActiveConsultation = false;
}
try {
final oauthResponse = await settingsRepository.getOAuthAccounts();
oauthAccounts = oauthResponse.data;
} catch (e) {
oauthAccounts = null;
}
try {
emit(
SettingsState.loaded(
@@ -51,6 +61,7 @@ class SettingsCubit extends Cubit<SettingsState> {
profile: profile,
consultationRequestModel: consultantInfo,
hasActiveConsultation: hasActiveConsultation,
oauthAccounts: oauthAccounts,
),
);
} catch (e) {
@@ -87,6 +98,35 @@ class SettingsCubit extends Cubit<SettingsState> {
}
}
Future<void> linkOAuthAccount({
required String provider,
required String accessToken,
required String refreshToken,
String? clientId,
}) async {
try {
final request = OAuthAttachRequest(
provider: provider,
clientId: clientId,
accessToken: accessToken,
refreshToken: refreshToken,
);
await settingsRepository.linkOAuthAccount(request: request);
await init();
} catch (e) {
rethrow;
}
}
Future<void> unlinkOAuthAccount({required String provider}) async {
try {
await settingsRepository.unlinkOAuthAccount(provider: provider);
await init();
} catch (e) {
rethrow;
}
}
Future<bool> _checkActiveConsultation() async {
try {
final userPurchases = await consultationsRepository.getUserPurchases(null, null);

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -17,6 +18,7 @@ abstract class SettingsState with _$SettingsState {
required ProfileModel profile,
ConsultationRequestModel? consultationRequestModel,
required bool hasActiveConsultation,
OAuthAccountsDataModel? oauthAccounts,
}) = SettingsStateLoaded;
const factory SettingsState.error({

View File

@@ -1,7 +1,9 @@
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class PhysicalActivityScreen extends StatefulWidget {
@@ -16,138 +18,176 @@ class PhysicalActivityScreen extends StatefulWidget {
enum PhysicalActivity { minimum, low, medium, high, extreme }
class _PhysicalActivityScreenState extends State<PhysicalActivityScreen> {
late PhysicalActivity? _physicalActivity;
PhysicalActivity? _physicalActivity;
@override
void initState() {
super.initState();
switch (widget.settingsEntity.profileDetailsModel.activityCoefficient) {
PhysicalActivity? _getPhysicalActivityFromCoefficient(double coefficient) {
switch (coefficient) {
case 1.2:
_physicalActivity = PhysicalActivity.minimum;
break;
return PhysicalActivity.minimum;
case 1.375:
_physicalActivity = PhysicalActivity.low;
break;
return PhysicalActivity.low;
case 1.55:
_physicalActivity = PhysicalActivity.medium;
break;
return PhysicalActivity.medium;
case 1.7:
_physicalActivity = PhysicalActivity.high;
break;
return PhysicalActivity.high;
case 1.9:
_physicalActivity = PhysicalActivity.extreme;
break;
return PhysicalActivity.extreme;
default:
return null;
}
}
void _updatePhysicalActivityFromState() {
final state = widget.settingsEntity.settingsCubit.state;
if (state is SettingsStateLoaded) {
final newActivity = _getPhysicalActivityFromCoefficient(state.profileDetails.activityCoefficient);
if (newActivity != _physicalActivity) {
setState(() {
_physicalActivity = newActivity;
});
}
}
}
@override
void initState() {
super.initState();
_updatePhysicalActivityFromState();
_physicalActivity ??= _getPhysicalActivityFromCoefficient(widget.settingsEntity.profileDetailsModel.activityCoefficient);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Update when returning to this screen
_updatePhysicalActivityFromState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Физическая нагрузка')),
bottomSheet: _saveButton(),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 16, bottom: 80),
child: Column(
children: [
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.minimum,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8, right: 16),
child: Text('1,2 — минимальный (сидячая работа, отсутствие физических нагрузок);'),
return BlocListener<SettingsCubit, SettingsState>(
bloc: widget.settingsEntity.settingsCubit,
listener: (context, state) {
_updatePhysicalActivityFromState();
},
child: BlocBuilder<SettingsCubit, SettingsState>(
bloc: widget.settingsEntity.settingsCubit,
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: const Text('Физическая нагрузка')),
bottomSheet: _saveButton(),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 16, bottom: 80),
child: Column(
children: [
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.minimum,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8, right: 16),
child: Text('1,2 — минимальный (сидячая работа, отсутствие физических нагрузок);'),
),
),
],
),
),
],
),
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.low,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(padding: EdgeInsets.only(left: 8, right: 16), child: Text('1,375 — низкий (тренировки не менее 20 мин 1–3 раза в неделю);')),
),
],
),
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.medium,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(padding: EdgeInsets.only(left: 8, right: 16), child: Text('1,55 — умеренный (тренировки 30–60 мин 3–4 раза в неделю);')),
),
],
),
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.high,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8, right: 16),
child: Text('1,7 — высокий (тренировки 30–60 мин 5–7 раза в неделю; тяжёлая физическая работа);'),
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.low,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8, right: 16),
child: Text('1,375 — низкий (тренировки не менее 20 мин 1–3 раза в неделю);'),
),
),
],
),
),
],
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.medium,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(padding: EdgeInsets.only(left: 8, right: 16), child: Text('1,55 — умеренный (тренировки 30–60 мин 3–4 раза в неделю);')),
),
],
),
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.high,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8, right: 16),
child: Text('1,7 — высокий (тренировки 30–60 мин 5–7 раза в неделю; тяжёлая физическая работа);'),
),
),
],
),
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.extreme,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8, right: 16),
child: Text('1,9 — экстремальный (несколько интенсивных тренировок в день'),
),
),
],
),
],
),
),
Row(
children: [
Radio<PhysicalActivity>(
value: PhysicalActivity.extreme,
// ignore: deprecated_member_use
groupValue: _physicalActivity,
// ignore: deprecated_member_use
onChanged: (PhysicalActivity? value) {
setState(() {
_physicalActivity = value;
});
},
),
const Expanded(
child: Padding(padding: EdgeInsets.only(left: 8, right: 16), child: Text('1,9 — экстремальный (несколько интенсивных тренировок в день')),
),
],
),
],
),
);
},
),
);
}

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
@@ -16,6 +17,7 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
class SettingsScreen extends StatefulWidget {
@@ -39,12 +41,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
return AlertDialog(
title: const Text('Предупреждение'),
content: const Text('Во время консультации этот параметр нельзя отключить'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Понятно'),
),
],
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Понятно'))],
);
},
);
@@ -74,6 +71,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
physics: const BouncingScrollPhysics(),
child: Column(
children: [
_oauthAccounts(state, settingsCubit),
const SizedBox(height: 12),
_additionalInfo(state),
const SizedBox(height: 12),
_notifications(state),
@@ -328,6 +327,326 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
);
}
Widget _oauthAccounts(SettingsStateLoaded state, SettingsCubit settingsCubit) {
final isVkLinked = state.oauthAccounts?.vk != null;
final isYandexLinked = state.oauthAccounts?.yandex != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Привязанные аккаунты',
style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_OAuthAccountButton(
onTap: () => _onVkPressed(state, settingsCubit),
backgroundColor: const Color(0xFF2787F5),
tooltip: 'VK ID',
icon: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24),
isLinked: isVkLinked,
accountName: state.oauthAccounts?.vk?.name,
),
const SizedBox(width: 12),
_OAuthAccountButton(
onTap: () => _onOkPressed(state, settingsCubit),
backgroundColor: const Color(0xFFF58220),
tooltip: 'Одноклассники',
icon: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24),
isLinked: isVkLinked,
accountName: state.oauthAccounts?.vk?.name,
),
const SizedBox(width: 12),
_OAuthAccountButton(
onTap: () => _onMailRuPressed(state, settingsCubit),
backgroundColor: const Color(0xFF005FF9),
tooltip: 'Mail.ru',
icon: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24),
isLinked: isVkLinked,
accountName: state.oauthAccounts?.vk?.name,
),
const SizedBox(width: 12),
_OAuthAccountButton(
onTap: () => _onYandexPressed(state, settingsCubit),
backgroundColor: const Color(0xFFFFCC00),
tooltip: 'Яндекс ID',
icon: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24),
isLinked: isYandexLinked,
accountName: state.oauthAccounts?.yandex?.name,
),
],
),
),
const SizedBox(height: 12),
],
);
}
Future<void> _onVkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
if (state.oauthAccounts?.vk != null) {
// Отвязываем аккаунт
if (!mounted) return;
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'VK ID');
if (shouldUnlink == true) {
try {
await settingsCubit.unlinkOAuthAccount(provider: 'vk');
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
}
}
}
return;
}
try {
if (!mounted) return;
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
if (authResult == null) return;
try {
await settingsCubit.linkOAuthAccount(
provider: 'vk',
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken ?? '',
clientId: authResult.clientId,
);
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
}
}
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
}
}
}
Future<void> _onOkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
if (state.oauthAccounts?.vk != null) {
// Отвязываем аккаунт (OK использует тот же VK ID провайдер)
if (!mounted) return;
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Одноклассники');
if (shouldUnlink == true) {
try {
await settingsCubit.unlinkOAuthAccount(provider: 'vk');
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
}
}
}
return;
}
try {
if (!mounted) return;
final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile();
if (authResult == null) return;
try {
await settingsCubit.linkOAuthAccount(
provider: 'vk',
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken ?? '',
clientId: authResult.clientId,
);
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
}
}
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
}
}
}
Future<void> _onMailRuPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
if (state.oauthAccounts?.vk != null) {
// Отвязываем аккаунт (Mail.ru использует тот же VK ID провайдер)
if (!mounted) return;
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Mail.ru');
if (shouldUnlink == true) {
try {
await settingsCubit.unlinkOAuthAccount(provider: 'vk');
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
}
}
}
return;
}
try {
if (!mounted) return;
final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
if (authResult == null) return;
try {
await settingsCubit.linkOAuthAccount(
provider: 'vk',
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken ?? '',
clientId: authResult.clientId,
);
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
}
}
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт');
}
}
}
Future<void> _onYandexPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
if (state.oauthAccounts?.yandex != null) {
// Отвязываем аккаунт
if (!mounted) return;
final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Яндекс ID');
if (shouldUnlink == true) {
try {
await settingsCubit.unlinkOAuthAccount(provider: 'yandex');
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
}
}
}
return;
}
try {
if (!mounted) return;
final authResult = await SocialAuthFacade.signInWithYandex();
if (authResult == null) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось авторизоваться через Яндекс');
}
return;
}
// Проверяем, что refreshToken не пустой для Яндекс
if (authResult.refreshToken == null || authResult.refreshToken!.isEmpty) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось получить токен обновления от Яндекс');
}
return;
}
// Проверяем, что clientId не пустой для Яндекс
if (authResult.clientId.isEmpty) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось получить client ID от Яндекс');
}
return;
}
try {
await settingsCubit.linkOAuthAccount(
provider: 'yandex',
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken!,
clientId: authResult.clientId,
);
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}');
}
}
} catch (e) {
if (mounted) {
showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}');
}
}
}
Future<bool?> _showUnlinkConfirmationDialog(BuildContext context, String providerName) {
return showDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text('Отвязать аккаунт'),
content: Text('Вы точно хотите отвязать аккаунт $providerName?'),
actions: [
CupertinoDialogAction(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('Отмена'),
),
CupertinoDialogAction(
onPressed: () {
Navigator.of(context).pop(true);
},
isDestructiveAction: true,
child: const Text('Отвязать'),
),
],
),
);
}
}
class _OAuthAccountButton extends StatelessWidget {
const _OAuthAccountButton({
required this.onTap,
required this.icon,
required this.backgroundColor,
required this.tooltip,
required this.isLinked,
this.accountName,
});
final VoidCallback onTap;
final Widget icon;
final Color backgroundColor;
final String tooltip;
final bool isLinked;
final String? accountName;
@override
Widget build(BuildContext context) {
return Expanded(
child: Opacity(
opacity: isLinked ? 0.5 : 1.0,
child: Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Ink(
height: 48,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFDBDBDB), width: 1),
),
child: Center(child: icon),
),
),
),
),
);
}
}
class DeleteAccountBottomSheet extends StatefulWidget {

View File

@@ -1,4 +1,6 @@
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
@@ -15,25 +17,41 @@ class SettingsRepository {
}
}
Future<ProfileSettingsModel> updateProfileSettings({
required ProfileSettingsModel profileSettingsModel,
}) async {
Future<ProfileSettingsModel> updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async {
try {
return await settingsService.updateProfileSettings(
data: profileSettingsModel.toJson(),
);
return await settingsService.updateProfileSettings(data: profileSettingsModel.toJson());
} catch (e) {
rethrow;
}
}
Future<ProfileDetailsModel> updateProfileDetails({
required ProfileDetailsModel profileDetailsModel,
}) async {
Future<ProfileDetailsModel> updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async {
try {
return await settingsService.updateProfileDetails(
data: profileDetailsModel.toJson(),
);
return await settingsService.updateProfileDetails(data: profileDetailsModel.toJson());
} catch (e) {
rethrow;
}
}
Future<OAuthAccountsResponseModel> getOAuthAccounts() async {
try {
return await settingsService.getOAuthAccounts();
} catch (e) {
rethrow;
}
}
Future<void> linkOAuthAccount({required OAuthAttachRequest request}) async {
try {
return await settingsService.linkOAuthAccount(request);
} catch (e) {
rethrow;
}
}
Future<void> unlinkOAuthAccount({required String provider}) async {
try {
return await settingsService.unlinkOAuthAccount(provider);
} catch (e) {
rethrow;
}

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