forked from eatvio/eatvio_app
Compare commits
272 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a492c2d964 | ||
| 60753a13d2 | |||
|
|
50deaeddd1 | ||
|
|
56fb2f1bac | ||
|
|
24185cc875 | ||
|
|
9e7bf258d0 | ||
|
|
098633b3ac | ||
|
|
c3aa77b02d | ||
|
|
2d5384f2ec | ||
|
|
1c7e494f90 | ||
|
|
8bbe2613d2 | ||
|
|
5f6ceab6a1 | ||
|
|
250a3f5d73 | ||
|
|
3e0d8b6c27 | ||
|
|
9ed3b37272 | ||
|
|
ae628a6ecc | ||
|
|
88d8ec52d9 | ||
|
|
937921e26f | ||
|
|
d324bd6540 | ||
|
|
5766e216c8 | ||
|
|
ee9b97b694 | ||
|
|
31eec6c9d7 | ||
|
|
5fae3eb5e8 | ||
|
|
b0a167b9e9 | ||
|
|
0b2e026d1d | ||
|
|
0641513ebe | ||
|
|
be0a99b164 | ||
|
|
6dab0d159b | ||
|
|
5167766dbf | ||
| cc79e40df9 | |||
|
|
cdc5936d3c | ||
|
|
67e770f5d0 | ||
|
|
6c4104e9ea | ||
|
|
6d1bcb85fb | ||
|
|
9753644570 | ||
|
|
75983818d1 | ||
|
|
5b149e2fd2 | ||
|
|
699148b202 | ||
|
|
6e7894b911 | ||
|
|
d4f41fe6cf | ||
|
|
b7804db49d | ||
|
|
e90a466a5b | ||
|
|
5f30aaeb2a | ||
|
|
516dfa4158 | ||
|
|
ae07851396 | ||
|
|
34eccb7db5 | ||
|
|
042cc0ea20 | ||
|
|
f81aebe9d8 | ||
|
|
d70aaf955f | ||
|
|
c91e83adaf | ||
|
|
531f732b1a | ||
|
|
d10f2f1356 | ||
|
|
bf110b70c1 | ||
|
|
97418e11d4 | ||
|
|
225077907f | ||
|
|
296adc8229 | ||
|
|
7dd7e3b488 | ||
|
|
a496edaf4c | ||
|
|
400cc004ef | ||
|
|
beda152df0 | ||
|
|
42ffde820d | ||
|
|
adb7b0bd74 | ||
|
|
a768eefed0 | ||
|
|
0ac50d6d18 | ||
|
|
a0d8fda50d | ||
|
|
1d286fa8d9 | ||
|
|
c2a5e7305e | ||
|
|
bbb1b28692 | ||
|
|
21a65caea9 | ||
|
|
eac366e9ae | ||
|
|
278f4d7e10 | ||
|
|
ac5504a514 | ||
|
|
843e1ce4dd | ||
|
|
db6db89567 | ||
|
|
044d86e710 | ||
|
|
4ff880e079 | ||
|
|
05b9ca17c7 | ||
|
|
a1c0c2694b | ||
|
|
2d7407ce99 | ||
| 62fe5ebfa0 | |||
|
|
3dc36ab52d | ||
| 190e212eb4 | |||
|
|
cf3d1a945c | ||
|
|
c54915f45d | ||
|
|
b188570882 | ||
|
|
d0794670f6 | ||
|
|
3b53ed6f7d | ||
|
|
a003814265 | ||
|
|
e99465a550 | ||
|
|
a01f3914c4 | ||
|
|
15f9149cfd | ||
|
|
79df8c5f9a | ||
|
|
a93da1b505 | ||
|
|
8144013747 | ||
|
|
7bac2a13ac | ||
|
|
29589ef6fd | ||
|
|
97a6fe57e9 | ||
|
|
8309498d9e | ||
|
|
aa098b31bc | ||
|
|
7a85c65918 | ||
|
|
6ff35fd162 | ||
|
|
6811b325be | ||
|
|
92205c09d6 | ||
| 008dba9fed | |||
|
|
55ca558bfb | ||
|
|
91499ae207 | ||
|
|
4bacdf5e76 | ||
|
|
e76c1aed20 | ||
|
|
de8f879f59 | ||
|
|
f6809a06b3 | ||
|
|
775d4fb80f | ||
|
|
64e9df69f7 | ||
|
|
6ccfcdedda | ||
|
|
d3c8034b8f | ||
|
|
44ede73ceb | ||
| 2c63990afc | |||
|
|
eb69b253bc | ||
|
|
f26886a7c5 | ||
|
|
13ccf56df0 | ||
|
|
c0032f7a95 | ||
|
|
4852d2cf10 | ||
|
|
41e96c63c6 | ||
|
|
c786821275 | ||
|
|
7b425e56f1 | ||
|
|
bf2ba1fbda | ||
|
|
d206e7ec12 | ||
|
|
a778257215 | ||
|
|
497ff19cfa | ||
|
|
bb59552e3e | ||
|
|
55b2bb0436 | ||
|
|
90746d0be7 | ||
|
|
d70d172c69 | ||
|
|
4e23992d22 | ||
|
|
7a846ed218 | ||
|
|
fcc2936c22 | ||
|
|
b11cf2c0b0 | ||
|
|
0ead14bca5 | ||
|
|
08e56d9777 | ||
|
|
99659f7ea1 | ||
|
|
e158d812e4 | ||
|
|
3d774267ca | ||
|
|
aeb89a9aba | ||
|
|
a4f5bfdb4d | ||
|
|
5b705dfb72 | ||
|
|
a792dcd163 | ||
|
|
9c7ca14017 | ||
|
|
38118055a5 | ||
|
|
5d6e25858b | ||
|
|
42b443c3ba | ||
|
|
21f7c40cde | ||
|
|
f56092d88f | ||
|
|
2b97c0f57d | ||
|
|
a1304e4612 | ||
|
|
7b534cf891 | ||
|
|
22b2e5f249 | ||
|
|
5dfdbe4543 | ||
|
|
5c9a9c22ba | ||
|
|
4f920359c8 | ||
|
|
2ab3bc1cf0 | ||
|
|
91bd8f9d68 | ||
| 9c6f6bdfb1 | |||
|
|
fe1e81e455 | ||
|
|
b7e064a560 | ||
|
|
f1b7a24044 | ||
|
|
cb441b4027 | ||
| 7282fea59a | |||
|
|
4adec4dbbc | ||
|
|
d5b0e972a5 | ||
|
|
391efeb8dd | ||
| d115af6f8e | |||
|
|
2408443a26 | ||
|
|
c378c7fc16 | ||
|
|
070a3e39e7 | ||
|
|
d1753e99f8 | ||
|
|
960afbd6ed | ||
|
|
70e4a42e98 | ||
|
|
04061cb4c1 | ||
|
|
edd064d20d | ||
|
|
cdb7378265 | ||
|
|
746d366c18 | ||
|
|
c2a506cdbf | ||
|
|
abe4985f0e | ||
|
|
ca06b2bd0f | ||
|
|
04c932646d | ||
|
|
3d4363185b | ||
|
|
953230b387 | ||
|
|
9d845e1264 | ||
|
|
32de60742e | ||
|
|
3923b61c67 | ||
|
|
0cd8e662d9 | ||
| 89ea73fc59 | |||
|
|
9e33063c30 | ||
|
|
f3d4478c65 | ||
| 673aa86370 | |||
|
|
1738d27b82 | ||
|
|
d0becb9e11 | ||
|
|
965ae2f02b | ||
|
|
9fcfac87d4 | ||
|
|
2a25be5aa4 | ||
| b420d11996 | |||
|
|
cbeb91a9f0 | ||
|
|
9ca09e8137 | ||
|
|
8917f54e7b | ||
|
|
626f74d20d | ||
|
|
7df32dabd2 | ||
|
|
2e57cd9519 | ||
|
|
c6c42b0525 | ||
|
|
58bb766721 | ||
|
|
0be7392a39 | ||
|
|
4deda92c5e | ||
|
|
b83d91e7f2 | ||
| e5029f581d | |||
|
|
c69fe7f0a6 | ||
| fc0e9b1063 | |||
|
|
453f119317 | ||
|
|
6d0bee4da8 | ||
|
|
04d4be38d4 | ||
| fa22cf5d36 | |||
| 91e8348015 | |||
|
|
1b948b99e3 | ||
|
|
b6eac3c46f | ||
|
|
6fa944b3df | ||
|
|
c31411c4a2 | ||
|
|
6214d19a14 | ||
|
|
db5dddff86 | ||
|
|
39775cd7a8 | ||
|
|
36e2f12e3f | ||
|
|
66d2533add | ||
|
|
425f16bfc9 | ||
|
|
c5e30d8f00 | ||
|
|
d0935fbaae | ||
| dda0aa7440 | |||
|
|
c81327b895 | ||
| 732d667ac1 | |||
|
|
ba9f4a3584 | ||
|
|
f63d901a55 | ||
|
|
9148a4c668 | ||
|
|
a3f0aa3e67 | ||
|
|
8a390c2a9b | ||
|
|
92b37e77b1 | ||
| 101c1e15af | |||
|
|
172c05ac69 | ||
|
|
4eecc458f8 | ||
|
|
4b781adb79 | ||
|
|
dc400ef5b7 | ||
|
|
d1160ba35f | ||
|
|
a46ac5bb4c | ||
|
|
992cb38cdd | ||
|
|
2a06a5476a | ||
|
|
f3853b58d9 | ||
|
|
bbc99ec9f4 | ||
| ff946a5938 | |||
|
|
a72c40d8ee | ||
|
|
6ed7faed15 | ||
|
|
12f51d49ea | ||
|
|
16fccaea8e | ||
|
|
a723673d23 | ||
|
|
89f697f7a6 | ||
| d883c9e19a | |||
|
|
2e3172bc45 | ||
|
|
e147f6e182 | ||
|
|
2440204294 | ||
|
|
46841fb502 | ||
|
|
8e72350152 | ||
|
|
8c2cd5eedd | ||
|
|
996f2ecd70 | ||
|
|
19787f459e | ||
|
|
7e64b24a8c | ||
|
|
a07d085ba7 | ||
|
|
5616bedd25 | ||
|
|
0a3d9e577d | ||
|
|
55e3abec99 |
@@ -3,19 +3,19 @@ name: Eatvio App Android Test
|
|||||||
on: [ pull_request ]
|
on: [ pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
flutter_version: "3.32.0"
|
flutter_version: "3.41.2"
|
||||||
java_version: "21.x"
|
java_version: "21.x"
|
||||||
flutter_channel: "stable"
|
flutter_channel: "stable"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: [ dev-office ]
|
runs-on: [ ubuntu-latest ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: https://github.com/actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: https://github.com/actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
java-version: ${{ env.java_version }}
|
java-version: ${{ env.java_version }}
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Install Android SDK
|
- name: Install Android SDK
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y openjdk-11-jdk wget
|
apt-get install -y openjdk-11-jdk wget xz-utils tar
|
||||||
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
|
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
|
||||||
unzip commandlinetools.zip
|
unzip commandlinetools.zip
|
||||||
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
|
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
apt-get install -y jq
|
apt-get install -y jq
|
||||||
|
|
||||||
- id: flutter-action
|
- id: flutter-action
|
||||||
uses: actions/flutter-action@v2
|
uses: https://github.com/subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: ${{ env.flutter_channel }}
|
channel: ${{ env.flutter_channel }}
|
||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload APK artifact
|
- name: Upload APK artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: https://github.com/actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.APK_NAME }}
|
name: ${{ env.APK_NAME }}
|
||||||
path: ${{ env.APK_NAME }}
|
path: ${{ env.APK_NAME }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker
|
|||||||
on: [ pull_request ]
|
on: [ pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
flutter_version: "3.32.0"
|
flutter_version: "3.41.2"
|
||||||
java_version: "21.x"
|
java_version: "21.x"
|
||||||
flutter_channel: "stable"
|
flutter_channel: "stable"
|
||||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: [ dev-office-flutter ]
|
runs-on: [ dev-office-flutter ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: https://github.com/actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Android environment variables
|
- name: Set up Android environment variables
|
||||||
id: "android-env-setup"
|
id: "android-env-setup"
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
# echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
# echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
# - name: Upload APK artifact
|
# - name: Upload APK artifact
|
||||||
# uses: actions/upload-artifact@v3
|
# uses: https://github.com/actions/upload-artifact@v3
|
||||||
# with:
|
# with:
|
||||||
# name: ${{ env.APK_NAME }}
|
# name: ${{ env.APK_NAME }}
|
||||||
# path: ${{ env.APK_NAME }}
|
# path: ${{ env.APK_NAME }}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ jobs:
|
|||||||
runs-on: [macos-arm64]
|
runs-on: [macos-arm64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: https://github.com/actions/checkout@v2
|
||||||
|
|
||||||
- name: Checkout env repository
|
- name: Checkout env repository
|
||||||
uses: actions/checkout@v2
|
uses: https://github.com/actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: mso7nueh/eatvio_app_env
|
repository: mso7nueh/eatvio_app_env
|
||||||
path: env_repo
|
path: env_repo
|
||||||
@@ -54,11 +54,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Flutter build
|
- name: Flutter build
|
||||||
run: |
|
run: |
|
||||||
|
flutter upgrade
|
||||||
flutter clean
|
flutter clean
|
||||||
flutter pub get
|
flutter pub get
|
||||||
flutter pub upgrade
|
flutter pub upgrade
|
||||||
flutter pub run build_runner build --delete-conflicting-outputs
|
flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
# Configure Flutter for release mode
|
# Configure Flutter for release mode
|
||||||
|
pod repo update
|
||||||
flutter build ios --config-only --release
|
flutter build ios --config-only --release
|
||||||
cd ios
|
cd ios
|
||||||
pod update
|
pod update
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
|||||||
on: [ pull_request ]
|
on: [ pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
flutter_version: "3.35.1"
|
flutter_version: "3.41.2"
|
||||||
java_version: "21.x"
|
java_version: "21.x"
|
||||||
flutter_channel: "stable"
|
flutter_channel: "stable"
|
||||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||||
@@ -11,13 +11,23 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: [ dev-office ]
|
runs-on: [ ubuntu-latest ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: https://github.com/actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set proxy server
|
||||||
|
env:
|
||||||
|
PROXY_USER: ${{ secrets.PROXY_USER }}
|
||||||
|
PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "Setting up proxy..."
|
||||||
|
cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
|
||||||
|
Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Checkout env repository
|
- name: Checkout env repository
|
||||||
uses: actions/checkout@v3
|
uses: https://github.com/actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: mso7nueh/eatvio_app_env
|
repository: mso7nueh/eatvio_app_env
|
||||||
path: env_repo
|
path: env_repo
|
||||||
@@ -56,13 +66,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache Android SDK
|
- name: Cache Android SDK
|
||||||
id: cache-sdk
|
id: cache-sdk
|
||||||
uses: "https://gitea.com/actions/cache@v4"
|
uses: "https://github.com/actions/cache@v4"
|
||||||
with:
|
with:
|
||||||
path: ${{ env.my_local_cache_dir }}
|
path: ${{ env.my_local_cache_dir }}
|
||||||
key: ${{ runner.os }}-cache-sdk
|
key: ${{ runner.os }}-cache-sdk
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: https://github.com/actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
java-version: ${{ env.java_version }}
|
java-version: ${{ env.java_version }}
|
||||||
@@ -71,7 +81,7 @@ jobs:
|
|||||||
- name: Install utils for Android SDK
|
- name: Install utils for Android SDK
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y openjdk-11-jdk wget jq clang cmake ninja-build libgtk-3-dev
|
apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
|
||||||
|
|
||||||
- name: Install Android SDK
|
- name: Install Android SDK
|
||||||
if: steps.cache-sdk.outputs.cache-hit != 'true'
|
if: steps.cache-sdk.outputs.cache-hit != 'true'
|
||||||
@@ -99,16 +109,23 @@ jobs:
|
|||||||
echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV
|
echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV
|
||||||
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
|
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
|
||||||
|
|
||||||
- id: flutter-action
|
|
||||||
uses: actions/flutter-action@v2
|
- name: Set up Flutter
|
||||||
with:
|
run: |
|
||||||
channel: ${{ env.flutter_channel }}
|
FLUTTER_DIR="${{ env.my_local_cache_dir }}flutter"
|
||||||
flutter-version: ${{ env.flutter_version }}
|
if [ ! -f "$FLUTTER_DIR/bin/flutter" ]; then
|
||||||
cache: true
|
echo "Downloading Flutter ${{ env.flutter_version }}..."
|
||||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache
|
curl -fsSL -o /tmp/flutter.tar.xz "https://storage.googleapis.com/flutter_infra_release/releases/${{ env.flutter_channel }}/linux/flutter_linux_${{ env.flutter_version }}-${{ env.flutter_channel }}.tar.xz"
|
||||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
|
echo "Extracting Flutter..."
|
||||||
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache of dart pub get dependencies
|
mkdir -p "${{ env.my_local_cache_dir }}"
|
||||||
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
|
tar xf /tmp/flutter.tar.xz -C "${{ env.my_local_cache_dir }}"
|
||||||
|
rm /tmp/flutter.tar.xz
|
||||||
|
else
|
||||||
|
echo "Flutter already cached at $FLUTTER_DIR"
|
||||||
|
fi
|
||||||
|
echo "$FLUTTER_DIR/bin" >> $GITHUB_PATH
|
||||||
|
echo "$FLUTTER_DIR/bin/cache/dart-sdk/bin" >> $GITHUB_PATH
|
||||||
|
echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
# - name: Print flutter-action outputs
|
# - name: Print flutter-action outputs
|
||||||
# shell: bash
|
# shell: bash
|
||||||
@@ -126,7 +143,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Fix dir permissions
|
- name: Fix dir permissions
|
||||||
run: |
|
run: |
|
||||||
git config --global --add safe.directory /opt/hostedtoolcache/flutter/${{ env.flutter_channel }}-${{ env.flutter_version }}-${{ steps.flutter-action.outputs.ARCHITECTURE }}
|
ARCH=$(uname -m)
|
||||||
|
if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi
|
||||||
|
git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter"
|
||||||
flutter config --android-sdk="$ANDROID_HOME"
|
flutter config --android-sdk="$ANDROID_HOME"
|
||||||
flutter doctor -v
|
flutter doctor -v
|
||||||
|
|
||||||
@@ -140,7 +159,69 @@ jobs:
|
|||||||
run: flutter analyze
|
run: flutter analyze
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: flutter test
|
run: flutter test --coverage
|
||||||
|
|
||||||
|
- name: Test coverage
|
||||||
|
run: |
|
||||||
|
echo "📊 Analyzing test coverage..."
|
||||||
|
echo ""
|
||||||
|
awk -F: '
|
||||||
|
/^SF:/ {
|
||||||
|
file = $0
|
||||||
|
sub(/^SF:/, "", file)
|
||||||
|
sub(/^[[:space:]]+/, "", file)
|
||||||
|
is_generated = 0
|
||||||
|
if (file ~ /\.(g|freezed|gr|mocks)\.dart$/) {
|
||||||
|
is_generated = 1
|
||||||
|
generated_files++
|
||||||
|
} else if (file ~ /\/(build|\.dart_tool)\//) {
|
||||||
|
is_generated = 1
|
||||||
|
generated_files++
|
||||||
|
} else {
|
||||||
|
source_files++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^LF:/ {
|
||||||
|
if (!is_generated) {
|
||||||
|
total += $2
|
||||||
|
} else {
|
||||||
|
generated_lines += $2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^LH:/ {
|
||||||
|
if (!is_generated) {
|
||||||
|
covered += $2
|
||||||
|
} else {
|
||||||
|
generated_covered += $2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
print "📁 Files:"
|
||||||
|
printf " Source files: %d\n", source_files
|
||||||
|
printf " Generated files: %d (excluded)\n", generated_files
|
||||||
|
print ""
|
||||||
|
print "📝 Lines:"
|
||||||
|
printf " Total lines: %d\n", total
|
||||||
|
printf " Covered lines: %d\n", covered
|
||||||
|
printf " Uncovered lines: %d\n", total - covered
|
||||||
|
print ""
|
||||||
|
if (total == 0) {
|
||||||
|
print "⚠️ Coverage: 0%"
|
||||||
|
exit 1
|
||||||
|
} else {
|
||||||
|
percentage = (covered / total) * 100
|
||||||
|
printf "✅ Coverage: %.2f%%\n", percentage
|
||||||
|
print ""
|
||||||
|
if (percentage < 60) {
|
||||||
|
printf "⚠️ Warning: Coverage is below 60%%\n"
|
||||||
|
} else if (percentage < 80) {
|
||||||
|
printf "💛 Good: Coverage is above 60%%\n"
|
||||||
|
} else {
|
||||||
|
printf "💚 Excellent: Coverage is above 80%%\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' coverage/lcov.info
|
||||||
|
|
||||||
- name: Build apk
|
- name: Build apk
|
||||||
run: |
|
run: |
|
||||||
@@ -158,7 +239,7 @@ jobs:
|
|||||||
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload APK artifact
|
- name: Upload APK artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: https://github.com/actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.APK_NAME }}
|
name: ${{ env.APK_NAME }}
|
||||||
path: ${{ env.APK_NAME }}
|
path: ${{ env.APK_NAME }}
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -20,3 +20,7 @@ env/.env
|
|||||||
.env
|
.env
|
||||||
android/key.properties
|
android/key.properties
|
||||||
app/upload-keystore.jks
|
app/upload-keystore.jks
|
||||||
|
coverage/
|
||||||
|
.fvm/
|
||||||
|
.fvmrc
|
||||||
|
/eatvio
|
||||||
3
Makefile
3
Makefile
@@ -8,3 +8,6 @@ run-checks: run-analyse run-tests
|
|||||||
|
|
||||||
build-generated-files:
|
build-generated-files:
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
clean-coverage:
|
||||||
|
@./scripts/remove_generated_from_coverage.sh
|
||||||
55
analyze_coverage.py
Normal file
55
analyze_coverage.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
|
||||||
|
def analyze_coverage():
|
||||||
|
files = {}
|
||||||
|
current_file = None
|
||||||
|
|
||||||
|
with open('coverage/lcov.info', 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i].strip()
|
||||||
|
if line.startswith('SF:'):
|
||||||
|
current_file = line[3:]
|
||||||
|
files[current_file] = {'total': 0, 'covered': 0}
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LF:') and current_file:
|
||||||
|
files[current_file]['total'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LH:') and current_file:
|
||||||
|
files[current_file]['covered'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Filter out generated files
|
||||||
|
filtered = []
|
||||||
|
for f, d in files.items():
|
||||||
|
if not f.endswith('.g.dart') and not 'generated' in f.lower() and not f.endswith('l10n.dart'):
|
||||||
|
filtered.append((f, d))
|
||||||
|
|
||||||
|
# Find files with less than 50% coverage
|
||||||
|
low_coverage = []
|
||||||
|
for f, d in filtered:
|
||||||
|
if d['total'] > 0:
|
||||||
|
pct = d['covered']/d['total']*100
|
||||||
|
if pct < 50:
|
||||||
|
low_coverage.append((f, d))
|
||||||
|
|
||||||
|
# Sort by coverage percentage (lowest first)
|
||||||
|
sorted_files = sorted(
|
||||||
|
low_coverage,
|
||||||
|
key=lambda x: (x[1]['covered']/x[1]['total']*100 if x[1]['total'] > 0 else 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
print('Files with less than 50% coverage:')
|
||||||
|
print('=' * 80)
|
||||||
|
for f, d in sorted_files[:50]:
|
||||||
|
pct = d['covered']/d['total']*100
|
||||||
|
print(f'{pct:6.1f}% ({d["covered"]:4d}/{d["total"]:4d}) - {f}')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
analyze_coverage()
|
||||||
|
|
||||||
52
analyze_coverage_by_uncovered.py
Normal file
52
analyze_coverage_by_uncovered.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
|
||||||
|
def analyze_coverage_by_uncovered():
|
||||||
|
files = {}
|
||||||
|
current_file = None
|
||||||
|
|
||||||
|
with open('coverage/lcov.info', 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i].strip()
|
||||||
|
if line.startswith('SF:'):
|
||||||
|
current_file = line[3:]
|
||||||
|
files[current_file] = {'total': 0, 'covered': 0}
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LF:') and current_file:
|
||||||
|
files[current_file]['total'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LH:') and current_file:
|
||||||
|
files[current_file]['covered'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Filter out generated files and UI screens/widgets (hard to test)
|
||||||
|
filtered = []
|
||||||
|
exclude_patterns = ['.g.dart', 'generated', 'l10n.dart', '/pages/', '/widgets/', '_screen.dart', '_widget.dart']
|
||||||
|
|
||||||
|
for f, d in files.items():
|
||||||
|
if d['total'] > 0:
|
||||||
|
# Skip UI files
|
||||||
|
should_exclude = any(pattern in f for pattern in exclude_patterns)
|
||||||
|
if not should_exclude:
|
||||||
|
uncovered = d['total'] - d['covered']
|
||||||
|
pct = d['covered']/d['total']*100
|
||||||
|
filtered.append((f, d, uncovered, pct))
|
||||||
|
|
||||||
|
# Sort by uncovered lines (most uncovered first)
|
||||||
|
sorted_files = sorted(filtered, key=lambda x: x[2], reverse=True)
|
||||||
|
|
||||||
|
print('Files sorted by uncovered lines (excluding UI screens/widgets):')
|
||||||
|
print('=' * 100)
|
||||||
|
print(f'{"Uncovered":>10} {"Covered":>8} {"Total":>8} {"%":>7} {"File":<60}')
|
||||||
|
print('=' * 100)
|
||||||
|
for f, d, uncovered, pct in sorted_files[:30]:
|
||||||
|
print(f'{uncovered:>10} {d["covered"]:>8} {d["total"]:>8} {pct:>6.1f}% {f}')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
analyze_coverage_by_uncovered()
|
||||||
|
|
||||||
52
analyze_output.txt
Normal file
52
analyze_output.txt
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
Files with less than 50% coverage:
|
||||||
|
================================================================================
|
||||||
|
0.0% ( 0/ 37) - lib/core/firebase_api.dart
|
||||||
|
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_confirm_request.dart
|
||||||
|
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_confirm_response.dart
|
||||||
|
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_request_response.dart
|
||||||
|
0.0% ( 0/ 342) - lib/features/dev_tools/test_cases.dart
|
||||||
|
0.9% ( 2/ 220) - lib/features/dev_tools/presentation/dev_tools_screen.dart
|
||||||
|
2.4% ( 1/ 42) - lib/features/auth/presentation/pages/scan_qr_code_screen.dart
|
||||||
|
2.9% ( 1/ 34) - lib/features/diary/presentation/pages/barcode_scanner_screen.dart
|
||||||
|
3.6% ( 1/ 28) - lib/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart
|
||||||
|
3.8% ( 1/ 26) - lib/features/update_screen/presentation/update_screen.dart
|
||||||
|
4.4% ( 4/ 90) - lib/core/widgets/custom_popup.dart
|
||||||
|
5.9% ( 1/ 17) - lib/features/recipe/presentation/pages/my_recipes_screen.dart
|
||||||
|
6.2% ( 2/ 32) - lib/features/consultations/presentation/pages/consultations_screen.dart
|
||||||
|
7.1% ( 1/ 14) - lib/features/menu/presentation/pages/blocked_users_screen.dart
|
||||||
|
7.7% ( 1/ 13) - lib/features/registration_v2/presentation/pages/registration_v2_page.dart
|
||||||
|
8.3% ( 18/ 217) - lib/features/blog_post/presentation/pages/blog_post_screen.dart
|
||||||
|
11.1% ( 1/ 9) - lib/features/registration_v2/presentation/widgets/step_progress.dart
|
||||||
|
11.8% ( 14/ 119) - lib/features/auth/sdk/social_auth_facade.dart
|
||||||
|
13.0% ( 3/ 23) - lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart
|
||||||
|
14.5% ( 11/ 76) - lib/core/app_router.dart
|
||||||
|
19.3% ( 11/ 57) - lib/features/create_recipe/presentation/widgets/image_widget.dart
|
||||||
|
19.6% ( 10/ 51) - lib/features/create_recipe/entities/recipe_create_entity.dart
|
||||||
|
19.7% ( 12/ 61) - lib/features/create_recipe/presentation/widgets/step_image_widget.dart
|
||||||
|
20.0% ( 6/ 30) - lib/core/image_compressing/image_compress_utility.dart
|
||||||
|
20.0% ( 57/ 285) - lib/features/complaining/presentation/complaining_bottom_sheet.dart
|
||||||
|
21.9% ( 7/ 32) - lib/core/routing/content_routes.dart
|
||||||
|
25.6% ( 46/ 180) - lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart
|
||||||
|
26.8% ( 11/ 41) - lib/features/create_recipe/reporitories/recipe_repository.dart
|
||||||
|
27.4% ( 96/ 351) - lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
|
||||||
|
28.0% ( 28/ 100) - lib/core/utils/image_constant.dart
|
||||||
|
28.4% ( 23/ 81) - lib/core/websockets/ws_manager.dart
|
||||||
|
29.0% ( 157/ 541) - lib/features/profile/presentation/pages/profile_screen.dart
|
||||||
|
29.3% ( 137/ 468) - lib/features/notifications/models/notification_model.dart
|
||||||
|
33.1% ( 47/ 142) - lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart
|
||||||
|
33.3% ( 9/ 27) - lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart
|
||||||
|
35.2% ( 58/ 165) - lib/features/registration/presentation/pages/registration_screen.dart
|
||||||
|
35.6% ( 36/ 101) - lib/features/chat/presentation/pages/empty_chat_screen.dart
|
||||||
|
36.0% ( 9/ 25) - lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart
|
||||||
|
36.7% ( 11/ 30) - lib/core/routing/main_navigation_routes.dart
|
||||||
|
37.5% ( 3/ 8) - lib/core/permission_handler/permission_handler.dart
|
||||||
|
38.6% ( 22/ 57) - lib/features/registration/presentation/pages/date_of_birth_screen.dart
|
||||||
|
38.9% ( 35/ 90) - lib/core/routing/other_routes.dart
|
||||||
|
39.0% ( 30/ 77) - lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart
|
||||||
|
39.3% ( 119/ 303) - lib/features/settings/presentation/pages/settings_screen.dart
|
||||||
|
39.3% ( 22/ 56) - lib/features/consultant/become_consultant/presentation/pages/photo_or_scans_screen.dart
|
||||||
|
40.0% ( 4/ 10) - lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart
|
||||||
|
40.0% ( 4/ 10) - lib/features/registration/services/registration_analytics_service.dart
|
||||||
|
41.5% ( 95/ 229) - lib/features/create_diet/presentation/cubit/create_diet_cubit.dart
|
||||||
|
41.9% ( 31/ 74) - lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart
|
||||||
|
42.7% ( 67/ 157) - lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart
|
||||||
@@ -1 +1,94 @@
|
|||||||
{"agcgw":{"url":"connect-drru.hispace.dbankcloud.ru","backurl":"connect-drru.hispace.dbankcloud.cn","websocketurl":"connect-ws-drru.hispace.dbankcloud.ru","websocketbackurl":"connect-ws-drru.hispace.dbankcloud.cn"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"20007000036693451","product_id":"461323198429446894","client_id":"1595824543740483264","client_secret":"2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE","project_id":"461323198429446894","app_id":"113238569","api_key":"DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==","package_name":"com.eatvioapp.app"},"oauth_client":{"client_id":"113238569","client_type":1},"app_info":{"app_id":"113238569","package_name":"com.eatvioapp.app"},"service":{"analytics":{"collector_url":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drru.cloud.huawei.ru"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"RU","configuration_version":"3.0","appInfos":[{"package_name":"com.eatvioapp.app","client":{"app_id":"113238569"},"app_info":{"package_name":"com.eatvioapp.app","app_id":"113238569"},"oauth_client":{"client_type":1,"client_id":"113238569"}}]}
|
{
|
||||||
|
"agcgw" : {
|
||||||
|
"url" : "connect-drru.hispace.dbankcloud.ru",
|
||||||
|
"backurl" : "connect-drru.hispace.dbankcloud.cn",
|
||||||
|
"websocketurl" : "connect-ws-drru.hispace.dbankcloud.ru",
|
||||||
|
"websocketbackurl" : "connect-ws-drru.hispace.dbankcloud.cn"
|
||||||
|
},
|
||||||
|
"agcgw_all" : {
|
||||||
|
"SG" : "connect-dra.dbankcloud.cn",
|
||||||
|
"SG_back" : "connect-dra.hispace.hicloud.com",
|
||||||
|
"CN" : "connect-drcn.dbankcloud.cn",
|
||||||
|
"CN_back" : "connect-drcn.hispace.hicloud.com",
|
||||||
|
"RU" : "connect-drru.hispace.dbankcloud.ru",
|
||||||
|
"RU_back" : "connect-drru.hispace.dbankcloud.cn",
|
||||||
|
"DE" : "connect-dre.dbankcloud.cn",
|
||||||
|
"DE_back" : "connect-dre.hispace.hicloud.com"
|
||||||
|
},
|
||||||
|
"websocketgw_all" : {
|
||||||
|
"SG" : "connect-ws-dra.hispace.dbankcloud.cn",
|
||||||
|
"SG_back" : "connect-ws-dra.hispace.dbankcloud.com",
|
||||||
|
"CN" : "connect-ws-drcn.hispace.dbankcloud.cn",
|
||||||
|
"CN_back" : "connect-ws-drcn.hispace.dbankcloud.com",
|
||||||
|
"RU" : "connect-ws-drru.hispace.dbankcloud.ru",
|
||||||
|
"RU_back" : "connect-ws-drru.hispace.dbankcloud.cn",
|
||||||
|
"DE" : "connect-ws-dre.hispace.dbankcloud.cn",
|
||||||
|
"DE_back" : "connect-ws-dre.hispace.dbankcloud.com"
|
||||||
|
},
|
||||||
|
"client" : {
|
||||||
|
"cp_id" : "20007000036693451",
|
||||||
|
"product_id" : "461323198429446894",
|
||||||
|
"client_id" : "1595824543740483264",
|
||||||
|
"client_secret" : "2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE",
|
||||||
|
"project_id" : "461323198429446894",
|
||||||
|
"app_id" : "113238569",
|
||||||
|
"api_key" : "DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==",
|
||||||
|
"package_name" : "com.eatvioapp.app"
|
||||||
|
},
|
||||||
|
"oauth_client" : {
|
||||||
|
"client_id" : "113238569",
|
||||||
|
"client_type" : 1
|
||||||
|
},
|
||||||
|
"app_info" : {
|
||||||
|
"app_id" : "113238569",
|
||||||
|
"package_name" : "com.eatvioapp.app"
|
||||||
|
},
|
||||||
|
"service" : {
|
||||||
|
"analytics" : {
|
||||||
|
"collector_url" : "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||||
|
"collector_url_cn" : "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||||
|
"collector_url_de" : "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||||
|
"collector_url_ru" : "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||||
|
"collector_url_sg" : "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||||
|
"resource_id" : "p1",
|
||||||
|
"channel_id" : ""
|
||||||
|
},
|
||||||
|
"ml" : {
|
||||||
|
"mlservice_url" : "ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"
|
||||||
|
},
|
||||||
|
"cloudstorage" : {
|
||||||
|
"storage_url" : "https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||||
|
"storage_url_ru" : "https://agc-storage-drru.cloud.huawei.ru",
|
||||||
|
"storage_url_sg" : "https://ops-dra.agcstorage.link",
|
||||||
|
"storage_url_de" : "https://ops-dre.agcstorage.link",
|
||||||
|
"storage_url_cn" : "https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||||
|
"storage_url_ru_back" : "https://agc-storage-drru.cloud.huawei.ru",
|
||||||
|
"storage_url_sg_back" : "https://agc-storage-dra.cloud.huawei.asia",
|
||||||
|
"storage_url_de_back" : "https://agc-storage-dre.cloud.huawei.eu",
|
||||||
|
"storage_url_cn_back" : "https://agc-storage-drcn.cloud.huawei.com.cn"
|
||||||
|
},
|
||||||
|
"search" : {
|
||||||
|
"url" : "https://search-drru.cloud.huawei.ru"
|
||||||
|
},
|
||||||
|
"edukit" : {
|
||||||
|
"edu_url" : "edukit.cloud.huawei.com.cn",
|
||||||
|
"dh_url" : "edukit.cloud.huawei.com.cn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"region" : "RU",
|
||||||
|
"configuration_version" : "3.0",
|
||||||
|
"appInfos" : [ {
|
||||||
|
"package_name" : "com.eatvioapp.app",
|
||||||
|
"client" : {
|
||||||
|
"app_id" : "113238569"
|
||||||
|
},
|
||||||
|
"app_info" : {
|
||||||
|
"package_name" : "com.eatvioapp.app",
|
||||||
|
"app_id" : "113238569"
|
||||||
|
},
|
||||||
|
"oauth_client" : {
|
||||||
|
"client_type" : 1,
|
||||||
|
"client_id" : "113238569"
|
||||||
|
}
|
||||||
|
} ]
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ plugins {
|
|||||||
id 'com.google.gms.google-services'
|
id 'com.google.gms.google-services'
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
|
id "kotlin-parcelize"
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
id 'com.huawei.agconnect'
|
id 'com.huawei.agconnect'
|
||||||
}
|
}
|
||||||
@@ -19,15 +20,15 @@ def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property(
|
|||||||
android {
|
android {
|
||||||
namespace = "com.eatvioapp.app"
|
namespace = "com.eatvioapp.app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -68,10 +69,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
|
implementation 'com.huawei.agconnect:agconnect-core:1.9.5.300'
|
||||||
implementation 'com.huawei.hms:push:6.12.0.300'
|
implementation 'com.huawei.hms:push:6.13.0.301'
|
||||||
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
|
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
|
||||||
implementation 'com.huawei.hms:hianalytics:6.12.0.300'
|
implementation 'com.huawei.hms:hianalytics:6.12.0.301'
|
||||||
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
|
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
|
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="push_kit_auto_init_enabled"
|
android:name="push_kit_auto_init_enabled"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.huawei.hms.client.appid"
|
||||||
|
android:value="appid=113238569" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.huawei.hms.client.cpid"
|
||||||
|
android:value="cpid=20007000036693451" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -47,17 +47,21 @@ class MainActivity: FlutterActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appId.isNullOrBlank()) {
|
if (appId.isNullOrBlank()) {
|
||||||
|
android.util.Log.e("HMS", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.")
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
|
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
android.util.Log.i("HMS", "Getting token for appId: $appId")
|
||||||
val token = HmsInstanceId.getInstance(applicationContext)
|
val token = HmsInstanceId.getInstance(applicationContext)
|
||||||
.getToken(appId, "HCM")
|
.getToken(appId, "HCM")
|
||||||
|
android.util.Log.i("HMS", "Token retrieved successfully")
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
result.success(token)
|
result.success(token)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("HMS", "Error getting HMS token: ${e.message}", e)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
result.error("HMS_TOKEN_ERROR", e.message, null)
|
result.error("HMS_TOKEN_ERROR", e.message, null)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
assets/fonts/Mulish-Black.ttf
Normal file
BIN
assets/fonts/Mulish-Black.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-BlackItalic.ttf
Normal file
BIN
assets/fonts/Mulish-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Bold.ttf
Normal file
BIN
assets/fonts/Mulish-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-BoldItalic.ttf
Normal file
BIN
assets/fonts/Mulish-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraBold.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraBoldItalic.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraLight.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraLightItalic.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Italic.ttf
Normal file
BIN
assets/fonts/Mulish-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Light.ttf
Normal file
BIN
assets/fonts/Mulish-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-LightItalic.ttf
Normal file
BIN
assets/fonts/Mulish-LightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Medium.ttf
Normal file
BIN
assets/fonts/Mulish-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-MediumItalic.ttf
Normal file
BIN
assets/fonts/Mulish-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Regular.ttf
Normal file
BIN
assets/fonts/Mulish-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-SemiBold.ttf
Normal file
BIN
assets/fonts/Mulish-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-SemiBoldItalic.ttf
Normal file
BIN
assets/fonts/Mulish-SemiBoldItalic.ttf
Normal file
Binary file not shown.
3
assets/icons/check.svg
Normal file
3
assets/icons/check.svg
Normal 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 |
11
assets/icons/check_filled.svg
Normal file
11
assets/icons/check_filled.svg
Normal 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 |
12
assets/icons/close_mark.svg
Normal file
12
assets/icons/close_mark.svg
Normal 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 |
3
assets/icons/crossed_eye.svg
Normal file
3
assets/icons/crossed_eye.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
3
assets/icons/sucess.svg
Normal file
3
assets/icons/sucess.svg
Normal 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 |
28716
coverage/lcov.info
28716
coverage/lcov.info
File diff suppressed because it is too large
Load Diff
@@ -388,7 +388,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 81;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -406,7 +406,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.3.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||||
@@ -539,7 +539,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 81;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -557,7 +557,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.3.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 81;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -596,7 +596,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.3.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>81</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -48,8 +48,33 @@ platform :ios do
|
|||||||
UI.message "Configuring Flutter for release mode..."
|
UI.message "Configuring Flutter for release mode..."
|
||||||
sh("cd .. && flutter build ios --config-only --release")
|
sh("cd .. && flutter build ios --config-only --release")
|
||||||
|
|
||||||
|
# Read build number from Flutter Generated.xcconfig
|
||||||
|
generated_xcconfig_path = File.join("..", "Flutter", "Generated.xcconfig")
|
||||||
|
flutter_build_number = nil
|
||||||
|
|
||||||
|
if File.exist?(generated_xcconfig_path)
|
||||||
|
File.foreach(generated_xcconfig_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_BUILD_NUMBER=(.*)/)
|
||||||
|
if matches
|
||||||
|
flutter_build_number = matches[1].strip
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if flutter_build_number.nil?
|
||||||
|
UI.user_error!("Could not find FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}")
|
||||||
|
end
|
||||||
|
|
||||||
|
UI.message "Flutter build number: #{flutter_build_number}"
|
||||||
|
|
||||||
match(type: "appstore", readonly: true)
|
match(type: "appstore", readonly: true)
|
||||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
|
||||||
|
# Set build number explicitly instead of incrementing
|
||||||
|
increment_build_number(
|
||||||
|
xcodeproj: "Runner.xcodeproj",
|
||||||
|
build_number: flutter_build_number
|
||||||
|
)
|
||||||
|
|
||||||
build_app(
|
build_app(
|
||||||
workspace: "Runner.xcworkspace",
|
workspace: "Runner.xcworkspace",
|
||||||
@@ -74,7 +99,7 @@ platform :ios do
|
|||||||
skip_submission: true,
|
skip_submission: true,
|
||||||
distribute_external: false,
|
distribute_external: false,
|
||||||
notify_external_testers: false,
|
notify_external_testers: false,
|
||||||
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
|
changelog: "TestFlight build #{flutter_build_number}",
|
||||||
verbose: true
|
verbose: true
|
||||||
)
|
)
|
||||||
rescue => ex
|
rescue => ex
|
||||||
|
|||||||
@@ -33,15 +33,16 @@ class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Список глобальных BlocProvider'ов
|
/// Список глобальных BlocProvider'ов
|
||||||
|
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
|
||||||
List<BlocProvider> get globalBlocProviders => [
|
List<BlocProvider> get globalBlocProviders => [
|
||||||
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
|
BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
|
||||||
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
|
BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
|
||||||
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
|
BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
|
||||||
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
|
BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
|
||||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
|
||||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
|
||||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||||
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
|
BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Делегаты локализации
|
/// Делегаты локализации
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/env_config.dart';
|
import 'package:eatvio_app/core/env_config.dart';
|
||||||
@@ -37,9 +39,19 @@ class AppInitializer {
|
|||||||
|
|
||||||
/// Инициализация Firebase
|
/// Инициализация Firebase
|
||||||
static Future<void> _initializeFirebase() async {
|
static Future<void> _initializeFirebase() async {
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('Firebase initialization timeout');
|
||||||
|
},
|
||||||
|
);
|
||||||
if (sl<AppConfig>().buildType == AppBuildType.prod) {
|
if (sl<AppConfig>().buildType == AppBuildType.prod) {
|
||||||
await FirebaseMessaging.instance.requestPermission();
|
try {
|
||||||
|
await FirebaseMessaging.instance.requestPermission().timeout(const Duration(seconds: 5));
|
||||||
|
} catch (e) {
|
||||||
|
// Разрешение не критично, продолжаем работу
|
||||||
|
log('Failed to request Firebase messaging permission: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +80,10 @@ class AppInitializer {
|
|||||||
options.tracesSampleRate = 1.0;
|
options.tracesSampleRate = 1.0;
|
||||||
options.debug = false;
|
options.debug = false;
|
||||||
}, appRunner: appRunner);
|
}, appRunner: appRunner);
|
||||||
|
FlutterError.onError = (details) async {
|
||||||
|
if (kDebugMode) return;
|
||||||
|
await Sentry.captureException(details.exception, stackTrace: details.stack);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
import 'package:eatvio_app/core/routing/auth_routes.dart';
|
import 'package:eatvio_app/core/routing/auth_routes.dart';
|
||||||
import 'package:eatvio_app/core/routing/content_routes.dart';
|
import 'package:eatvio_app/core/routing/content_routes.dart';
|
||||||
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
|
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
|
||||||
@@ -13,6 +14,9 @@ import 'package:go_router/go_router.dart';
|
|||||||
class AppRouter {
|
class AppRouter {
|
||||||
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
// Домены для deep links
|
||||||
|
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru'];
|
||||||
|
|
||||||
static GoRouter appRouter = GoRouter(
|
static GoRouter appRouter = GoRouter(
|
||||||
initialLocation: RouteConstants.updateScreen,
|
initialLocation: RouteConstants.updateScreen,
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
@@ -27,51 +31,128 @@ class AppRouter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// Обработка редиректов для deep links
|
/// Обработка редиректов для deep links
|
||||||
|
/// Возвращает null для обычной навигации, чтобы не мешать внутренним переходам
|
||||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||||
final segments = state.uri.pathSegments;
|
final uri = state.uri;
|
||||||
if (segments.length == 4) {
|
final host = uri.host.toLowerCase();
|
||||||
final type = segments[1];
|
final segments = uri.pathSegments;
|
||||||
switch (type) {
|
|
||||||
case 'diary':
|
// Проверяем, является ли это deep link по домену
|
||||||
FlutterNativeSplash.remove();
|
final isDeepLink = _deepLinkHosts.contains(host);
|
||||||
return '/diary-screen';
|
|
||||||
}
|
// Если это не deep link, не мешаем обычной навигации
|
||||||
|
if (!isDeepLink) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (segments.length == 3) {
|
|
||||||
|
// Логирование для отладки
|
||||||
|
developer.log(
|
||||||
|
'Deep link detected: host=$host, path=${uri.path}, segments=$segments',
|
||||||
|
name: 'AppRouter',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проверка на пустой путь или некорректный формат
|
||||||
|
if (segments.isEmpty) {
|
||||||
|
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка языка (первый сегмент должен быть ru или en)
|
||||||
|
if (segments[0] != 'ru' && segments[0] != 'en') {
|
||||||
|
developer.log(
|
||||||
|
'Invalid language segment: ${segments[0]}, redirecting to feed',
|
||||||
|
name: 'AppRouter',
|
||||||
|
);
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка deep links с учетом языка
|
||||||
|
// Формат: /{lang}/{type}/{identifier?}
|
||||||
|
if (segments.length == 4) {
|
||||||
|
// Формат: /{lang}/diary/{...}
|
||||||
|
final type = segments[1];
|
||||||
|
|
||||||
|
if (type == 'diary') {
|
||||||
|
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.diaryScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
developer.log(
|
||||||
|
'Unknown deep link type for 4 segments: $type, redirecting to feed',
|
||||||
|
name: 'AppRouter',
|
||||||
|
);
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
} else if (segments.length == 3) {
|
||||||
|
// Формат: /{lang}/{type}/{identifier}
|
||||||
final type = segments[1];
|
final type = segments[1];
|
||||||
final contentIdentifier = segments[2];
|
final contentIdentifier = segments[2];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'recipe':
|
case 'recipe':
|
||||||
|
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '/about-recipes-screen?slug=$contentIdentifier';
|
return '/about-recipes-screen?slug=$contentIdentifier';
|
||||||
case 'blog_post':
|
case 'blog_post':
|
||||||
|
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '/blog-post-screen?id=$contentIdentifier';
|
return '/blog-post-screen?id=$contentIdentifier';
|
||||||
case 'plan':
|
case 'plan':
|
||||||
|
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '/about-plan-screen?slug=$contentIdentifier';
|
return '/about-plan-screen?slug=$contentIdentifier';
|
||||||
case 'diet':
|
case 'diet':
|
||||||
|
developer.log('Deep link: diet/$contentIdentifier', name: 'AppRouter');
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '/about-diet-screen?slug=$contentIdentifier';
|
return '/about-diet-screen?slug=$contentIdentifier';
|
||||||
case 'chat':
|
case 'chat':
|
||||||
|
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '/chat-select-screen';
|
return RouteConstants.chatSelectScreen;
|
||||||
case 'profile':
|
case 'profile':
|
||||||
|
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '/profile-screen?slug=$contentIdentifier';
|
return '/profile-screen?slug=$contentIdentifier';
|
||||||
}
|
case 'diary':
|
||||||
}
|
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
|
||||||
if (segments.length == 2) {
|
|
||||||
final type = segments[1];
|
|
||||||
switch (type) {
|
|
||||||
case 'chat':
|
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
return '/chat-select-screen';
|
return RouteConstants.diaryScreen;
|
||||||
|
default:
|
||||||
|
developer.log(
|
||||||
|
'Unknown deep link type: $type, redirecting to feed',
|
||||||
|
name: 'AppRouter',
|
||||||
|
);
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
}
|
}
|
||||||
}
|
} else if (segments.length == 2) {
|
||||||
|
// Формат: /{lang}/{type} - только для chat
|
||||||
|
final type = segments[1];
|
||||||
|
|
||||||
return null;
|
if (type == 'chat') {
|
||||||
|
developer.log('Deep link: chat', name: 'AppRouter');
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.chatSelectScreen;
|
||||||
|
} else {
|
||||||
|
developer.log(
|
||||||
|
'Unknown deep link type for 2 segments: $type, redirecting to feed',
|
||||||
|
name: 'AppRouter',
|
||||||
|
);
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Неизвестный формат deep link
|
||||||
|
developer.log(
|
||||||
|
'Unknown deep link format: ${segments.length} segments, redirecting to feed',
|
||||||
|
name: 'AppRouter',
|
||||||
|
);
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {
|
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
|
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
|
||||||
import 'package:eatvio_app/core/comments/services/comments_service.dart';
|
import 'package:eatvio_app/core/comments/services/comments_service.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class CommentsRepository {
|
class CommentsRepository {
|
||||||
final CommentsService service;
|
final CommentsService service;
|
||||||
@@ -9,7 +10,8 @@ class CommentsRepository {
|
|||||||
Future<void> deleteComment({required int id}) async {
|
Future<void> deleteComment({required int id}) async {
|
||||||
try {
|
try {
|
||||||
return service.deleteComment(id);
|
return service.deleteComment(id);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +19,8 @@ class CommentsRepository {
|
|||||||
Future<void> updateComment({required int id, required String body}) async {
|
Future<void> updateComment({required int id, required String body}) async {
|
||||||
try {
|
try {
|
||||||
return service.updateComment(id, CommentUpdateModel(body: body));
|
return service.updateComment(id, CommentUpdateModel(body: body));
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,4 +69,9 @@ class AppConstants {
|
|||||||
static const String usersEndpoint = '/users';
|
static const String usersEndpoint = '/users';
|
||||||
static const String postsEndpoint = '/posts';
|
static const String postsEndpoint = '/posts';
|
||||||
static const String commentsEndpoint = '/comments';
|
static const String commentsEndpoint = '/comments';
|
||||||
|
|
||||||
|
// Delay of pending requests
|
||||||
|
/// 30 секунд стоит потому что запрос может идти 12-15 секунд
|
||||||
|
/// Особенно если производится отладка на сервере разработчика
|
||||||
|
static const Duration pendingDelay = Duration(seconds: 30);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class StateUtils {
|
|||||||
if (isError(state)) {
|
if (isError(state)) {
|
||||||
// Попытка извлечь сообщение об ошибке из состояния
|
// Попытка извлечь сообщение об ошибке из состояния
|
||||||
final stateString = state.toString();
|
final stateString = state.toString();
|
||||||
final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString);
|
final errorMatch = RegExp(r'error[:\s]+([^,}]+)', caseSensitive: false).firstMatch(stateString);
|
||||||
return errorMatch?.group(1)?.trim();
|
return errorMatch?.group(1)?.trim();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -39,14 +39,16 @@ 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/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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
import 'package:eatvio_app/features/order_info/cubit/order_info_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/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/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_cubit.dart';
|
||||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_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/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/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/features/user_details/presentation/cubit/user_details_cubit.dart';
|
||||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||||
@@ -74,11 +76,12 @@ class CubitRegistrations {
|
|||||||
/// Регистрация Cubit'ов как фабрик
|
/// Регистрация Cubit'ов как фабрик
|
||||||
static void _registerFactoryCubits() {
|
static void _registerFactoryCubits() {
|
||||||
// Auth related cubits
|
// Auth related cubits
|
||||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
|
||||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
||||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
|
||||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
||||||
|
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
|
||||||
// Feed related cubits
|
// Feed related cubits
|
||||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
||||||
@@ -111,7 +114,7 @@ class CubitRegistrations {
|
|||||||
// User content cubits
|
// User content cubits
|
||||||
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
|
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
|
||||||
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
|
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
|
||||||
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
|
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl(), recipeRepository: sl()));
|
||||||
|
|
||||||
// Other feature cubits
|
// Other feature cubits
|
||||||
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
||||||
@@ -145,5 +148,6 @@ class CubitRegistrations {
|
|||||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
||||||
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
|
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
|
||||||
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
|
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
|
||||||
|
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
|
import 'package:eatvio_app/core/constants/app_constants.dart';
|
||||||
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
|
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
|
||||||
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:sentry_dio/sentry_dio.dart';
|
||||||
|
|
||||||
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
||||||
class ExternalRegistrations {
|
class ExternalRegistrations {
|
||||||
@@ -19,14 +21,21 @@ class ExternalRegistrations {
|
|||||||
/// Регистрация Dio HTTP клиента
|
/// Регистрация Dio HTTP клиента
|
||||||
static void _registerDio() {
|
static void _registerDio() {
|
||||||
sl.registerLazySingleton(
|
sl.registerLazySingleton(
|
||||||
() =>
|
() => Dio(
|
||||||
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
BaseOptions(
|
||||||
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
baseUrl: sl<AppConfig>().baseUrl,
|
||||||
|
headers: {'accept': 'application/json', 'content-type': 'application/json'},
|
||||||
|
connectTimeout: AppConstants.pendingDelay,
|
||||||
|
receiveTimeout: AppConstants.pendingDelay,
|
||||||
|
sendTimeout: AppConstants.pendingDelay,
|
||||||
|
),
|
||||||
|
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
|
||||||
|
..addSentry(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Регистрация Prefs
|
/// Регистрация Prefs
|
||||||
static void _registerPrefs() {
|
static void _registerPrefs() {
|
||||||
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance()));
|
sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,21 @@ import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
|
|||||||
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||||
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
|
||||||
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
|
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
|
||||||
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
||||||
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
|
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
|
||||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
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/my_plans/repositories/my_diets_repository.dart';
|
||||||
import 'package:eatvio_app/features/notifications/repository/notifications_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/plan_repository.dart';
|
||||||
import 'package:eatvio_app/features/plans/repositories/plans_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/profile/repositories/profile_repository.dart';
|
||||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_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/recipe/repositories/my_recipes_repository.dart';
|
||||||
import 'package:eatvio_app/features/registration/repositories/registration_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:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
@@ -53,6 +56,7 @@ class RepositoryRegistrations {
|
|||||||
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
|
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
|
||||||
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
|
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
|
||||||
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
|
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
|
||||||
|
sl.registerLazySingleton<BarcodeRepository>(() => BarcodeRepository(barcodeService: sl(), dio: sl()));
|
||||||
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
|
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
|
||||||
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
|
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
|
||||||
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
|
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
|
||||||
@@ -68,6 +72,8 @@ class RepositoryRegistrations {
|
|||||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||||
|
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
|
||||||
|
sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl()));
|
||||||
|
|
||||||
// Content repositories
|
// Content repositories
|
||||||
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
|
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
|
||||||
|
|||||||
@@ -19,18 +19,21 @@ import 'package:eatvio_app/features/diary/services/diary_service.dart';
|
|||||||
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
||||||
import 'package:eatvio_app/features/feed/services/feed_service.dart';
|
import 'package:eatvio_app/features/feed/services/feed_service.dart';
|
||||||
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
|
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
|
||||||
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
|
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
|
||||||
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
|
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
|
||||||
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
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_finance/services/cards_service.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/services/my_plans_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/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/plan_service.dart';
|
||||||
import 'package:eatvio_app/features/plans/services/plans_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/profile/services/profile_service.dart';
|
||||||
import 'package:eatvio_app/features/recipe/services/about_recipe_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/recipe/services/my_recipes_service.dart';
|
||||||
import 'package:eatvio_app/features/registration/services/registration_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:eatvio_app/features/settings/services/settings_service.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
@@ -54,6 +57,7 @@ class ServiceRegistrations {
|
|||||||
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
|
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
|
||||||
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
|
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
|
||||||
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
|
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
|
||||||
|
sl.registerLazySingleton<BarcodeService>(() => BarcodeService(sl()));
|
||||||
sl.registerLazySingleton<DietService>(() => DietService(sl()));
|
sl.registerLazySingleton<DietService>(() => DietService(sl()));
|
||||||
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
|
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
|
||||||
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
|
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
|
||||||
@@ -76,6 +80,8 @@ class ServiceRegistrations {
|
|||||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||||
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
||||||
|
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
|
||||||
|
sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl()));
|
||||||
|
|
||||||
// Content services
|
// Content services
|
||||||
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
@@ -11,6 +12,7 @@ import 'package:eatvio_app/core/locator_service.dart';
|
|||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
|
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class FirebaseApi {
|
class FirebaseApi {
|
||||||
final _firebaseMessaging = FirebaseMessaging.instance;
|
final _firebaseMessaging = FirebaseMessaging.instance;
|
||||||
@@ -19,12 +21,25 @@ class FirebaseApi {
|
|||||||
String? token;
|
String? token;
|
||||||
|
|
||||||
if (await _isHuaweiDevice()) {
|
if (await _isHuaweiDevice()) {
|
||||||
token = await _getHuaweiPushTokenNative();
|
token = await _getHuaweiPushTokenNative().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
|
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
|
||||||
await _firebaseMessaging.requestPermission();
|
try {
|
||||||
|
await _firebaseMessaging.requestPermission().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Разрешение не критично, продолжаем
|
||||||
|
log('Failed to request Firebase messaging permission: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
token = await _firebaseMessaging.getToken();
|
token = await _firebaseMessaging.getToken().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
@@ -35,13 +50,20 @@ class FirebaseApi {
|
|||||||
await sl<Dio>().post(
|
await sl<Dio>().post(
|
||||||
'/api/anonymous_token',
|
'/api/anonymous_token',
|
||||||
data: {
|
data: {
|
||||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
'platform': Platform.isIOS ? 'ios' : (await _isHuaweiDevice()) ? 'huawei' : 'android',
|
||||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||||
'token': await getToken(),
|
'token': await getToken(),
|
||||||
},
|
},
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('registerAnonymousPushToken timeout');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
await Sentry.captureException(e, stackTrace: st);
|
||||||
|
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +79,16 @@ class FirebaseApi {
|
|||||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||||
'token': await getToken(),
|
'token': await getToken(),
|
||||||
},
|
},
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('registerPushToken timeout');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
await Sentry.captureException(e, stackTrace: st);
|
||||||
|
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,10 +113,12 @@ class FirebaseApi {
|
|||||||
} on PlatformException catch (e, st) {
|
} on PlatformException catch (e, st) {
|
||||||
showErrorToast('$e $st');
|
showErrorToast('$e $st');
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
await Sentry.captureException(e, stackTrace: st);
|
||||||
return null;
|
return null;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
showErrorToast('$e $st');
|
showErrorToast('$e $st');
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
await Sentry.captureException(e, stackTrace: st);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:eatvio_app/core/app_initializer.dart';
|
||||||
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -27,6 +30,17 @@ abstract class ImageCompressUtility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<File> compressFile(File file) async {
|
static Future<File> compressFile(File file) async {
|
||||||
|
// Skip compression in test environment to avoid hangs
|
||||||
|
try {
|
||||||
|
final appConfig = sl<AppConfig>();
|
||||||
|
if (appConfig.buildType == AppBuildType.tests) {
|
||||||
|
// In tests, return the original file without compression
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// If AppConfig is not available, proceed with compression
|
||||||
|
}
|
||||||
|
|
||||||
final image = img.decodeImage(file.readAsBytesSync())!;
|
final image = img.decodeImage(file.readAsBytesSync())!;
|
||||||
int width = image.width;
|
int width = image.width;
|
||||||
int height = image.height;
|
int height = image.height;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class TokenInterceptor extends Interceptor {
|
class TokenInterceptor extends Interceptor {
|
||||||
@@ -23,6 +25,7 @@ class TokenInterceptor extends Interceptor {
|
|||||||
ErrorInterceptorHandler handler,
|
ErrorInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
if (err.response?.statusCode == 401) {
|
if (err.response?.statusCode == 401) {
|
||||||
|
await sl<WsManager>().clear();
|
||||||
await TokenStorage.removeTokenInStorage();
|
await TokenStorage.removeTokenInStorage();
|
||||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
|||||||
import 'package:eatvio_app/core/di/external_registrations.dart';
|
import 'package:eatvio_app/core/di/external_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/service_registrations.dart';
|
import 'package:eatvio_app/core/di/service_registrations.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
|
|||||||
// Регистрация внешних зависимостей (Dio, etc.)
|
// Регистрация внешних зависимостей (Dio, etc.)
|
||||||
ExternalRegistrations.registerAll();
|
ExternalRegistrations.registerAll();
|
||||||
|
|
||||||
|
// Ожидание инициализации асинхронных зависимостей
|
||||||
|
await sl.getAsync<Prefs>();
|
||||||
|
|
||||||
// Регистрация сервисов
|
// Регистрация сервисов
|
||||||
ServiceRegistrations.registerAll();
|
ServiceRegistrations.registerAll();
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }
|
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped }
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
|
||||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||||
@@ -9,6 +7,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/registration_screen.dart';
|
||||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_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/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';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
/// Маршруты для аутентификации
|
/// Маршруты для аутентификации
|
||||||
@@ -20,12 +19,11 @@ class AuthRoutes {
|
|||||||
path: RouteConstants.authScreen,
|
path: RouteConstants.authScreen,
|
||||||
name: 'authScreen',
|
name: 'authScreen',
|
||||||
builder: (context, state) => const AuthScreen(),
|
builder: (context, state) => const AuthScreen(),
|
||||||
redirect: (context, state) {
|
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
|
||||||
sl<FirebaseApi>().registerAnonymousPushToken();
|
// и не должен блокировать навигацию
|
||||||
return null;
|
|
||||||
},
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||||
|
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.forgetPasswordScreen,
|
path: RouteConstants.forgetPasswordScreen,
|
||||||
name: 'forgetPasswordScreen',
|
name: 'forgetPasswordScreen',
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ class MainNavigationRoutes {
|
|||||||
return [
|
return [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) {
|
builder: (context, state, child) {
|
||||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
|
||||||
builder: (BuildContext context, ProfileAvatarState state) {
|
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
|
||||||
if (state is ProfileAvatarStateLoaded) {
|
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
|
||||||
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
|
// Вызываем getMyProfile() только если состояние loading (первый запуск)
|
||||||
}
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
return const SizedBox.shrink();
|
profileAvatarCubit.getMyProfile();
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return BottomNavigationBarWidget(child: child);
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
@@ -6,6 +7,7 @@ import 'package:eatvio_app/core/prefs.dart';
|
|||||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
|
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
|
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
|
||||||
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
||||||
@@ -29,6 +31,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_finance/presentation/pages/user_finance_screen.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_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/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/models/profile_model.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
|
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
|
||||||
@@ -57,24 +60,33 @@ class OtherRoutes {
|
|||||||
builder: (context, state) => const WelcomeScreen(),
|
builder: (context, state) => const WelcomeScreen(),
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
try {
|
try {
|
||||||
final prefs = sl<Prefs>();
|
// Prefs уже инициализирован в locator_service, но на всякий случай проверяем
|
||||||
final firebaseApi = sl<FirebaseApi>();
|
final prefs = await sl.getAsync<Prefs>();
|
||||||
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
||||||
await firebaseApi.registerPushToken();
|
|
||||||
if (!welcomeScreenWasShowed) {
|
if (!welcomeScreenWasShowed) {
|
||||||
return null;
|
return null;
|
||||||
} else if (await TokenStorage.haveTokenInStorage()) {
|
}
|
||||||
return '/feed-screen';
|
|
||||||
} else {
|
if (await TokenStorage.haveTokenInStorage()) {
|
||||||
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
try {
|
||||||
if (!hasAnonymousTokenBeenSent) {
|
final profileRepository = sl<ProfileRepository>();
|
||||||
await firebaseApi.registerAnonymousPushToken();
|
final profile = await profileRepository.getMyProfile();
|
||||||
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
|
||||||
|
final firebaseApi = sl<FirebaseApi>();
|
||||||
|
await firebaseApi.registerPushToken();
|
||||||
|
|
||||||
|
if (profile.emailVerifiedAt != null || (prefs.getBool(PrefsKeys.isEmailVerificationSkipped) ?? false)) {
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
|
return RouteConstants.verifyEmailScreen;
|
||||||
|
} catch (e) {
|
||||||
|
return '/auth-screen';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return '/auth-screen';
|
return '/auth-screen';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return '/feed-screen';
|
return '/auth-screen';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -84,7 +96,27 @@ class OtherRoutes {
|
|||||||
builder: (context, state) => const UpdateScreen(),
|
builder: (context, state) => const UpdateScreen(),
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
try {
|
try {
|
||||||
final versions = (await sl<Dio>().get('${sl<AppConfig>().baseUrl}/api/app_versions')).data;
|
// Проверка версии с таймаутом, чтобы не блокировать запуск приложения
|
||||||
|
final versions = await sl<Dio>()
|
||||||
|
.get('${sl<AppConfig>().baseUrl}/api/app_versions')
|
||||||
|
.timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () {
|
||||||
|
// При таймауте пропускаем проверку и идем дальше
|
||||||
|
throw TimeoutException('Version check timeout');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((response) => response.data)
|
||||||
|
.catchError((e) {
|
||||||
|
// При любой ошибке пропускаем проверку версии
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (versions == null) {
|
||||||
|
// Если проверка не удалась, пропускаем экран обновления
|
||||||
|
return RouteConstants.welcomeScreen;
|
||||||
|
}
|
||||||
|
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
String version = packageInfo.version;
|
String version = packageInfo.version;
|
||||||
Version currentVersion = Version.parse(version);
|
Version currentVersion = Version.parse(version);
|
||||||
@@ -95,6 +127,7 @@ class OtherRoutes {
|
|||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// При любой ошибке пропускаем экран обновления
|
||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -120,22 +153,35 @@ class OtherRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RecipeCreateScreen.path,
|
path: RecipeCreateScreen.path,
|
||||||
name: RecipeCreateScreen.name,
|
name: RecipeCreateScreen.name,
|
||||||
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
|
builder: (context, state) {
|
||||||
|
if (state.extra is Map<String, dynamic>) {
|
||||||
|
final data = state.extra as Map<String, dynamic>;
|
||||||
|
return RecipeCreateScreen(
|
||||||
|
recipe: data['recipe'] as Recipe?,
|
||||||
|
autoValidate: data['autoValidate'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: CreateDietScreen.path,
|
path: CreateDietScreen.path,
|
||||||
name: CreateDietScreen.name,
|
name: CreateDietScreen.name,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final diet = state.extra as Diet?;
|
final data = state.extra as Map<String, dynamic>?;
|
||||||
return CreateDietScreen(diet: diet);
|
final diet = data?['diet'] as Diet?;
|
||||||
|
final isEditing = data?['isEditing'] as bool?;
|
||||||
|
return CreateDietScreen(diet: diet, isEditing: isEditing);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: DietPreviewScreen.path,
|
path: DietPreviewScreen.path,
|
||||||
name: DietPreviewScreen.name,
|
name: DietPreviewScreen.name,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final data = state.extra as Diet;
|
final data = state.extra as Map<String, dynamic>;
|
||||||
return DietPreviewScreen(diet: data);
|
final diet = data['diet'] as Diet;
|
||||||
|
final isImageInBase64 = data['isImageInBase64'] as bool;
|
||||||
|
return DietPreviewScreen(diet: diet, isImageInBase64: isImageInBase64);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
|
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
|
||||||
@@ -156,7 +202,7 @@ class OtherRoutes {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.subsScreen,
|
path: '${RouteConstants.subsScreen}/:slug',
|
||||||
name: 'subsScreen',
|
name: 'subsScreen',
|
||||||
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
|
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
|
||||||
),
|
),
|
||||||
@@ -183,7 +229,12 @@ class OtherRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.stepperBecomeConsultantScreen,
|
path: RouteConstants.stepperBecomeConsultantScreen,
|
||||||
name: 'stepperBecomeConsultantScreen',
|
name: 'stepperBecomeConsultantScreen',
|
||||||
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
|
builder: (context, state) {
|
||||||
|
final data = state.extra as Map<String, dynamic>;
|
||||||
|
final slug = data['slug'] as String?;
|
||||||
|
final isCanceled = data['isCanceled'] as bool?;
|
||||||
|
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled,);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
|
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
|
||||||
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
||||||
@@ -194,7 +245,11 @@ class OtherRoutes {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Other routes
|
// 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.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.notificationsSettingsScreen,
|
path: RouteConstants.notificationsSettingsScreen,
|
||||||
@@ -239,6 +294,11 @@ class OtherRoutes {
|
|||||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: PersonalDietSuggestionScreen.path,
|
||||||
|
name: PersonalDietSuggestionScreen.name,
|
||||||
|
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ class RouteConstants {
|
|||||||
// Auth routes
|
// Auth routes
|
||||||
static const String authScreen = '/auth-screen';
|
static const String authScreen = '/auth-screen';
|
||||||
static const String registrationScreen = 'registration-screen';
|
static const String registrationScreen = 'registration-screen';
|
||||||
|
static const String registrationScreenV2 = 'registration-screen-v2';
|
||||||
static const String forgetPasswordScreen = 'forget-password-screen';
|
static const String forgetPasswordScreen = 'forget-password-screen';
|
||||||
static const String scanQRCodeScreen = 'scan-qr-code-screen';
|
static const String scanQRCodeScreen = 'scan-qr-code-screen';
|
||||||
static const String verifyScreen = '/verify-screen';
|
static const String verifyScreen = '/verify-screen';
|
||||||
|
|||||||
@@ -10,161 +10,76 @@ class CustomTextStyles {
|
|||||||
static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
|
static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
|
||||||
|
|
||||||
// Title text style
|
// Title text style
|
||||||
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
|
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(fontWeight: FontWeight.w600);
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
|
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900);
|
||||||
color: appTheme.black900,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
|
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900, fontSize: 13);
|
||||||
color: appTheme.black900,
|
|
||||||
fontSize: 13,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
|
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
|
||||||
color: appTheme.black,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
|
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
|
||||||
color: appTheme.black,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
|
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
|
||||||
color: appTheme.black,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
|
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.whiteA700);
|
||||||
color: appTheme.whiteA700,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
|
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black900, fontSize: 12);
|
||||||
color: appTheme.black900,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
|
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
|
||||||
color: appTheme.black,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
|
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
|
||||||
color: appTheme.black,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
|
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
|
||||||
color: appTheme.black,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
|
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.blueGray900, fontSize: 12);
|
||||||
color: appTheme.blueGray900,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
|
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.gray600);
|
||||||
color: appTheme.gray600,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
|
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(color: theme.colorScheme.primary, fontSize: 12);
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
|
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(color: appTheme.black900, fontSize: 12);
|
||||||
color: appTheme.black900,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Label text style
|
// Label text style
|
||||||
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
|
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(color: appTheme.gray60003);
|
||||||
color: appTheme.gray60003,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
|
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(color: appTheme.whiteA700);
|
||||||
color: appTheme.whiteA700,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Title text style
|
// Title text style
|
||||||
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
|
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(fontSize: 16);
|
||||||
fontSize: 16,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
|
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(fontSize: 17, fontWeight: FontWeight.w900);
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
|
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(color: appTheme.black900);
|
||||||
color: appTheme.black900,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
|
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600);
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
|
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(color: appTheme.whiteA700, fontWeight: FontWeight.w600);
|
||||||
color: appTheme.whiteA700,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.blue800, fontWeight: FontWeight.w800);
|
||||||
color: appTheme.blue800,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.black);
|
||||||
color: appTheme.black,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w700);
|
||||||
color: appTheme.black,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w800);
|
||||||
color: appTheme.black,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(fontSize: 15, fontWeight: FontWeight.w700);
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001);
|
||||||
color: appTheme.greenA70001,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001, fontWeight: FontWeight.w800);
|
||||||
color: appTheme.greenA70001,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(color: appTheme.lightGreen300, fontWeight: FontWeight.w800);
|
||||||
color: appTheme.lightGreen300,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(color: appTheme.red200, fontWeight: FontWeight.w800);
|
||||||
color: appTheme.red200,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallSecondaryContainer =>
|
||||||
color: theme.colorScheme.secondaryContainer,
|
theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer, fontWeight: FontWeight.w800);
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
|
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer);
|
||||||
color: theme.colorScheme.secondaryContainer,
|
|
||||||
);
|
|
||||||
|
|
||||||
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
|
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(color: theme.colorScheme.onPrimary);
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on TextStyle {
|
extension on TextStyle {
|
||||||
@@ -178,81 +93,32 @@ extension on TextStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class TextStyles {
|
abstract class TextStyles {
|
||||||
static const headingXLSemibold = TextStyle(
|
static const headingXLSemibold = TextStyle(fontSize: 20, fontWeight: FontWeight.w600, height: 1.255);
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const headingFS40FW600 = TextStyle(
|
static const headingFS40FW600 = TextStyle(fontSize: 40, fontWeight: FontWeight.w600, height: 1.255);
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textLSemibold = TextStyle(
|
static const textLSemibold = TextStyle(fontSize: 18, fontWeight: FontWeight.w600, height: 1.255);
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textLMedium = TextStyle(
|
static const textLMedium = TextStyle(fontSize: 18, fontWeight: FontWeight.w500, height: 1.255);
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textMSemibold = TextStyle(
|
static const textMSemibold = TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1.255);
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textMMedium = TextStyle(
|
static const textMMedium = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1);
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
height: 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textMMediumAuto = TextStyle(
|
static const textMMediumAuto = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.255);
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textSSemibold = TextStyle(
|
static const textSSemibold = TextStyle(fontSize: 14, fontWeight: FontWeight.w600, height: 1.255);
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textSMedium = TextStyle(
|
static const textSMedium = TextStyle(fontSize: 14, fontWeight: FontWeight.w500, height: 1.255);
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textFS13FW400 = TextStyle(
|
static const textFS13FW400 = TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1.255);
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textXSSemibold = TextStyle(
|
static const textXSSemibold = TextStyle(fontSize: 12, fontWeight: FontWeight.w500, height: 1.255);
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const textFS10FW500 = TextStyle(
|
static const textFS10FW500 = TextStyle(fontSize: 10, fontWeight: FontWeight.w500, height: 1.255);
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w500,
|
static const textFS24FW700 = TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.255);
|
||||||
height: 1.255,
|
|
||||||
);
|
static const textFS22FW600 = TextStyle(fontSize: 22, fontWeight: FontWeight.w600, height: 1.255);
|
||||||
|
|
||||||
static const textFS24FW700 = TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
height: 1.255,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
|
|
||||||
/// Helper class for managing themes and colors.
|
/// Helper class for managing themes and colors.
|
||||||
class ThemeHelper {
|
class ThemeHelper {
|
||||||
// The current app theme
|
// The current app theme
|
||||||
final _appTheme = 'primary';
|
final _appTheme = 'primary';
|
||||||
|
|
||||||
// A map of custom color themes supported by the app
|
// A map of custom color themes supported by the app
|
||||||
final Map<String, PrimaryColors> _supportedCustomColor = {
|
final Map<String, PrimaryColors> _supportedCustomColor = {'primary': PrimaryColors()};
|
||||||
'primary': PrimaryColors(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// A map of color schemes supported by the app
|
// A map of color schemes supported by the app
|
||||||
final Map<String, ColorScheme> _supportedColorScheme = {
|
final Map<String, ColorScheme> _supportedColorScheme = {'primary': ColorSchemes.primaryColorScheme};
|
||||||
'primary': ColorSchemes.primaryColorScheme,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the primary colors for the current theme.
|
/// Returns the primary colors for the current theme.
|
||||||
PrimaryColors _getThemeColors() {
|
PrimaryColors _getThemeColors() {
|
||||||
//throw exception to notify given theme is not found or not generated by the generator
|
//throw exception to notify given theme is not found or not generated by the generator
|
||||||
if (!_supportedCustomColor.containsKey(_appTheme)) {
|
if (!_supportedCustomColor.containsKey(_appTheme)) {
|
||||||
throw Exception(
|
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
|
||||||
"$_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
|
//return theme from map
|
||||||
|
|
||||||
@@ -34,15 +28,14 @@ class ThemeHelper {
|
|||||||
ThemeData _getThemeData() {
|
ThemeData _getThemeData() {
|
||||||
//throw exception to notify given theme is not found or not generated by the generator
|
//throw exception to notify given theme is not found or not generated by the generator
|
||||||
if (!_supportedColorScheme.containsKey(_appTheme)) {
|
if (!_supportedColorScheme.containsKey(_appTheme)) {
|
||||||
throw Exception(
|
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
|
||||||
"$_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
|
//return theme from map
|
||||||
|
|
||||||
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
|
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
textTheme: GoogleFonts.mulishTextTheme(),
|
fontFamily: 'Mulish-Local',
|
||||||
|
textTheme: ThemeData.light().textTheme.apply(fontFamily: 'Mulish-Local'),
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
scaffoldBackgroundColor: colorScheme.onPrimary,
|
scaffoldBackgroundColor: colorScheme.onPrimary,
|
||||||
@@ -60,17 +53,9 @@ class ThemeHelper {
|
|||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
side: BorderSide(
|
side: BorderSide(color: colorScheme.primary, width: 1),
|
||||||
color: colorScheme.primary,
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
width: 1,
|
visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
vertical: -4,
|
|
||||||
horizontal: -4,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -149,6 +134,9 @@ class PrimaryColors {
|
|||||||
Color get greenA70001 => const Color(0XFF00BE13);
|
Color get greenA70001 => const Color(0XFF00BE13);
|
||||||
|
|
||||||
Color get green => const Color(0XFF0BB450);
|
Color get green => const Color(0XFF0BB450);
|
||||||
|
Color get darkGreen => const Color(0XFF16A249);
|
||||||
|
|
||||||
|
Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
|
||||||
|
|
||||||
// LightGreen
|
// LightGreen
|
||||||
Color get lightGreen300 => const Color(0XFFB2DF8A);
|
Color get lightGreen300 => const Color(0XFFB2DF8A);
|
||||||
@@ -182,6 +170,8 @@ class PrimaryColors {
|
|||||||
|
|
||||||
Color get lightGray0 => const Color(0xFFF6F6F6);
|
Color get lightGray0 => const Color(0xFFF6F6F6);
|
||||||
|
|
||||||
|
Color get darkGrey => const Color(0xFF5B656F);
|
||||||
|
|
||||||
Color get lightGray00 => const Color(0xFFE9E9EA);
|
Color get lightGray00 => const Color(0xFFE9E9EA);
|
||||||
|
|
||||||
Color get dottedBorder => const Color(0xFFE6E6E6);
|
Color get dottedBorder => const Color(0xFFE6E6E6);
|
||||||
@@ -208,6 +198,8 @@ class PrimaryColors {
|
|||||||
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
|
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
|
||||||
|
|
||||||
Color get grey7B => const Color(0xFF7B7B7B);
|
Color get grey7B => const Color(0xFF7B7B7B);
|
||||||
|
|
||||||
|
Color get greyD9 => const Color(0xFFD9D9D9);
|
||||||
}
|
}
|
||||||
|
|
||||||
PrimaryColors get appTheme => ThemeHelper().themeColor();
|
PrimaryColors get appTheme => ThemeHelper().themeColor();
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
abstract class TokenStorage {
|
abstract class TokenStorage {
|
||||||
|
static SharedPreferences? _cachedPrefs;
|
||||||
|
|
||||||
|
static Future<SharedPreferences> _getPrefs() async {
|
||||||
|
_cachedPrefs ??= await SharedPreferences.getInstance();
|
||||||
|
return _cachedPrefs!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the cached SharedPreferences instance.
|
||||||
|
/// This is primarily used for testing to ensure fresh state between tests.
|
||||||
|
@visibleForTesting
|
||||||
|
static void clearCache() {
|
||||||
|
_cachedPrefs = null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> haveTokenInStorage() async {
|
static Future<bool> haveTokenInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
try {
|
try {
|
||||||
|
final sharedPreferences = await _getPrefs();
|
||||||
if (sharedPreferences.getString('token') == null) {
|
if (sharedPreferences.getString('token') == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -15,25 +30,25 @@ abstract class TokenStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> getTokenFromStorage() async {
|
static Future<String?> getTokenFromStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
return sharedPreferences.getString('token');
|
return sharedPreferences.getString('token');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveTokenInStorage(String token) async {
|
static Future<void> saveTokenInStorage(String token) async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
await sharedPreferences.setString('token', token);
|
await sharedPreferences.setString('token', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> removeTokenInStorage() async {
|
static Future<void> removeTokenInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
await sharedPreferences.remove('token');
|
await sharedPreferences.remove('token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SocketIdStorage {
|
abstract class SocketIdStorage {
|
||||||
static Future<bool> haveSocketIdInStorage() async {
|
static Future<bool> haveSocketIdInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
try {
|
try {
|
||||||
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
if (sharedPreferences.getString('socketId') == null) {
|
if (sharedPreferences.getString('socketId') == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> getSocketIdFromStorage() async {
|
static Future<String?> getSocketIdFromStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
return sharedPreferences.getString('socketId');
|
return sharedPreferences.getString('socketId');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
await sharedPreferences.setString('socketId', socketId);
|
await sharedPreferences.setString('socketId', socketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> removeSocketIdInStorage() async {
|
static Future<void> removeSocketIdInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
await sharedPreferences.remove('socketId');
|
await sharedPreferences.remove('socketId');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
lib/core/utils/markdown_utils.dart
Normal file
67
lib/core/utils/markdown_utils.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/// Утилиты для обработки markdown текста
|
||||||
|
class MarkdownUtils {
|
||||||
|
/// Обрабатывает markdown текст:
|
||||||
|
/// - Исправляет неправильный синтаксис markdown ссылок
|
||||||
|
/// - Автоматически преобразует URL в markdown ссылки
|
||||||
|
/// - Преобразует **текст** в <b>текст</b> для надежного отображения жирного текста
|
||||||
|
static String processMarkdown(String text) {
|
||||||
|
// Сначала исправляем неправильный синтаксис markdown ссылок
|
||||||
|
// Например: [http://example.com](url) -> [http://example.com](http://example.com)
|
||||||
|
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
||||||
|
text = text.replaceAllMapped(wrongLinkRegex, (match) {
|
||||||
|
final urlInText = match.group(1)!;
|
||||||
|
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
||||||
|
return match.group(0)!;
|
||||||
|
}
|
||||||
|
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
||||||
|
return '[$urlInText]($fullUrl)';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Находим все существующие markdown ссылки и их позиции
|
||||||
|
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
||||||
|
final existingLinks = <_LinkRange>[];
|
||||||
|
for (final linkMatch in linkRegex.allMatches(text)) {
|
||||||
|
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регулярное выражение для поиска URL (http://, https://, www.)
|
||||||
|
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
||||||
|
|
||||||
|
// Заменяем URL на markdown ссылки, если они еще не являются ссылками
|
||||||
|
String result = text;
|
||||||
|
int offset = 0;
|
||||||
|
for (final urlMatch in urlRegex.allMatches(text)) {
|
||||||
|
// Проверяем, не находится ли URL внутри существующей markdown ссылки
|
||||||
|
bool isInsideLink = existingLinks.any((link) =>
|
||||||
|
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
||||||
|
if (!isInsideLink) {
|
||||||
|
final url = urlMatch.group(0)!;
|
||||||
|
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
||||||
|
final replacement = '[$url]($fullUrl)';
|
||||||
|
result = result.replaceRange(
|
||||||
|
urlMatch.start + offset,
|
||||||
|
urlMatch.end + offset,
|
||||||
|
replacement,
|
||||||
|
);
|
||||||
|
offset += replacement.length - url.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Гарантируем, что жирный текст будет распознан
|
||||||
|
// Заменяем **текст** на <b>текст</b> вручную для надежности
|
||||||
|
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
|
||||||
|
result = result.replaceAllMapped(boldRegex, (match) {
|
||||||
|
return '<b>${match.group(1)}</b>';
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Вспомогательный класс для хранения диапазона позиций ссылки
|
||||||
|
class _LinkRange {
|
||||||
|
final int start;
|
||||||
|
final int end;
|
||||||
|
|
||||||
|
_LinkRange(this.start, this.end);
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
extension Capitalization on String {
|
extension Capitalization on String {
|
||||||
String capitalize() {
|
String capitalize() {
|
||||||
|
if (isEmpty) return '';
|
||||||
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
String parseGender() {
|
String parseGender() {
|
||||||
if (this == 'male') {
|
if (this == 'male') {
|
||||||
return 'Мужской';
|
return 'Мужской';
|
||||||
} else if (this == 'female') {
|
} else if (this == 'female') {
|
||||||
return 'Женский';
|
return 'Женский';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ class ValidationUtils {
|
|||||||
return regex.hasMatch(phone);
|
return regex.hasMatch(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Нормализация номера телефона (+79991234455)
|
||||||
|
static String normalizePhone(String phone) {
|
||||||
|
if (phone.isEmpty) return '';
|
||||||
|
final digits = phone.replaceAll(RegExp(r'[^\d]'), '');
|
||||||
|
if (digits.isEmpty) return '';
|
||||||
|
|
||||||
|
String normalized = digits;
|
||||||
|
if (digits.startsWith('8')) {
|
||||||
|
normalized = '7${digits.substring(1)}';
|
||||||
|
} else if (digits.length == 10) {
|
||||||
|
normalized = '7$digits';
|
||||||
|
}
|
||||||
|
return '+$normalized';
|
||||||
|
}
|
||||||
|
|
||||||
/// Валидация URL
|
/// Валидация URL
|
||||||
static bool isValidUrl(String url) {
|
static bool isValidUrl(String url) {
|
||||||
if (url.isEmpty) return false;
|
if (url.isEmpty) return false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:eatvio_app/core/views/service/views_service.dart';
|
import 'package:eatvio_app/core/views/service/views_service.dart';
|
||||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class ViewsRepository {
|
class ViewsRepository {
|
||||||
final ViewsService viewsService;
|
final ViewsService viewsService;
|
||||||
@@ -9,7 +10,8 @@ class ViewsRepository {
|
|||||||
Future<void> viewContent({required ViewModel viewModel}) async {
|
Future<void> viewContent({required ViewModel viewModel}) async {
|
||||||
try {
|
try {
|
||||||
await viewsService.viewContent(viewModel: viewModel);
|
await viewsService.viewContent(viewModel: viewModel);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ import 'package:eatvio_app/core/const.dart';
|
|||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_service.dart';
|
import 'package:eatvio_app/core/websockets/ws_service.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||||
|
|
||||||
class WsManager {
|
class WsManager {
|
||||||
@@ -22,6 +24,10 @@ class WsManager {
|
|||||||
PrivateChannel? _privateMessagesChannel;
|
PrivateChannel? _privateMessagesChannel;
|
||||||
PrivateChannel? _privateDirectChatChannel;
|
PrivateChannel? _privateDirectChatChannel;
|
||||||
|
|
||||||
|
StreamSubscription? _lifecycleSubscription;
|
||||||
|
StreamSubscription? _eventSubscription;
|
||||||
|
StreamSubscription? _connectionEstablishedSubscription;
|
||||||
|
|
||||||
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
|
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
|
||||||
|
|
||||||
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
|
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
|
||||||
@@ -33,7 +39,7 @@ class WsManager {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
try {
|
try {
|
||||||
await TokenStorage.haveTokenInStorage();
|
if (!await TokenStorage.haveTokenInStorage()) return;
|
||||||
final wsUrl = await wsService.getWebsocket();
|
final wsUrl = await wsService.getWebsocket();
|
||||||
|
|
||||||
final options = PusherChannelsOptions.fromHost(
|
final options = PusherChannelsOptions.fromHost(
|
||||||
@@ -71,7 +77,7 @@ class WsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client?.eventStream.listen((event) async {
|
_connectionEstablishedSubscription = client?.eventStream.listen((event) async {
|
||||||
if (event.name == 'pusher:connection_established') {
|
if (event.name == 'pusher:connection_established') {
|
||||||
final data = event.data;
|
final data = event.data;
|
||||||
final socketId = extractSocketId(data);
|
final socketId = extractSocketId(data);
|
||||||
@@ -87,21 +93,26 @@ class WsManager {
|
|||||||
await _initPrivateNotificationsChannel();
|
await _initPrivateNotificationsChannel();
|
||||||
await _initPrivateMessagesChannel();
|
await _initPrivateMessagesChannel();
|
||||||
|
|
||||||
client?.lifecycleStream.listen((data) {
|
_lifecycleSubscription = client?.lifecycleStream.listen((data) {
|
||||||
if (data == PusherChannelsClientLifeCycleState.establishedConnection) {
|
if (data == PusherChannelsClientLifeCycleState.establishedConnection) {
|
||||||
_unsubscribe();
|
_unsubscribe();
|
||||||
_subscribe();
|
_subscribe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client?.eventStream.listen((event) {
|
_eventSubscription = client?.eventStream.listen((event) {
|
||||||
EasyDebounce.debounce(
|
log('WsManager received event: ${event.name}');
|
||||||
'searchChatRoom',
|
// Только если это не системные сообщения Pusher, обновляем список чатов
|
||||||
const Duration(milliseconds: 300),
|
if (!event.name.startsWith('pusher:')) {
|
||||||
() => sl<ChatsCubit>().getChatRooms(),
|
EasyDebounce.debounce(
|
||||||
);
|
'searchChatRoom',
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
() => sl<ChatsCubit>().getChatRooms(),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
_subscribe();
|
_subscribe();
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
|
await Sentry.captureException(e, stackTrace: st);
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +177,12 @@ class WsManager {
|
|||||||
|
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
await client?.disconnect();
|
await client?.disconnect();
|
||||||
|
await _lifecycleSubscription?.cancel();
|
||||||
|
await _eventSubscription?.cancel();
|
||||||
|
await _connectionEstablishedSubscription?.cancel();
|
||||||
|
_lifecycleSubscription = null;
|
||||||
|
_eventSubscription = null;
|
||||||
|
_connectionEstablishedSubscription = null;
|
||||||
client = null;
|
client = null;
|
||||||
userId = null;
|
userId = null;
|
||||||
_privateDirectChatChannel?.unsubscribe();
|
_privateDirectChatChannel?.unsubscribe();
|
||||||
|
|||||||
@@ -3,16 +3,28 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
|
||||||
class CustomDropDownButton<T> extends StatelessWidget {
|
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 T? value;
|
||||||
final List<DropdownMenuItem<T>> items;
|
final List<DropdownMenuItem<T>> items;
|
||||||
final void Function(T?)? onChanged;
|
final void Function(T?)? onChanged;
|
||||||
final Widget? hint;
|
final Widget? hint;
|
||||||
final bool hasError;
|
final bool hasError;
|
||||||
|
final List<Widget> Function(BuildContext)? selectedItemBuilder;
|
||||||
|
final bool isExpanded;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DropdownButtonFormField(
|
return DropdownButtonFormField(
|
||||||
hint: hint,
|
hint: hint,
|
||||||
|
isExpanded: isExpanded,
|
||||||
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
|
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
dropdownColor: appTheme.lightGray0,
|
dropdownColor: appTheme.lightGray0,
|
||||||
@@ -35,6 +47,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
|||||||
initialValue: value,
|
initialValue: value,
|
||||||
items: items,
|
items: items,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
|
selectedItemBuilder: selectedItemBuilder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class CustomOutlinedButton extends BaseButton {
|
|||||||
margin: margin,
|
margin: margin,
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
style: buttonStyle!,
|
style: buttonStyle ?? OutlinedButton.styleFrom(),
|
||||||
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -59,7 +59,7 @@ class CustomOutlinedButton extends BaseButton {
|
|||||||
margin: margin,
|
margin: margin,
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
style: buttonStyle!,
|
style: buttonStyle ?? OutlinedButton.styleFrom(),
|
||||||
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class FeedIconButton extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (svgIcon != null) svgIcon!,
|
?svgIcon,
|
||||||
if (icon != null)
|
if (icon != null)
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -12,15 +12,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
|
||||||
class InfoRow extends StatelessWidget {
|
class InfoRow extends StatelessWidget {
|
||||||
const InfoRow({
|
const InfoRow({required this.type, super.key, this.diet, this.itemsBackgroundColor, this.disableDescriptionOfTypes = false, this.plan, this.recipe});
|
||||||
required this.type,
|
|
||||||
super.key,
|
|
||||||
this.diet,
|
|
||||||
this.itemsBackgroundColor,
|
|
||||||
this.disableDescriptionOfTypes = false,
|
|
||||||
this.plan,
|
|
||||||
this.recipe,
|
|
||||||
});
|
|
||||||
final ProductType type;
|
final ProductType type;
|
||||||
final Color? itemsBackgroundColor;
|
final Color? itemsBackgroundColor;
|
||||||
final Diet? diet;
|
final Diet? diet;
|
||||||
@@ -50,44 +42,13 @@ class InfoRow extends StatelessWidget {
|
|||||||
InfoBouble(
|
InfoBouble(
|
||||||
backgroundColor: itemsBackgroundColor,
|
backgroundColor: itemsBackgroundColor,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset('assets/icons/time2.svg', width: 13.3, height: 13.3),
|
||||||
'assets/icons/time2.svg',
|
|
||||||
width: 13.3,
|
|
||||||
height: 13.3,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(switch (type) {
|
||||||
switch (type) {
|
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
||||||
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
||||||
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
ProductType.recipe => timeTitle ?? '',
|
||||||
ProductType.recipe => timeTitle ?? '',
|
}, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||||
},
|
|
||||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
InfoBouble(
|
|
||||||
backgroundColor: itemsBackgroundColor,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
switch (type) {
|
|
||||||
ProductType.diet => 'assets/icons/people.svg',
|
|
||||||
ProductType.plan => 'assets/icons/people.svg',
|
|
||||||
ProductType.recipe => 'assets/icons/calories.svg',
|
|
||||||
},
|
|
||||||
width: 13.3,
|
|
||||||
height: 13.3,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
switch (type) {
|
|
||||||
ProductType.diet => '80 человек прошли',
|
|
||||||
ProductType.plan => '80 человек прошли',
|
|
||||||
ProductType.recipe => '${recipe?.calories.toStringAsFixed(0) ?? '0'} ккал',
|
|
||||||
},
|
|
||||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -95,11 +56,17 @@ class InfoRow extends StatelessWidget {
|
|||||||
InfoBouble(
|
InfoBouble(
|
||||||
backgroundColor: itemsBackgroundColor,
|
backgroundColor: itemsBackgroundColor,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset('assets/icons/calories.svg', width: 13.3, height: 13.3),
|
||||||
'assets/icons/steps.svg',
|
const SizedBox(width: 4),
|
||||||
width: 13.3,
|
Text('${(recipe?.calories ?? 0).toStringAsFixed(0)} ккал', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||||
height: 13.3,
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
if (type == ProductType.recipe)
|
||||||
|
InfoBouble(
|
||||||
|
backgroundColor: itemsBackgroundColor,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset('assets/icons/steps.svg', width: 13.3, height: 13.3),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${recipe?.steps?.length ?? 0} ${getStepForm(recipe?.steps?.length ?? 0)}',
|
'${recipe?.steps?.length ?? 0} ${getStepForm(recipe?.steps?.length ?? 0)}',
|
||||||
@@ -110,13 +77,7 @@ class InfoRow extends StatelessWidget {
|
|||||||
if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true)
|
if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true)
|
||||||
InfoBouble(
|
InfoBouble(
|
||||||
backgroundColor: itemsBackgroundColor,
|
backgroundColor: itemsBackgroundColor,
|
||||||
children: [
|
children: [TypeBadges(type: diet?.type, planType: plan?.type, disableDescriptionOfTypes: disableDescriptionOfTypes)],
|
||||||
TypeBadges(
|
|
||||||
type: diet?.type,
|
|
||||||
planType: plan?.type,
|
|
||||||
disableDescriptionOfTypes: disableDescriptionOfTypes,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
124
lib/core/widgets/markdown_html_widget.dart
Normal file
124
lib/core/widgets/markdown_html_widget.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:eatvio_app/core/utils/markdown_utils.dart';
|
||||||
|
|
||||||
|
/// Переиспользуемый виджет для отображения HTML из markdown текста
|
||||||
|
class MarkdownHtmlWidget extends StatelessWidget {
|
||||||
|
/// Markdown текст для отображения
|
||||||
|
final String markdownText;
|
||||||
|
|
||||||
|
/// Ширина изображений (по умолчанию ширина экрана минус 32)
|
||||||
|
final double? imageWidth;
|
||||||
|
|
||||||
|
/// Цвет текста для body и параграфов
|
||||||
|
final Color? textColor;
|
||||||
|
|
||||||
|
/// Размер шрифта (по умолчанию 16)
|
||||||
|
final double? fontSize;
|
||||||
|
|
||||||
|
/// Callback для обработки нажатий на ссылки
|
||||||
|
final OnTap? onLinkTap;
|
||||||
|
|
||||||
|
const MarkdownHtmlWidget({
|
||||||
|
super.key,
|
||||||
|
required this.markdownText,
|
||||||
|
this.imageWidth,
|
||||||
|
this.textColor,
|
||||||
|
this.fontSize,
|
||||||
|
this.onLinkTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Обрабатываем markdown текст
|
||||||
|
final processedText = MarkdownUtils.processMarkdown(markdownText);
|
||||||
|
|
||||||
|
// Конвертируем markdown в HTML
|
||||||
|
final htmlData = md.markdownToHtml(
|
||||||
|
processedText,
|
||||||
|
extensionSet: md.ExtensionSet.gitHubFlavored,
|
||||||
|
);
|
||||||
|
|
||||||
|
final screenWidth = MediaQuery.maybeOf(context)?.size.width ?? 400.0;
|
||||||
|
final defaultImageWidth = imageWidth ?? (screenWidth - 32);
|
||||||
|
final defaultTextColor = textColor ?? Colors.black87;
|
||||||
|
final defaultFontSize = fontSize ?? 16.0;
|
||||||
|
|
||||||
|
return Html(
|
||||||
|
data: htmlData,
|
||||||
|
style: {
|
||||||
|
"body": Style(
|
||||||
|
margin: Margins.zero,
|
||||||
|
padding: HtmlPaddings.zero,
|
||||||
|
fontSize: FontSize(defaultFontSize),
|
||||||
|
lineHeight: const LineHeight(1.5),
|
||||||
|
color: defaultTextColor,
|
||||||
|
),
|
||||||
|
"p": Style(
|
||||||
|
margin: Margins.only(bottom: 12),
|
||||||
|
color: defaultTextColor,
|
||||||
|
),
|
||||||
|
"b, strong": Style(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
"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: defaultTextColor,
|
||||||
|
),
|
||||||
|
"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(defaultImageWidth)),
|
||||||
|
},
|
||||||
|
onLinkTap: onLinkTap ?? ((url, attributes, element) async {
|
||||||
|
if (url != null) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error launching URL: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as OnTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,10 @@ import 'package:eatvio_app/generated/l10n.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class OptionWidget extends StatelessWidget {
|
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 PacketOptions? option;
|
||||||
final bool disableDuration;
|
final bool disableDuration;
|
||||||
|
final int duration;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (option == null) {
|
if (option == null) {
|
||||||
@@ -34,8 +35,12 @@ class OptionWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (!disableDuration) ...[
|
if (!disableDuration) ...[
|
||||||
const _OptionDurationWidget(duration: 7),
|
_OptionDurationWidget(duration: duration),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: option?.hasPersonalDiet ?? false ? 8 : 12),
|
||||||
|
],
|
||||||
|
if ((option?.hasPersonalDiet ?? false) && !disableDuration) ...[
|
||||||
|
const _OptionHasPersonalDietWidget(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
const CustomDivider(),
|
const CustomDivider(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -52,7 +57,6 @@ class OptionWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _OptionDurationWidget extends StatelessWidget {
|
class _OptionDurationWidget extends StatelessWidget {
|
||||||
const _OptionDurationWidget({required this.duration});
|
const _OptionDurationWidget({required this.duration});
|
||||||
final int 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:eatvio_app/core/app_router.dart';
|
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
|
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
|
||||||
@@ -45,7 +43,7 @@ Future<void> showErrorDialog(BuildContext context, {String? title, String? descr
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AppRouter.navigatorKey.currentContext?.pop();
|
Navigator.of(context).pop();
|
||||||
if (needToOpenSettings ?? false) {
|
if (needToOpenSettings ?? false) {
|
||||||
openAppSettings();
|
openAppSettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,33 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
||||||
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false});
|
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false, this.isDrafted, this.isUpdated});
|
||||||
final String title;
|
final String title;
|
||||||
final bool isPlan;
|
final bool isPlan;
|
||||||
|
final bool? isDrafted;
|
||||||
|
final bool? isUpdated;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
|
final String text;
|
||||||
|
if (isDrafted == true) {
|
||||||
|
text = 'cохранена в черновиках';
|
||||||
|
} else if (isUpdated == true) {
|
||||||
|
text = 'успешно обновлена';
|
||||||
|
} else if (isPlan == true) {
|
||||||
|
text = 'опубликован';
|
||||||
|
} else {
|
||||||
|
text = 'опубликованна';
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 20),
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
const BottomSheetHandlerBar(),
|
const BottomSheetHandlerBar(),
|
||||||
const SizedBox(
|
const SizedBox(height: 20),
|
||||||
height: 20,
|
Text(title, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
),
|
Text(text, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
Text(
|
const SizedBox(height: 20),
|
||||||
title,
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
isPlan ? 'опубликован' : 'опубликованна',
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomOutlinedButton(
|
child: CustomOutlinedButton(
|
||||||
@@ -43,15 +44,10 @@ class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
|||||||
text: s.ok,
|
text: s.ok,
|
||||||
height: 52,
|
height: 52,
|
||||||
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
|
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
|
||||||
buttonStyle: ButtonStyle(
|
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
|
||||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 20),
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'oauth_verification_confirm_request.freezed.dart';
|
||||||
|
part 'oauth_verification_confirm_request.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class OAuthVerificationConfirmRequest with _$OAuthVerificationConfirmRequest {
|
||||||
|
const factory OAuthVerificationConfirmRequest({
|
||||||
|
@JsonKey(name: 'access_token') required String accessToken,
|
||||||
|
@JsonKey(name: 'refresh_token') required String refreshToken,
|
||||||
|
@JsonKey(name: 'client_id') required String clientId,
|
||||||
|
required String provider,
|
||||||
|
required String id,
|
||||||
|
String? email,
|
||||||
|
String? phone,
|
||||||
|
required String code,
|
||||||
|
@JsonKey(name: 'verification_type') required String verificationType,
|
||||||
|
@JsonKey(name: 'first_name') required String firstName,
|
||||||
|
@JsonKey(name: 'last_name') required String lastName,
|
||||||
|
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
|
||||||
|
required String gender,
|
||||||
|
required int weight,
|
||||||
|
required int height,
|
||||||
|
}) = _OAuthVerificationConfirmRequest;
|
||||||
|
|
||||||
|
factory OAuthVerificationConfirmRequest.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmRequestFromJson(json);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'oauth_verification_confirm_response.freezed.dart';
|
||||||
|
part 'oauth_verification_confirm_response.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmResponse {
|
||||||
|
const factory OAuthVerificationConfirmResponse({
|
||||||
|
required bool success,
|
||||||
|
required String message,
|
||||||
|
required String token,
|
||||||
|
required Map<String, dynamic> user,
|
||||||
|
@JsonKey(name: 'is_new_user') required bool isNewUser,
|
||||||
|
}) = _OAuthVerificationConfirmResponse;
|
||||||
|
|
||||||
|
factory OAuthVerificationConfirmResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmResponseFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'oauth_verification_request_response.freezed.dart';
|
||||||
|
part 'oauth_verification_request_response.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestResponse {
|
||||||
|
const factory OAuthVerificationRequestResponse({required String email, required String status}) = _OAuthVerificationRequestResponse;
|
||||||
|
|
||||||
|
factory OAuthVerificationRequestResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationRequestResponseFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
|
||||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
|
|
||||||
class AuthCubit extends Cubit<AuthState> {
|
class AuthCubit extends Cubit<AuthState> {
|
||||||
final AuthRepository authRepository;
|
final AuthRepository authRepository;
|
||||||
|
final ProfileRepository profileRepository;
|
||||||
|
final FirebaseApi firebaseApi;
|
||||||
|
final Prefs prefs;
|
||||||
|
|
||||||
AuthCubit({required this.authRepository}) : super(const AuthState());
|
AuthCubit({required this.authRepository, required this.prefs, required this.profileRepository, required this.firebaseApi}) : super(const AuthState());
|
||||||
|
|
||||||
Future<bool> getTokenByCredentials(String email, String password) async {
|
Future<bool> getTokenByCredentials(String email, String password) async {
|
||||||
emit(const AuthState.loading());
|
emit(const AuthState.loading());
|
||||||
@@ -25,4 +33,26 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> initializeUser() async {
|
||||||
|
try {
|
||||||
|
await registerAnonymousPushToken();
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
} catch (e) {
|
||||||
|
log('Error in initializeUser: $e');
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerAnonymousPushToken() async {
|
||||||
|
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
||||||
|
if (!hasAnonymousTokenBeenSent) {
|
||||||
|
await firebaseApi.registerAnonymousPushToken();
|
||||||
|
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerPushToken() async {
|
||||||
|
await firebaseApi.registerPushToken();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.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:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -28,6 +29,39 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
log('OAuthCubit: Got token model: ${tokenModel.token}');
|
log('OAuthCubit: Got token model: ${tokenModel.token}');
|
||||||
TokenStorage.saveTokenInStorage(tokenModel.token);
|
TokenStorage.saveTokenInStorage(tokenModel.token);
|
||||||
emit(OAuthState.loaded(token: tokenModel.token));
|
emit(OAuthState.loaded(token: tokenModel.token));
|
||||||
|
} on DioException catch (e) {
|
||||||
|
log('OAuthCubit: Error occurred: $e');
|
||||||
|
// Проверяем, является ли ошибка ошибкой валидации email/phone
|
||||||
|
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 errorsList = <RegistrationValidationErrors>[];
|
||||||
|
if (emailErrors != null && emailErrors.isNotEmpty) {
|
||||||
|
for (final error in emailErrors) {
|
||||||
|
final errorStr = error.toString();
|
||||||
|
if (errorStr.contains('Некорректный формат email')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
|
}
|
||||||
|
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||||
|
for (final error in phoneErrors) {
|
||||||
|
final errorStr = error.toString();
|
||||||
|
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorsList.isNotEmpty) {
|
||||||
|
emit(OAuthState.error(errorMessage: 'validation_error', registrationValidationErrors: errorsList));
|
||||||
|
} else {
|
||||||
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('OAuthCubit: Error occurred: $e');
|
log('OAuthCubit: Error occurred: $e');
|
||||||
emit(OAuthState.error(errorMessage: e.toString()));
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
@@ -77,16 +111,22 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
|
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
|
||||||
final errorsList = <RegistrationValidationErrors>[];
|
final errorsList = <RegistrationValidationErrors>[];
|
||||||
if (emailErrors != null && emailErrors.isNotEmpty) {
|
if (emailErrors != null && emailErrors.isNotEmpty) {
|
||||||
if (emailErrors.contains('Некорректный формат email')) {
|
for (final error in emailErrors) {
|
||||||
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
final errorStr = error.toString();
|
||||||
}
|
if (errorStr.contains('Некорректный формат email')) {
|
||||||
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
|
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
errorsList.add(RegistrationValidationErrors.emailExist);
|
}
|
||||||
|
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||||
if (phoneErrors.contains('Такой phone уже существует.')) {
|
for (final error in phoneErrors) {
|
||||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
final errorStr = error.toString();
|
||||||
|
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
||||||
@@ -101,6 +141,70 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
|
||||||
|
try {
|
||||||
|
emit(const OAuthState.loading());
|
||||||
|
log('OAuthCubit: Requesting verification code for email: $email, phone: $phone');
|
||||||
|
final response = await authRepository.requestOAuthVerification(email: email, phone: phone);
|
||||||
|
if (response != null) {
|
||||||
|
log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}');
|
||||||
|
}
|
||||||
|
// Сбрасываем состояние после успешного запроса кода
|
||||||
|
emit(const OAuthState());
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
log('OAuthCubit: Error requesting verification code: $e');
|
||||||
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> confirmOAuthVerification({
|
||||||
|
required String provider,
|
||||||
|
required String accessToken,
|
||||||
|
required String refreshToken,
|
||||||
|
required String clientId,
|
||||||
|
required String id,
|
||||||
|
String? email,
|
||||||
|
String? phone,
|
||||||
|
required String code,
|
||||||
|
required String verificationType,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
required String dateOfBirth,
|
||||||
|
required String gender,
|
||||||
|
required int weight,
|
||||||
|
required int height,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
emit(const OAuthState.loading());
|
||||||
|
log('OAuthCubit: Confirming OAuth verification - provider: $provider, verificationType: $verificationType, code: $code');
|
||||||
|
final response = await authRepository.confirmOAuthVerification(
|
||||||
|
provider: provider,
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
clientId: clientId,
|
||||||
|
id: id,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
code: code,
|
||||||
|
verificationType: verificationType,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
dateOfBirth: dateOfBirth,
|
||||||
|
gender: gender,
|
||||||
|
weight: weight,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
log('OAuthCubit: OAuth verification confirmed successfully, token received');
|
||||||
|
TokenStorage.saveTokenInStorage(response.token);
|
||||||
|
emit(OAuthState.loaded(token: response.token));
|
||||||
|
} catch (e) {
|
||||||
|
log('OAuthCubit: Error confirming OAuth verification: $e');
|
||||||
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
|
rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }
|
|
||||||
|
|||||||
@@ -10,5 +10,10 @@ abstract class OAuthState with _$OAuthState {
|
|||||||
|
|
||||||
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
|
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
|
||||||
|
|
||||||
const factory OAuthState.error({required String errorMessage}) = OAuthStateError;
|
const factory OAuthState.error({
|
||||||
|
required String errorMessage,
|
||||||
|
List<RegistrationValidationErrors>? registrationValidationErrors,
|
||||||
|
}) = OAuthStateError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -232,9 +231,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
final bool useMobileLayout = shortestSide < 600;
|
final bool useMobileLayout = shortestSide < 600;
|
||||||
if (!useMobileLayout) isFocused = true;
|
if (!useMobileLayout) isFocused = true;
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return BlocProvider<AuthCubit>(
|
return BlocProvider<AuthCubit>(
|
||||||
create: (BuildContext context) => authCubit,
|
create: (BuildContext context) => authCubit..initializeUser(),
|
||||||
child: BlocBuilder<AuthCubit, AuthState>(
|
child: BlocBuilder<AuthCubit, AuthState>(
|
||||||
builder: (BuildContext context, AuthState state) {
|
builder: (BuildContext context, AuthState state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -337,7 +335,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Text('Нет аккаунта? '),
|
const Text('Нет аккаунта? '),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
|
onTap: () => GoRouter.of(context).pushNamed('registrationScreenV2'),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Зарегистрироваться',
|
'Зарегистрироваться',
|
||||||
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||||
@@ -411,7 +409,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
label: state is AuthStateLoading
|
label: state is AuthStateLoading || _isLoading
|
||||||
? const CupertinoActivityIndicator()
|
? const CupertinoActivityIndicator()
|
||||||
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||||
),
|
),
|
||||||
@@ -426,8 +424,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
backgroundColor: state is AuthStateLoading || _isLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
||||||
onPressed: state is AuthStateLoading
|
onPressed: state is AuthStateLoading || _isLoading
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@@ -487,7 +485,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
label: state is AuthStateLoading
|
label: state is AuthStateLoading || _isLoading
|
||||||
? const CupertinoActivityIndicator()
|
? const CupertinoActivityIndicator()
|
||||||
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
|
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
|||||||
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@@ -29,14 +30,9 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
||||||
builder: (BuildContext context, ResetPasswordState state) {
|
builder: (BuildContext context, ResetPasswordState state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), centerTitle: false),
|
||||||
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
|
|
||||||
centerTitle: false,
|
|
||||||
),
|
|
||||||
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
||||||
? const Center(
|
? const Center(child: CircularProgressIndicator())
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -45,63 +41,32 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text(
|
const Text('E-mail', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'E-mail',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(controller: emailController, hintText: 'Введите e-mail'),
|
||||||
controller: emailController,
|
|
||||||
hintText: 'Введите e-mail',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
||||||
const Text(
|
const Text('Код', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'Код',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(controller: codeController, hintText: 'Введите код'),
|
||||||
controller: codeController,
|
|
||||||
hintText: 'Введите код',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text('Пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'Пароль',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
hintText: 'Введите новый пароль',
|
hintText: 'Введите новый пароль',
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text('Подтвердите пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'Подтвердите пароль',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
controller: passwordConfirmController,
|
controller: passwordConfirmController,
|
||||||
hintText: 'Введите новый пароль',
|
hintText: 'Введите новый пароль',
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@@ -109,11 +74,11 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
||||||
context.read<ResetPasswordCubit>().tryResetPassword(
|
context.read<ResetPasswordCubit>().tryResetPassword(
|
||||||
emailController.text,
|
emailController.text,
|
||||||
codeController.text,
|
codeController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
passwordConfirmController.text,
|
passwordConfirmController.text,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
||||||
}
|
}
|
||||||
@@ -122,20 +87,13 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
child: Ink(
|
child: Ink(
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
||||||
? 'Сменить пароль'
|
? 'Сменить пароль'
|
||||||
: 'Сбросить пароль',
|
: 'Сбросить пароль',
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/const.dart';
|
import 'package:eatvio_app/core/const.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
@@ -15,6 +16,7 @@ import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widge
|
|||||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
|
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -49,6 +51,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
bool weightError = false;
|
bool weightError = false;
|
||||||
bool heightError = false;
|
bool heightError = false;
|
||||||
bool incorrectEmailFormatError = false;
|
bool incorrectEmailFormatError = false;
|
||||||
|
bool showVerificationButton = false;
|
||||||
|
String? verificationType; // 'email' or 'phone'
|
||||||
|
|
||||||
// Состояние для выбора пола
|
// Состояние для выбора пола
|
||||||
bool isMaleSelected = true;
|
bool isMaleSelected = true;
|
||||||
@@ -221,7 +225,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
hasError: emailExistError || incorrectEmailFormatError,
|
hasError: emailExistError || incorrectEmailFormatError,
|
||||||
onChanged: (newValue) {},
|
onChanged: (newValue) {},
|
||||||
),
|
),
|
||||||
if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')],
|
if (emailExistError) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const ValidationErrorWidget(error: 'Такая почта уже существует.'),
|
||||||
|
if (showVerificationButton && verificationType == 'email') ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
onTap: () => _handleVerificationRequest(context, 'email'),
|
||||||
|
child: Ink(
|
||||||
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Да, это мой аккаунт, подтвердить',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
|
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text('Номер телефона'),
|
const Text('Номер телефона'),
|
||||||
@@ -235,7 +260,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
hasError: phoneExistError,
|
hasError: phoneExistError,
|
||||||
onChanged: (newValue) {},
|
onChanged: (newValue) {},
|
||||||
),
|
),
|
||||||
if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')],
|
if (phoneExistError) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const ValidationErrorWidget(error: 'Такой номер телефона уже существует.'),
|
||||||
|
if (showVerificationButton && verificationType == 'phone') ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
onTap: () => _handleVerificationRequest(context, 'phone'),
|
||||||
|
child: Ink(
|
||||||
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Да, это мой аккаунт, подтвердить',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -289,12 +335,17 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
const Text('Вес'),
|
const Text('Рост'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
|
CustomTextFormField(
|
||||||
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
|
controller: heightController,
|
||||||
|
hintText: 'Введите рост',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (newValue) {},
|
||||||
|
hasError: heightError,
|
||||||
|
),
|
||||||
|
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -303,11 +354,18 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text('Рост'),
|
const Text('Вес'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
|
CustomTextFormField(
|
||||||
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
|
controller: weightController,
|
||||||
|
hintText: 'Введите вес',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (newValue) {},
|
||||||
|
hasError: weightError,
|
||||||
|
),
|
||||||
|
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -481,7 +539,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
if (hasErrorWeightOrHeightError) return;
|
if (hasErrorWeightOrHeightError) return;
|
||||||
|
|
||||||
// Очищаем номер телефона от маски для отправки
|
// Очищаем номер телефона от маски для отправки
|
||||||
final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
|
final cleanPhone = ValidationUtils.normalizePhone(phoneController.text);
|
||||||
|
|
||||||
if (clientID == null) return;
|
if (clientID == null) return;
|
||||||
// Вызываем OAuth регистрацию
|
// Вызываем OAuth регистрацию
|
||||||
@@ -501,6 +559,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
clientId: clientID,
|
clientId: clientID,
|
||||||
);
|
);
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
|
bool hasEmailExist = false;
|
||||||
|
bool hasPhoneExist = false;
|
||||||
|
|
||||||
|
// Сначала определяем, какие ошибки присутствуют
|
||||||
for (final error in errors) {
|
for (final error in errors) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case RegistrationValidationErrors.incorrectEmailFormat:
|
case RegistrationValidationErrors.incorrectEmailFormat:
|
||||||
@@ -509,14 +571,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.emailExist:
|
case RegistrationValidationErrors.emailExist:
|
||||||
setState(() {
|
hasEmailExist = true;
|
||||||
emailExistError = true;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.phoneExist:
|
case RegistrationValidationErrors.phoneExist:
|
||||||
setState(() {
|
hasPhoneExist = true;
|
||||||
phoneExistError = true;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -525,8 +583,236 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Устанавливаем verificationType согласно правилам:
|
||||||
|
// - Если оба (email и phone) существуют -> 'email'
|
||||||
|
// - Если только email -> 'email'
|
||||||
|
// - Если только phone -> 'phone'
|
||||||
|
if (hasEmailExist || hasPhoneExist) {
|
||||||
|
setState(() {
|
||||||
|
emailExistError = hasEmailExist;
|
||||||
|
phoneExistError = hasPhoneExist;
|
||||||
|
showVerificationButton = true;
|
||||||
|
if (hasEmailExist && hasPhoneExist) {
|
||||||
|
verificationType = 'email'; // Если оба существуют, используем email
|
||||||
|
} else if (hasEmailExist) {
|
||||||
|
verificationType = 'email'; // Только email
|
||||||
|
} else {
|
||||||
|
verificationType = 'phone'; // Только phone
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если есть ошибки, возвращаемся и не показываем toast об успехе
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Регистрация успешна только если нет ошибок
|
||||||
showSuccessToast('Регистрация успешна');
|
showSuccessToast('Регистрация успешна');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleVerificationRequest(BuildContext context, String type) async {
|
||||||
|
try {
|
||||||
|
// Используем service locator для доступа к OAuthCubit
|
||||||
|
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
|
||||||
|
// Всегда передаем и email и phone
|
||||||
|
final String email = emailController.text;
|
||||||
|
final String phone = ValidationUtils.normalizePhone(phoneController.text);
|
||||||
|
|
||||||
|
final response = await oauthCubit.requestOAuthVerification(email: email, phone: phone);
|
||||||
|
|
||||||
|
if (response != null && context.mounted) {
|
||||||
|
// Показываем диалог для ввода кода
|
||||||
|
final displayText = response.email;
|
||||||
|
_showVerificationCodeDialog(context, type, displayText);
|
||||||
|
} else if ((emailController.text.isEmpty || phoneController.text.isEmpty) && context.mounted) {
|
||||||
|
showErrorToast('Пожалуйста, заполните все поля');
|
||||||
|
} else if (context.mounted) {
|
||||||
|
showErrorToast('Не удалось отправить код верификации');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showErrorToast('Ошибка: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showVerificationCodeDialog(BuildContext context, String verificationType, String displayText) {
|
||||||
|
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
|
||||||
|
final List<TextEditingController> controllers = List.generate(4, (_) => TextEditingController());
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Подтверждение аккаунта'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Код отправлен на $displayText'),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: List.generate(4, (index) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 50,
|
||||||
|
child: TextField(
|
||||||
|
controller: controllers[index],
|
||||||
|
focusNode: focusNodes[index],
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 1,
|
||||||
|
decoration: const InputDecoration(counterText: '', border: OutlineInputBorder()),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isNotEmpty && index < 3) {
|
||||||
|
focusNodes[index + 1].requestFocus();
|
||||||
|
}
|
||||||
|
if (value.isEmpty && index > 0) {
|
||||||
|
focusNodes[index - 1].requestFocus();
|
||||||
|
}
|
||||||
|
// Проверяем, заполнен ли весь код
|
||||||
|
final code = controllers.map((c) => c.text).join();
|
||||||
|
if (code.length == 4) {
|
||||||
|
// Небольшая задержка перед отправкой, чтобы пользователь увидел последний символ
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||||
|
if (dialogContext.mounted && context.mounted) {
|
||||||
|
await _handleVerificationConfirm(context, dialogContext, verificationType, code, controllers, focusNodes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
for (var controller in controllers) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
for (var node in focusNodes) {
|
||||||
|
node.dispose();
|
||||||
|
}
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Отмена'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleVerificationConfirm(
|
||||||
|
BuildContext widgetContext,
|
||||||
|
BuildContext dialogContext,
|
||||||
|
String verificationType,
|
||||||
|
String code,
|
||||||
|
List<TextEditingController> controllers,
|
||||||
|
List<FocusNode> focusNodes,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// Используем service locator для доступа к OAuthCubit, так как контекст из диалога может не иметь доступа к провайдеру
|
||||||
|
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
|
||||||
|
final authResult = widget.authResult;
|
||||||
|
|
||||||
|
if (authResult == null) {
|
||||||
|
showErrorToast('Ошибка: данные авторизации не найдены');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всегда передаем и email и phone (полный набор данных от провайдера)
|
||||||
|
// Используем данные из формы, если они есть, иначе из authResult
|
||||||
|
final String email = emailController.text.isNotEmpty ? emailController.text : (authResult.email ?? '');
|
||||||
|
final String phone = phoneController.text.isNotEmpty ? ValidationUtils.normalizePhone(phoneController.text) : ValidationUtils.normalizePhone(authResult.phone ?? '');
|
||||||
|
|
||||||
|
// Проверяем, что данные есть
|
||||||
|
if (email.isEmpty && phone.isEmpty) {
|
||||||
|
log('OAuth Verification Confirm: Both email and phone are empty');
|
||||||
|
showErrorToast('Ошибка: не указан email или телефон');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что все необходимые данные от провайдера есть
|
||||||
|
if (authResult.accessToken.isEmpty || authResult.clientId.isEmpty || authResult.userId.isEmpty) {
|
||||||
|
log('OAuth Verification Confirm: Missing required OAuth data');
|
||||||
|
showErrorToast('Ошибка: неполные данные авторизации');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что код не пустой
|
||||||
|
if (code.isEmpty || code.length != 4) {
|
||||||
|
showErrorToast('Введите полный код');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем данные профиля из формы
|
||||||
|
final String firstName = firstNameController.text.isNotEmpty ? firstNameController.text : (authResult.firstName ?? '');
|
||||||
|
final String lastName = lastNameController.text.isNotEmpty ? lastNameController.text : (authResult.lastName ?? '');
|
||||||
|
final String dateOfBirth = dateOfBirthController.text.isNotEmpty ? dateOfBirthController.text : (authResult.birthDate ?? '');
|
||||||
|
final String gender = isMaleSelected ? 'male' : 'female';
|
||||||
|
final int weight = weightController.text.isNotEmpty ? (int.tryParse(weightController.text) ?? 0) : 0;
|
||||||
|
final int height = heightController.text.isNotEmpty ? (int.tryParse(heightController.text) ?? 0) : 0;
|
||||||
|
|
||||||
|
// Проверяем, что все обязательные поля заполнены
|
||||||
|
if (firstName.isEmpty || lastName.isEmpty || dateOfBirth.isEmpty || weight == 0 || height == 0) {
|
||||||
|
log('OAuth Verification Confirm: Missing required profile data');
|
||||||
|
showErrorToast('Ошибка: заполните все обязательные поля');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем перед отправкой запроса
|
||||||
|
log('OAuth Verification Confirm: provider=${widget.provider}, verificationType=$verificationType, code=$code, email=$email, phone=$phone');
|
||||||
|
|
||||||
|
// Отправляем запрос на подтверждение с полным набором данных от провайдера
|
||||||
|
await oauthCubit.confirmOAuthVerification(
|
||||||
|
provider: widget.provider ?? 'social',
|
||||||
|
accessToken: authResult.accessToken,
|
||||||
|
refreshToken: authResult.refreshToken ?? '',
|
||||||
|
clientId: authResult.clientId,
|
||||||
|
id: authResult.userId,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
code: code,
|
||||||
|
verificationType: verificationType,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
dateOfBirth: dateOfBirth,
|
||||||
|
gender: gender,
|
||||||
|
weight: weight,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Закрываем диалог
|
||||||
|
for (var controller in controllers) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
for (var node in focusNodes) {
|
||||||
|
node.dispose();
|
||||||
|
}
|
||||||
|
if (dialogContext.mounted) {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перезагружаем веб-сокет и переходим на главный экран
|
||||||
|
if (widgetContext.mounted) {
|
||||||
|
_reconnectWebSocket(widgetContext);
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace);
|
||||||
|
if (widgetContext.mounted) {
|
||||||
|
showErrorToast('Неверный код. Попробуйте снова.');
|
||||||
|
}
|
||||||
|
// Очищаем поля
|
||||||
|
for (var controller in controllers) {
|
||||||
|
controller.clear();
|
||||||
|
}
|
||||||
|
if (focusNodes.isNotEmpty) {
|
||||||
|
focusNodes[0].requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class AuthRepository {
|
class AuthRepository {
|
||||||
final AuthService authService;
|
final AuthService authService;
|
||||||
@@ -14,12 +19,12 @@ class AuthRepository {
|
|||||||
return await authService.getTokenByCredentials(
|
return await authService.getTokenByCredentials(
|
||||||
credentials: CredentialsModel(email: email, password: password),
|
credentials: CredentialsModel(email: email, password: password),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<TokenModel> signInViaSocialMedia({
|
Future<TokenModel> signInViaSocialMedia({
|
||||||
required String provider,
|
required String provider,
|
||||||
required String accessToken,
|
required String accessToken,
|
||||||
@@ -30,7 +35,8 @@ class AuthRepository {
|
|||||||
return await authService.signInViaSocialMedia(
|
return await authService.signInViaSocialMedia(
|
||||||
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
|
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +75,73 @@ class AuthRepository {
|
|||||||
),
|
),
|
||||||
))['token']!
|
))['token']!
|
||||||
as String;
|
as String;
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
|
||||||
|
try {
|
||||||
|
Map<String, dynamic>? body;
|
||||||
|
if (email != null) {
|
||||||
|
body = {'email': email};
|
||||||
|
} else if (phone != null) {
|
||||||
|
body = {'phone': phone};
|
||||||
|
}
|
||||||
|
if (body != null) {
|
||||||
|
return await authService.requestOAuthVerification(body: body);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({
|
||||||
|
required String provider,
|
||||||
|
required String accessToken,
|
||||||
|
required String refreshToken,
|
||||||
|
required String clientId,
|
||||||
|
required String id,
|
||||||
|
String? email,
|
||||||
|
String? phone,
|
||||||
|
required String code,
|
||||||
|
required String verificationType,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
required String dateOfBirth,
|
||||||
|
required String gender,
|
||||||
|
required int weight,
|
||||||
|
required int height,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = OAuthVerificationConfirmRequest(
|
||||||
|
provider: provider,
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
clientId: clientId,
|
||||||
|
id: id,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
code: code,
|
||||||
|
verificationType: verificationType,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
dateOfBirth: dateOfBirth,
|
||||||
|
gender: gender,
|
||||||
|
weight: weight,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
// Логируем запрос перед отправкой
|
||||||
|
log(
|
||||||
|
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
|
||||||
|
);
|
||||||
|
return await authService.confirmOAuthVerification(body: request);
|
||||||
|
} catch (e, s) {
|
||||||
|
log('AuthRepository: Error in confirmOAuthVerification: $e');
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
|
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class ResetPasswordRepository {
|
class ResetPasswordRepository {
|
||||||
final ResetPasswordService resetPasswordService;
|
final ResetPasswordService resetPasswordService;
|
||||||
@@ -20,7 +21,8 @@ class ResetPasswordRepository {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +48,8 @@ class ResetPasswordRepository {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
import 'package:eatvio_app/core/env_config.dart';
|
import 'package:eatvio_app/core/env_config.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:vk_id/vk_id.dart';
|
import 'package:vk_id/vk_id.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
|
||||||
@@ -40,7 +42,8 @@ class SocialAuthFacade {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,8 +159,9 @@ class SocialAuthFacade {
|
|||||||
birthDate: yandexProfileData.birthday,
|
birthDate: yandexProfileData.birthday,
|
||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -204,7 +208,8 @@ class SocialAuthFacade {
|
|||||||
if (code == null || deviceId == null) return null;
|
if (code == null || deviceId == null) return null;
|
||||||
|
|
||||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,8 +260,9 @@ class SocialAuthFacade {
|
|||||||
if (!context.mounted) return null;
|
if (!context.mounted) return null;
|
||||||
return showDialog<Map<String, String>>(
|
return showDialog<Map<String, String>>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: true,
|
||||||
builder: (_) {
|
builder: (dialogContext) {
|
||||||
|
final s = S.of(dialogContext);
|
||||||
const borderRadius = BorderRadius.all(Radius.circular(16));
|
const borderRadius = BorderRadius.all(Radius.circular(16));
|
||||||
return Dialog(
|
return Dialog(
|
||||||
insetPadding: const EdgeInsets.all(16),
|
insetPadding: const EdgeInsets.all(16),
|
||||||
@@ -264,7 +270,21 @@ class SocialAuthFacade {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
|
child: SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.7,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(s.authorization),
|
||||||
|
),
|
||||||
|
body: WebViewWidget(controller: controller),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||||
import 'package:retrofit/retrofit.dart';
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
||||||
@@ -19,4 +22,10 @@ abstract class AuthService {
|
|||||||
|
|
||||||
@POST('/api/oauth/login')
|
@POST('/api/oauth/login')
|
||||||
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
|
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
|
||||||
|
|
||||||
|
@POST('/api/oauth/verification/request')
|
||||||
|
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required Map<String, dynamic> body});
|
||||||
|
|
||||||
|
@POST('/api/oauth/verification/confirm')
|
||||||
|
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body});
|
||||||
}
|
}
|
||||||
|
|||||||
29
lib/features/barcode/models/barcode_product_model.dart
Normal file
29
lib/features/barcode/models/barcode_product_model.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'barcode_product_model.freezed.dart';
|
||||||
|
part 'barcode_product_model.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class BarcodeProductModel with _$BarcodeProductModel {
|
||||||
|
const factory BarcodeProductModel({
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
required String GTIN,
|
||||||
|
@JsonKey(name: 'Товарный знак (ru)') String? brandName,
|
||||||
|
@JsonKey(name: 'Описание (ru)') String? description,
|
||||||
|
@JsonKey(name: 'Количество/вес') String? quantity,
|
||||||
|
@JsonKey(name: 'Рынок сбыта') String? market,
|
||||||
|
@JsonKey(name: 'Калорийность') double? calories,
|
||||||
|
@JsonKey(name: 'Белки') double? proteins,
|
||||||
|
@JsonKey(name: 'Жиры') double? fats,
|
||||||
|
@JsonKey(name: 'Углеводы') double? carbohydrates,
|
||||||
|
}) = _BarcodeProductModel;
|
||||||
|
|
||||||
|
factory BarcodeProductModel.fromJson(Map<String, Object?> json) => _$BarcodeProductModelFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class BarcodeTaskStatusModel with _$BarcodeTaskStatusModel {
|
||||||
|
const factory BarcodeTaskStatusModel({String? status, @JsonKey(name: 'task_id') String? taskId, BarcodeProductModel? data}) = _BarcodeTaskStatusModel;
|
||||||
|
|
||||||
|
factory BarcodeTaskStatusModel.fromJson(Map<String, Object?> json) => _$BarcodeTaskStatusModelFromJson(json);
|
||||||
|
}
|
||||||
112
lib/features/barcode/repositories/barcode_repository.dart
Normal file
112
lib/features/barcode/repositories/barcode_repository.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
|
class BarcodeRepository {
|
||||||
|
final BarcodeService barcodeService;
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
BarcodeRepository({
|
||||||
|
required this.barcodeService,
|
||||||
|
required this.dio,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Получить информацию о продукте по штрихкоду
|
||||||
|
/// Возвращает либо данные продукта, либо статус задачи обработки
|
||||||
|
Future<BarcodeResult> getProductByBarcode(String barcode) async {
|
||||||
|
try {
|
||||||
|
final response = await dio.get('/api/gsone/barcode/$barcode');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
// Данные найдены в БД
|
||||||
|
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>);
|
||||||
|
return BarcodeResult.success(product);
|
||||||
|
} else if (response.statusCode == 202) {
|
||||||
|
// Запущена задача обработки
|
||||||
|
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>);
|
||||||
|
return BarcodeResult.processing(taskStatus.taskId ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('Unexpected status code: ${response.statusCode}');
|
||||||
|
} on DioException catch (e, s) {
|
||||||
|
if (e.response?.statusCode == 202) {
|
||||||
|
// Если получен статус 202, значит запущена задача обработки
|
||||||
|
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
|
||||||
|
return BarcodeResult.processing(taskStatus.taskId ?? '');
|
||||||
|
}
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить статус задачи обработки
|
||||||
|
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
|
||||||
|
try {
|
||||||
|
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
|
||||||
|
|
||||||
|
if (statusModel.status == 'completed' && statusModel.data != null) {
|
||||||
|
return BarcodeTaskStatus.completed(statusModel.data!);
|
||||||
|
} else if (statusModel.status == 'failed') {
|
||||||
|
return BarcodeTaskStatus.failed();
|
||||||
|
} else {
|
||||||
|
return BarcodeTaskStatus.processing();
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Результат запроса информации о продукте по штрихкоду
|
||||||
|
class BarcodeResult {
|
||||||
|
final BarcodeResultType type;
|
||||||
|
final BarcodeProductModel? product;
|
||||||
|
final String? taskId;
|
||||||
|
|
||||||
|
BarcodeResult._({required this.type, this.product, this.taskId});
|
||||||
|
|
||||||
|
factory BarcodeResult.success(BarcodeProductModel product) {
|
||||||
|
return BarcodeResult._(type: BarcodeResultType.success, product: product);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BarcodeResult.processing(String taskId) {
|
||||||
|
return BarcodeResult._(type: BarcodeResultType.processing, taskId: taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BarcodeResultType {
|
||||||
|
success,
|
||||||
|
processing,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Статус задачи обработки
|
||||||
|
class BarcodeTaskStatus {
|
||||||
|
final BarcodeTaskStatusType type;
|
||||||
|
final BarcodeProductModel? product;
|
||||||
|
|
||||||
|
BarcodeTaskStatus._({required this.type, this.product});
|
||||||
|
|
||||||
|
factory BarcodeTaskStatus.completed(BarcodeProductModel product) {
|
||||||
|
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.completed, product: product);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BarcodeTaskStatus.processing() {
|
||||||
|
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.processing);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BarcodeTaskStatus.failed() {
|
||||||
|
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BarcodeTaskStatusType {
|
||||||
|
completed,
|
||||||
|
processing,
|
||||||
|
failed,
|
||||||
|
}
|
||||||
|
|
||||||
24
lib/features/barcode/services/barcode_service.dart
Normal file
24
lib/features/barcode/services/barcode_service.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
|
||||||
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
||||||
|
part 'barcode_service.g.dart';
|
||||||
|
|
||||||
|
@RestApi()
|
||||||
|
abstract class BarcodeService {
|
||||||
|
factory BarcodeService(Dio dio, {String baseUrl}) = _BarcodeService;
|
||||||
|
|
||||||
|
@GET('/api/gsone/barcode/{barcode}')
|
||||||
|
Future<BarcodeProductModel> getProductByBarcode({@Path('barcode') required String barcode});
|
||||||
|
|
||||||
|
@GET('/api/gsone/barcode/status/{task_id}')
|
||||||
|
Future<BarcodeTaskStatusModel> getTaskStatus({@Path('task_id') required String taskId});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
|
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
|
||||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||||
@@ -13,12 +13,11 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit
|
|||||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
|
|
||||||
class BlogPostScreen extends StatefulWidget {
|
class BlogPostScreen extends StatefulWidget {
|
||||||
final int id;
|
final int id;
|
||||||
@@ -78,96 +77,100 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
bottomSheet: Container(
|
bottomSheet: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
children: [
|
child: Row(
|
||||||
if (replyId != null)
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
GestureDetector(
|
children: [
|
||||||
onTap: () {
|
if (replyId != null)
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
GestureDetector(
|
||||||
setState(() {
|
onTap: () {
|
||||||
replyId = null;
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
textEditingController.clear();
|
setState(() {
|
||||||
});
|
replyId = null;
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
textEditingController.clear();
|
||||||
},
|
});
|
||||||
child: Container(
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (replyId != null) const SizedBox(width: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minHeight: 52),
|
|
||||||
child: TextFormField(
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
onChanged: (_) {
|
|
||||||
setState(() {});
|
|
||||||
},
|
},
|
||||||
controller: textEditingController,
|
child: Container(
|
||||||
maxLines: 5,
|
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||||
minLines: 1,
|
height: 32,
|
||||||
keyboardType: TextInputType.multiline,
|
width: 32,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
||||||
decoration: InputDecoration(
|
),
|
||||||
isDense: false,
|
),
|
||||||
// убираем true, чтобы padding учитывался корректно
|
if (replyId != null) const SizedBox(width: 8),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
Expanded(
|
||||||
// вертикальный паддинг увеличен
|
child: ConstrainedBox(
|
||||||
hintText: 'Комментарий',
|
constraints: const BoxConstraints(minHeight: 52),
|
||||||
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
child: TextFormField(
|
||||||
filled: true,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
border: OutlineInputBorder(
|
onChanged: (_) {
|
||||||
borderRadius: BorderRadius.circular(16),
|
setState(() {});
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
},
|
||||||
),
|
controller: textEditingController,
|
||||||
enabledBorder: OutlineInputBorder(
|
maxLines: 5,
|
||||||
borderRadius: BorderRadius.circular(16),
|
minLines: 1,
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
keyboardType: TextInputType.multiline,
|
||||||
),
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
||||||
focusedBorder: OutlineInputBorder(
|
decoration: InputDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
isDense: false,
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
// убираем true, чтобы padding учитывался корректно
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
// вертикальный паддинг увеличен
|
||||||
|
hintText: S.of(context).comment_hint,
|
||||||
|
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
||||||
|
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),
|
||||||
const SizedBox(width: 8),
|
GestureDetector(
|
||||||
GestureDetector(
|
onTap: () {
|
||||||
onTap: () {
|
if (textEditingController.text.length > 2) {
|
||||||
if (textEditingController.text.length > 2) {
|
commentsCubit.sendComment(
|
||||||
commentsCubit.sendComment(
|
id: widget.id.toString(),
|
||||||
id: widget.id.toString(),
|
commentText: textEditingController.text,
|
||||||
commentText: textEditingController.text,
|
parentCommentId: replyId,
|
||||||
parentCommentId: replyId,
|
commentSourceType: CommentSourceType.blogPost,
|
||||||
commentSourceType: CommentSourceType.blogPost,
|
);
|
||||||
);
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
setState(() {
|
||||||
setState(() {
|
textEditingController.clear();
|
||||||
textEditingController.clear();
|
replyId = null;
|
||||||
replyId = null;
|
});
|
||||||
});
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
}
|
||||||
}
|
},
|
||||||
},
|
child: Container(
|
||||||
child: Container(
|
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
|
||||||
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
|
height: 32,
|
||||||
height: 32,
|
width: 32,
|
||||||
width: 32,
|
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
||||||
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -194,7 +197,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.only(bottom: 80),
|
padding: EdgeInsets.only(bottom: 160 + MediaQuery.of(context).padding.bottom), // поле комментария (52-200) + padding (12+12+56) + safe area
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -233,34 +236,8 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Html(
|
child: MarkdownHtmlWidget(
|
||||||
data: md.markdownToHtml(blogPostState.blogPostModel!.description.replaceAll('\n', '<br>')),
|
markdownText: blogPostState.blogPostModel!.description,
|
||||||
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),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -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/app_router.dart';
|
||||||
import 'package:eatvio_app/core/permission_handler/permission_handler.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/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/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_cubit.dart';
|
||||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
|
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
|
||||||
@@ -567,6 +568,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
|
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
|
||||||
const SizedBox(height: 80),
|
const SizedBox(height: 80),
|
||||||
_createBlogPostButton(),
|
_createBlogPostButton(),
|
||||||
|
const SizedBox(height: 80),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -598,7 +600,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
showErrorDialog(context, title: S.current.could_not_update_blog_post);
|
showErrorDialog(context, title: S.current.could_not_update_blog_post);
|
||||||
} else {
|
} else {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.pop();
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final result = await blogPostCubit.createBlogPost(
|
final result = await blogPostCubit.createBlogPost(
|
||||||
@@ -615,21 +619,23 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
|||||||
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
|
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
|
||||||
} else {
|
} else {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.pop();
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Ink(
|
child: isLoading
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
? const Center(child: Loader())
|
||||||
height: 52,
|
: Ink(
|
||||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
child: isLoading
|
height: 52,
|
||||||
? const Center(child: CircularProgressIndicator())
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
|
||||||
: const Center(
|
child: const Center(
|
||||||
child: Text('Создать', style: TextStyle(color: Colors.white)),
|
child: Text('Создать', style: TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
|||||||
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
|
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class BlogPostRepository {
|
class BlogPostRepository {
|
||||||
final BlogPostService blogPostService;
|
final BlogPostService blogPostService;
|
||||||
@@ -29,9 +30,14 @@ class BlogPostRepository {
|
|||||||
if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name),
|
if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await blogPostService.createBlogPost(
|
try {
|
||||||
formData: formData,
|
return await blogPostService.createBlogPost(
|
||||||
);
|
formData: formData,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<XFile?> downloadImage(String imageUrl) async {
|
Future<XFile?> downloadImage(String imageUrl) async {
|
||||||
@@ -54,8 +60,9 @@ class BlogPostRepository {
|
|||||||
log('Ошибка загрузки изображения: ${response.statusCode}');
|
log('Ошибка загрузки изображения: ${response.statusCode}');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
log('Произошла ошибка: $e');
|
log('Произошла ошибка: $e');
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,11 +86,23 @@ class BlogPostRepository {
|
|||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await blogPostService.updateBlogPost(
|
try {
|
||||||
id: id,
|
return await blogPostService.updateBlogPost(
|
||||||
formData: formData,
|
id: id,
|
||||||
);
|
formData: formData,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteBlogPost(int id) async => await blogPostService.deleteBlogPost(id: id);
|
Future<void> deleteBlogPost(int id) async {
|
||||||
|
try {
|
||||||
|
await blogPostService.deleteBlogPost(id: id);
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,20 +36,24 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
|||||||
|
|
||||||
Future<void> getMyProfile() async {
|
Future<void> getMyProfile() async {
|
||||||
emit(const ProfileAvatarState.loading());
|
emit(const ProfileAvatarState.loading());
|
||||||
final profile = await profileRepository.getMyProfile();
|
|
||||||
final profileDetails = await profileRepository.getMyProfileDetails();
|
|
||||||
try {
|
try {
|
||||||
|
final profile = await profileRepository.getMyProfile();
|
||||||
|
final profileDetails = await profileRepository.getMyProfileDetails();
|
||||||
emit(ProfileAvatarState.loaded(profile: profile));
|
emit(ProfileAvatarState.loaded(profile: profile));
|
||||||
final notificationStatus = await Permission.notification.status;
|
try {
|
||||||
final isNotificationEnabled = notificationStatus.isGranted;
|
final notificationStatus = await Permission.notification.status;
|
||||||
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
|
final isNotificationEnabled = notificationStatus.isGranted;
|
||||||
AppMetricaNameAttribute.withValue(profile.name),
|
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
|
||||||
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
|
AppMetricaNameAttribute.withValue(profile.name),
|
||||||
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
|
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
|
||||||
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
|
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
|
||||||
]);
|
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
|
||||||
AppMetrica.setUserProfileID(profile.slug);
|
]);
|
||||||
AppMetrica.reportUserProfile(userProfile);
|
AppMetrica.setUserProfileID(profile.slug);
|
||||||
|
AppMetrica.reportUserProfile(userProfile);
|
||||||
|
} catch (e) {
|
||||||
|
// Игнорируем ошибки плагинов, профиль уже загружен
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
||||||
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
|
import 'package:eatvio_app/core/env_config.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
@@ -16,14 +18,9 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class BottomNavigationBarWidget extends StatefulWidget {
|
class BottomNavigationBarWidget extends StatefulWidget {
|
||||||
final ProfileModel profileModel;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const BottomNavigationBarWidget({
|
const BottomNavigationBarWidget({super.key, required this.child});
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
required this.profileModel,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BottomNavigationBarWidget> createState() => _BottomNavigationBarWidgetState();
|
State<BottomNavigationBarWidget> createState() => _BottomNavigationBarWidgetState();
|
||||||
@@ -31,37 +28,20 @@ class BottomNavigationBarWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||||
int currentBottomNavBarIndex = 1;
|
int currentBottomNavBarIndex = 1;
|
||||||
late final List<String> pages;
|
List<String>? pages;
|
||||||
int tapsCount = 0;
|
int tapsCount = 0;
|
||||||
|
|
||||||
@override
|
List<String> _getPages(bool isConsultant) {
|
||||||
void initState() {
|
if (isConsultant) {
|
||||||
super.initState();
|
return ['/my-profile-screen', '/feed-screen', '/diary-screen', '/consultations-screen', '/chat-select-screen', '/notifications-screen', '/menu-screen'];
|
||||||
|
|
||||||
if (widget.profileModel.roles.contains('consultant')) {
|
|
||||||
pages = [
|
|
||||||
'/my-profile-screen',
|
|
||||||
'/feed-screen',
|
|
||||||
'/diary-screen',
|
|
||||||
'/consultations-screen',
|
|
||||||
'/chat-select-screen',
|
|
||||||
'/notifications-screen',
|
|
||||||
'/menu-screen',
|
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
pages = [
|
return ['/my-profile-screen', '/feed-screen', '/diary-screen', '/chat-select-screen', '/notifications-screen', '/menu-screen'];
|
||||||
'/my-profile-screen',
|
|
||||||
'/feed-screen',
|
|
||||||
'/diary-screen',
|
|
||||||
'/chat-select-screen',
|
|
||||||
'/notifications-screen',
|
|
||||||
'/menu-screen',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onItemTapped(int index) async {
|
void _onItemTapped(int index) async {
|
||||||
if (pages.length == 7) {
|
if (pages == null) return;
|
||||||
|
if (pages!.length == 7) {
|
||||||
if (index == 6) {
|
if (index == 6) {
|
||||||
tapsCount++;
|
tapsCount++;
|
||||||
if (tapsCount == 7) {
|
if (tapsCount == 7) {
|
||||||
@@ -82,184 +62,189 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
currentBottomNavBarIndex = index;
|
currentBottomNavBarIndex = index;
|
||||||
});
|
});
|
||||||
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
|
GoRouter.of(context).replace(pages![currentBottomNavBarIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500
|
||||||
|
|
||||||
|
Widget _buildLogoIcon() {
|
||||||
|
final isProduction = sl<AppConfig>().baseUrl == EnvConfig.baseUrl;
|
||||||
|
final logo = SvgPicture.asset(
|
||||||
|
ImageConstant.imgLogo,
|
||||||
|
height: 32,
|
||||||
|
colorFilter: isProduction ? null : const ColorFilter.mode(_devAccentColor, BlendMode.srcIn),
|
||||||
|
);
|
||||||
|
if (isProduction) return logo;
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: _devAccentColor.withValues(alpha: 0.6), width: 1.5),
|
||||||
|
color: _devAccentColor.withValues(alpha: 0.08),
|
||||||
|
),
|
||||||
|
child: logo,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: -6,
|
||||||
|
right: -4,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _devAccentColor,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
boxShadow: [BoxShadow(color: _devAccentColor.withValues(alpha: 0.4), blurRadius: 4, offset: const Offset(0, 1))],
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'DEV',
|
||||||
|
style: TextStyle(fontSize: 8, fontWeight: FontWeight.w700, color: Colors.white, letterSpacing: 0.3, height: 1.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
return Scaffold(
|
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||||
body: widget.child,
|
builder: (BuildContext context, ProfileAvatarState state) {
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
if (state is ProfileAvatarStateLoaded) {
|
||||||
type: BottomNavigationBarType.fixed,
|
final profileModel = state.profile;
|
||||||
onTap: _onItemTapped,
|
final isConsultant = profileModel.roles.contains('consultant');
|
||||||
selectedFontSize: 10,
|
|
||||||
unselectedFontSize: 10,
|
// Инициализируем pages на основе profileModel
|
||||||
selectedItemColor: appTheme.blueA400,
|
pages ??= _getPages(isConsultant);
|
||||||
currentIndex: currentBottomNavBarIndex,
|
|
||||||
items: [
|
return Scaffold(
|
||||||
BottomNavigationBarItem(
|
body: widget.child,
|
||||||
label: '',
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
icon: CircleAvatar(
|
type: BottomNavigationBarType.fixed,
|
||||||
foregroundImage: NetworkImage(
|
onTap: _onItemTapped,
|
||||||
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + widget.profileModel.profilePhotoUrl,
|
selectedFontSize: 10,
|
||||||
),
|
unselectedFontSize: 10,
|
||||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
selectedItemColor: appTheme.blueA400,
|
||||||
radius: 16,
|
currentIndex: currentBottomNavBarIndex,
|
||||||
),
|
items: [
|
||||||
),
|
BottomNavigationBarItem(
|
||||||
BottomNavigationBarItem(
|
label: '',
|
||||||
label: 'Лента',
|
icon: CircleAvatar(
|
||||||
icon: Padding(
|
foregroundImage: NetworkImage(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
profileModel.profilePhotoUrl.contains('https') ? profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + profileModel.profilePhotoUrl,
|
||||||
child: SvgPicture.asset(
|
|
||||||
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
ImageConstant.icnFeed,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
activeIcon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
ImageConstant.icnFeed,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
label: s.diary,
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
|
||||||
ImageConstant.icnDiary,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
activeIcon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
ImageConstant.icnDiary,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.profileModel.roles.contains('consultant'))
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
label: 'Консультации',
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
|
||||||
'assets/icons/consultations.svg',
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
activeIcon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
'assets/icons/consultations.svg',
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
label: s.chat,
|
|
||||||
icon: BlocBuilder<ChatsCubit, ChatsState>(
|
|
||||||
builder: (BuildContext context, ChatsState state) {
|
|
||||||
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
|
|
||||||
return badges.Badge(
|
|
||||||
position: badges.BadgePosition.topEnd(),
|
|
||||||
badgeAnimation: const badges.BadgeAnimation.fade(),
|
|
||||||
badgeStyle: badges.BadgeStyle(
|
|
||||||
badgeColor: hasUnreadMessages ? Colors.red : Colors.transparent,
|
|
||||||
),
|
|
||||||
badgeContent: Text(
|
|
||||||
hasUnreadMessages ? state.chatsWithUnreadMessagesCount.toString() : '',
|
|
||||||
style: TextStyle(
|
|
||||||
color: hasUnreadMessages ? Colors.white : Colors.transparent,
|
|
||||||
fontSize: 8,
|
|
||||||
),
|
),
|
||||||
|
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||||
|
radius: 16,
|
||||||
),
|
),
|
||||||
child: Padding(
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: 'Лента',
|
||||||
|
icon: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), width: 20, height: 20, ImageConstant.icnFeed),
|
||||||
ImageConstant.icnChat,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
colorFilter: pages[currentBottomNavBarIndex] == '/chat-select-screen'
|
|
||||||
? ColorFilter.mode(
|
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
)
|
|
||||||
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
activeIcon: Padding(
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
label: 'Уведомления',
|
|
||||||
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
|
|
||||||
builder: (BuildContext context, NotificationsState state) {
|
|
||||||
return badges.Badge(
|
|
||||||
position: badges.BadgePosition.topEnd(),
|
|
||||||
badgeAnimation: const badges.BadgeAnimation.fade(),
|
|
||||||
showBadge: state is NotificationsStateLoaded && state.hasUnreadNotifications,
|
|
||||||
badgeContent: const Text(
|
|
||||||
'',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(ImageConstant.icnFeed, width: 20, height: 20, colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)),
|
||||||
ImageConstant.icnBell,
|
),
|
||||||
colorFilter: pages[currentBottomNavBarIndex] == '/notifications-screen'
|
),
|
||||||
? ColorFilter.mode(
|
BottomNavigationBarItem(
|
||||||
appTheme.blueA400,
|
label: s.diary,
|
||||||
BlendMode.srcIn,
|
icon: Padding(
|
||||||
)
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
child: SvgPicture.asset(colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), ImageConstant.icnDiary, width: 20, height: 20),
|
||||||
width: 20,
|
),
|
||||||
height: 20,
|
activeIcon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(ImageConstant.icnDiary, width: 20, height: 20, colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (profileModel.roles.contains('consultant'))
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: 'Консультации',
|
||||||
|
icon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||||
|
'assets/icons/consultations.svg',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
activeIcon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/icons/consultations.svg',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
BottomNavigationBarItem(
|
||||||
},
|
label: s.chat,
|
||||||
|
icon: BlocBuilder<ChatsCubit, ChatsState>(
|
||||||
|
builder: (BuildContext context, ChatsState state) {
|
||||||
|
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
|
||||||
|
return badges.Badge(
|
||||||
|
position: badges.BadgePosition.topEnd(),
|
||||||
|
badgeAnimation: const badges.BadgeAnimation.fade(),
|
||||||
|
badgeStyle: badges.BadgeStyle(badgeColor: hasUnreadMessages ? Colors.red : Colors.transparent),
|
||||||
|
badgeContent: Text(
|
||||||
|
hasUnreadMessages ? state.chatsWithUnreadMessagesCount.toString() : '',
|
||||||
|
style: TextStyle(color: hasUnreadMessages ? Colors.white : Colors.transparent, fontSize: 8),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
ImageConstant.icnChat,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
|
||||||
|
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
|
||||||
|
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: 'Уведомления',
|
||||||
|
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
|
||||||
|
builder: (BuildContext context, NotificationsState state) {
|
||||||
|
return badges.Badge(
|
||||||
|
position: badges.BadgePosition.topEnd(),
|
||||||
|
badgeAnimation: const badges.BadgeAnimation.fade(),
|
||||||
|
showBadge: state is NotificationsStateLoaded && state.hasUnreadNotifications,
|
||||||
|
badgeContent: const Text('', style: TextStyle(color: Colors.white, fontSize: 8)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
ImageConstant.icnBell,
|
||||||
|
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
|
||||||
|
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
|
||||||
|
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(label: '', icon: _buildLogoIcon()),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
BottomNavigationBarItem(
|
}
|
||||||
label: '',
|
// Показываем child даже когда профиль еще загружается, чтобы экраны могли инициализироваться
|
||||||
icon: SvgPicture.asset(
|
return Scaffold(body: widget.child);
|
||||||
ImageConstant.imgLogo,
|
},
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:eatvio_app/features/calendar/models/calendar_model.dart';
|
import 'package:eatvio_app/features/calendar/models/calendar_model.dart';
|
||||||
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
|
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class CalendarRepository {
|
class CalendarRepository {
|
||||||
final CalendarService calendarService;
|
final CalendarService calendarService;
|
||||||
@@ -9,7 +10,8 @@ class CalendarRepository {
|
|||||||
Future<List<CalendarModel>> getCalendar() async {
|
Future<List<CalendarModel>> getCalendar() async {
|
||||||
try {
|
try {
|
||||||
return await calendarService.getCalendar();
|
return await calendarService.getCalendar();
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
|
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
|
||||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
|
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
class CancelPlanRepository {
|
class CancelPlanRepository {
|
||||||
final CancelPlanService cancelService;
|
final CancelPlanService cancelService;
|
||||||
@@ -10,7 +11,8 @@ class CancelPlanRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await cancelService.cancelChosenPlan(id: id, body: body);
|
await cancelService.cancelChosenPlan(id: id, body: body);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,36 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
part 'chat_message_model.freezed.dart';
|
part 'chat_message_model.freezed.dart';
|
||||||
part 'chat_message_model.g.dart';
|
part 'chat_message_model.g.dart';
|
||||||
|
|
||||||
|
int _parseAuthorId(dynamic json) {
|
||||||
|
if (json == null) return 0;
|
||||||
|
if (json is num) return json.toInt();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime _parseUpdatedAt(dynamic json) {
|
||||||
|
if (json == null) return DateTime.now();
|
||||||
|
if (json is String) return parseDate(json);
|
||||||
|
return DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _parseData(dynamic json) {
|
||||||
|
if (json == null) return '';
|
||||||
|
return json.toString();
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class ChatMessageModel with _$ChatMessageModel {
|
abstract class ChatMessageModel with _$ChatMessageModel {
|
||||||
const factory ChatMessageModel({
|
const factory ChatMessageModel({
|
||||||
required int id,
|
required int id,
|
||||||
required String data,
|
@JsonKey(name: 'data', fromJson: _parseData) required String data,
|
||||||
@JsonKey(name: 'media_data') required List<dynamic>? mediaData,
|
@JsonKey(name: 'media_data') required List<dynamic>? mediaData,
|
||||||
@JsonKey(name: 'author_id') required int authorId,
|
@JsonKey(name: 'urls_media_info') List<Map<String, dynamic>>? urlsMediaInfo,
|
||||||
@JsonKey(name: 'updated_at', fromJson: parseDate) required DateTime updatedAt,
|
@JsonKey(name: 'author_id', fromJson: _parseAuthorId) required int authorId,
|
||||||
|
@JsonKey(name: 'updated_at', fromJson: _parseUpdatedAt) required DateTime updatedAt,
|
||||||
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
|
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
|
||||||
@JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor,
|
@JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor,
|
||||||
@JsonKey(name: 'room_id') required String? roomId,
|
@JsonKey(name: 'room_id') required String? roomId,
|
||||||
|
@JsonKey(name: 'type') String? type,
|
||||||
@Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState,
|
@Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState,
|
||||||
@Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid,
|
@Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid,
|
||||||
}) = _ChatMessageModel;
|
}) = _ChatMessageModel;
|
||||||
|
|||||||
@@ -31,18 +31,18 @@ abstract class ChatRoom with _$ChatRoom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChatMessageModel? parseLastMessage(dynamic json) {
|
ChatMessageModel? parseLastMessage(dynamic json) {
|
||||||
if (json is List) {
|
if (json == null || json is List) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ChatMessageModel.fromJson(json);
|
if (json is Map<String, dynamic>) {
|
||||||
|
return ChatMessageModel.fromJson(json);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class Users with _$Users {
|
abstract class Users with _$Users {
|
||||||
const factory Users({
|
const factory Users({@JsonKey(name: "from_chat_user") required ChatUser fromChatUser, @JsonKey(name: "to_chat_user") required ChatUser toChatUser}) = _Users;
|
||||||
@JsonKey(name: "from_chat_user") required ChatUser fromChatUser,
|
|
||||||
@JsonKey(name: "to_chat_user") required ChatUser toChatUser,
|
|
||||||
}) = _Users;
|
|
||||||
|
|
||||||
factory Users.fromJson(Map<String, dynamic> json) => _$UsersFromJson(json);
|
factory Users.fromJson(Map<String, dynamic> json) => _$UsersFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user