forked from eatvio/eatvio_app
Compare commits
205 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 | |||
|
|
f3d4478c65 | ||
|
|
d0becb9e11 | ||
|
|
965ae2f02b | ||
|
|
9fcfac87d4 | ||
|
|
2a25be5aa4 | ||
|
|
7df32dabd2 | ||
|
|
2e57cd9519 | ||
|
|
58bb766721 | ||
|
|
0be7392a39 | ||
|
|
4deda92c5e | ||
|
|
6fa944b3df | ||
|
|
c31411c4a2 | ||
|
|
6214d19a14 | ||
|
|
db5dddff86 |
@@ -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
|
||||||
|
|
||||||
@@ -144,14 +163,62 @@ jobs:
|
|||||||
|
|
||||||
- name: Test coverage
|
- name: Test coverage
|
||||||
run: |
|
run: |
|
||||||
|
echo "📊 Analyzing test coverage..."
|
||||||
|
echo ""
|
||||||
awk -F: '
|
awk -F: '
|
||||||
/^LF:/ { total += $2 }
|
/^SF:/ {
|
||||||
/^LH:/ { covered += $2 }
|
file = $0
|
||||||
END {
|
sub(/^SF:/, "", file)
|
||||||
if (total == 0) {
|
sub(/^[[:space:]]+/, "", file)
|
||||||
print "Coverage: 0%"
|
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 {
|
} else {
|
||||||
printf "Coverage: %.2f%%\n", (covered / total) * 100
|
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
|
' coverage/lcov.info
|
||||||
@@ -172,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 }}
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -19,4 +19,8 @@ ios/XCBuildData/
|
|||||||
env/.env
|
env/.env
|
||||||
.env
|
.env
|
||||||
android/key.properties
|
android/key.properties
|
||||||
app/upload-keystore.jks
|
app/upload-keystore.jks
|
||||||
|
coverage/
|
||||||
|
.fvm/
|
||||||
|
.fvmrc
|
||||||
|
/eatvio
|
||||||
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'
|
||||||
}
|
}
|
||||||
@@ -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/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 |
@@ -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 = 83;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -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 = 83;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -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 = 83;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>83</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];
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/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/registration_v2/cubit/registration_cubit.dart';
|
||||||
@@ -76,10 +76,10 @@ 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()));
|
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
|
||||||
// Feed related cubits
|
// Feed related cubits
|
||||||
@@ -114,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()));
|
||||||
|
|||||||
@@ -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,6 +17,7 @@ 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';
|
||||||
@@ -55,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()));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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';
|
||||||
@@ -56,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()));
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -21,10 +19,8 @@ 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(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -58,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';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -85,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);
|
||||||
@@ -96,6 +127,7 @@ class OtherRoutes {
|
|||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// При любой ошибке пропускаем экран обновления
|
||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -121,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()),
|
||||||
@@ -184,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()),
|
||||||
@@ -247,7 +297,7 @@ class OtherRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: PersonalDietSuggestionScreen.path,
|
path: PersonalDietSuggestionScreen.path,
|
||||||
name: PersonalDietSuggestionScreen.name,
|
name: PersonalDietSuggestionScreen.name,
|
||||||
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
|
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 {
|
||||||
@@ -34,7 +34,8 @@ class ThemeHelper {
|
|||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,166 +1,124 @@
|
|||||||
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/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
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_bloc/flutter_bloc.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
class ForgetPasswordScreen extends StatefulWidget {
|
|
||||||
final bool isAuth;
|
class ForgetPasswordScreen extends StatefulWidget {
|
||||||
const ForgetPasswordScreen({super.key, required this.isAuth});
|
final bool isAuth;
|
||||||
|
const ForgetPasswordScreen({super.key, required this.isAuth});
|
||||||
@override
|
|
||||||
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
|
@override
|
||||||
}
|
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
|
||||||
|
}
|
||||||
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|
||||||
final emailController = TextEditingController();
|
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||||
final codeController = TextEditingController();
|
final emailController = TextEditingController();
|
||||||
final passwordController = TextEditingController();
|
final codeController = TextEditingController();
|
||||||
final passwordConfirmController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
|
final passwordConfirmController = TextEditingController();
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
@override
|
||||||
return BlocProvider<ResetPasswordCubit>(
|
Widget build(BuildContext context) {
|
||||||
create: (BuildContext context) => sl<ResetPasswordCubit>(),
|
return BlocProvider<ResetPasswordCubit>(
|
||||||
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
create: (BuildContext context) => sl<ResetPasswordCubit>(),
|
||||||
builder: (BuildContext context, ResetPasswordState state) {
|
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
||||||
return Scaffold(
|
builder: (BuildContext context, ResetPasswordState state) {
|
||||||
appBar: AppBar(
|
return Scaffold(
|
||||||
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
|
appBar: AppBar(title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), centerTitle: false),
|
||||||
centerTitle: false,
|
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
||||||
),
|
? const Center(child: CircularProgressIndicator())
|
||||||
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
: Padding(
|
||||||
? const Center(
|
padding: const EdgeInsets.all(16),
|
||||||
child: CircularProgressIndicator(),
|
child: Column(
|
||||||
)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(16),
|
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
||||||
child: Column(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
||||||
children: [
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
),
|
||||||
Text(
|
const SizedBox(height: 24),
|
||||||
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
const Text('E-mail', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
style: const TextStyle(
|
const SizedBox(height: 4),
|
||||||
fontSize: 16,
|
CustomTextFormField(controller: emailController, hintText: 'Введите e-mail'),
|
||||||
fontWeight: FontWeight.w500,
|
],
|
||||||
),
|
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
||||||
),
|
const Text('Код', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
CustomTextFormField(controller: codeController, hintText: 'Введите код'),
|
||||||
'E-mail',
|
const SizedBox(height: 12),
|
||||||
style: TextStyle(
|
const Text('Пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
fontSize: 14,
|
const SizedBox(height: 4),
|
||||||
fontWeight: FontWeight.w500,
|
CustomTextFormField(
|
||||||
),
|
controller: passwordController,
|
||||||
),
|
hintText: 'Введите новый пароль',
|
||||||
const SizedBox(height: 4),
|
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||||
CustomTextFormField(
|
),
|
||||||
controller: emailController,
|
const SizedBox(height: 12),
|
||||||
hintText: 'Введите e-mail',
|
const Text('Подтвердите пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
],
|
CustomTextFormField(
|
||||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
controller: passwordConfirmController,
|
||||||
const Text(
|
hintText: 'Введите новый пароль',
|
||||||
'Код',
|
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||||
style: TextStyle(
|
),
|
||||||
fontSize: 14,
|
],
|
||||||
fontWeight: FontWeight.w500,
|
const Spacer(),
|
||||||
),
|
InkWell(
|
||||||
),
|
onTap: () {
|
||||||
const SizedBox(height: 4),
|
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
||||||
CustomTextFormField(
|
context.read<ResetPasswordCubit>().tryResetPassword(
|
||||||
controller: codeController,
|
emailController.text,
|
||||||
hintText: 'Введите код',
|
codeController.text,
|
||||||
),
|
passwordController.text,
|
||||||
const SizedBox(height: 12),
|
passwordConfirmController.text,
|
||||||
const Text(
|
);
|
||||||
'Пароль',
|
} else {
|
||||||
style: TextStyle(
|
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
||||||
fontSize: 14,
|
}
|
||||||
fontWeight: FontWeight.w500,
|
},
|
||||||
),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
child: Ink(
|
||||||
const SizedBox(height: 4),
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
CustomTextFormField(
|
padding: const EdgeInsets.all(16),
|
||||||
controller: passwordController,
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||||
hintText: 'Введите новый пароль',
|
child: Center(
|
||||||
),
|
child: Text(
|
||||||
const SizedBox(height: 12),
|
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
||||||
const Text(
|
? 'Сменить пароль'
|
||||||
'Подтвердите пароль',
|
: 'Сбросить пароль',
|
||||||
style: TextStyle(
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||||
fontSize: 14,
|
),
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
],
|
||||||
CustomTextFormField(
|
),
|
||||||
controller: passwordConfirmController,
|
),
|
||||||
hintText: 'Введите новый пароль',
|
);
|
||||||
),
|
},
|
||||||
],
|
listener: (BuildContext context, ResetPasswordState state) {
|
||||||
const Spacer(),
|
if (state is ResetPasswordStateCodeSendSuccess) {
|
||||||
InkWell(
|
showSuccessToast('Код на почту успешно отправлен');
|
||||||
onTap: () {
|
}
|
||||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
if (state is ResetPasswordStateCodeSendError) {
|
||||||
context.read<ResetPasswordCubit>().tryResetPassword(
|
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
|
||||||
emailController.text,
|
}
|
||||||
codeController.text,
|
if (state is ResetPasswordStateResetPasswordError) {
|
||||||
passwordController.text,
|
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
|
||||||
passwordConfirmController.text,
|
}
|
||||||
);
|
if (state is ResetPasswordStateResetPassowrdSuccess) {
|
||||||
} else {
|
GoRouter.of(context).pop();
|
||||||
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
),
|
||||||
child: Ink(
|
);
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
}
|
||||||
padding: const EdgeInsets.all(16),
|
}
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
|
||||||
? 'Сменить пароль'
|
|
||||||
: 'Сбросить пароль',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
listener: (BuildContext context, ResetPasswordState state) {
|
|
||||||
if (state is ResetPasswordStateCodeSendSuccess) {
|
|
||||||
showSuccessToast('Код на почту успешно отправлен');
|
|
||||||
}
|
|
||||||
if (state is ResetPasswordStateCodeSendError) {
|
|
||||||
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
|
|
||||||
}
|
|
||||||
if (state is ResetPasswordStateResetPasswordError) {
|
|
||||||
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
|
|
||||||
}
|
|
||||||
if (state is ResetPasswordStateResetPassowrdSuccess) {
|
|
||||||
GoRouter.of(context).pop();
|
|
||||||
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -568,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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -599,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(
|
||||||
@@ -616,7 +619,9 @@ 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,9 +164,15 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
messagesSubscription = wsManager.directChatStream?.listen((event) {
|
messagesSubscription = wsManager.directChatStream?.listen((event) {
|
||||||
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
|
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
|
||||||
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
|
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
|
||||||
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
|
final currentState = state as ChatRoomStateLoaded;
|
||||||
|
|
||||||
|
// Сообщения от текущего пользователя не приходят через WebSocket,
|
||||||
|
// они обрабатываются через ответ API в sendMessage
|
||||||
|
// Здесь обрабатываем только входящие сообщения от других пользователей
|
||||||
|
List<ChatMessageModel> messagesList = [message, ...currentState.chatMessages];
|
||||||
|
|
||||||
chatRepository.emitNewMessageToChats(message);
|
chatRepository.emitNewMessageToChats(message);
|
||||||
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
|
emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading));
|
||||||
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
|
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,6 +191,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
final messageUuid = uuid.v4();
|
final messageUuid = uuid.v4();
|
||||||
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
|
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
|
||||||
AudioPlayer().play((AssetSource('audio/send_message.wav')));
|
AudioPlayer().play((AssetSource('audio/send_message.wav')));
|
||||||
|
|
||||||
|
// Создаем временное сообщение с состоянием loading
|
||||||
emit(
|
emit(
|
||||||
(state as ChatRoomStateLoaded).copyWith(
|
(state as ChatRoomStateLoaded).copyWith(
|
||||||
chatMessages: [
|
chatMessages: [
|
||||||
@@ -206,12 +214,25 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
|
// Отправляем сообщение и получаем ответ от сервера с правильными данными
|
||||||
|
final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
|
||||||
|
|
||||||
|
// Заменяем локальное сообщение данными с сервера (с правильными URL файлов)
|
||||||
emit(
|
emit(
|
||||||
(state as ChatRoomStateLoaded).copyWith(
|
(state as ChatRoomStateLoaded).copyWith(
|
||||||
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
|
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
|
||||||
if (elem.uuid == messageUuid) {
|
if (elem.uuid == messageUuid) {
|
||||||
return elem.copyWith(messageState: MessageState.sent);
|
// Сохраняем UUID, состояние и обновляем данные из ответа сервера
|
||||||
|
// Используем значения из локального сообщения для полей, которые могут отсутствовать в ответе
|
||||||
|
return serverMessage.copyWith(
|
||||||
|
uuid: messageUuid,
|
||||||
|
messageState: MessageState.sent,
|
||||||
|
isCurrentUserAuthor: true,
|
||||||
|
isRead: elem.isRead,
|
||||||
|
roomId: elem.roomId ?? serverMessage.roomId,
|
||||||
|
authorId: serverMessage.authorId,
|
||||||
|
updatedAt: serverMessage.updatedAt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return elem;
|
return elem;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -320,6 +341,18 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
if (message != null) {
|
if (message != null) {
|
||||||
try {
|
try {
|
||||||
AudioPlayer().play((AssetSource('audio/send_message.wav')));
|
AudioPlayer().play((AssetSource('audio/send_message.wav')));
|
||||||
|
|
||||||
|
// Определяем, есть ли файлы для повторной отправки
|
||||||
|
List<File>? filesToResend;
|
||||||
|
if (message.mediaData != null && message.mediaData!.isNotEmpty) {
|
||||||
|
// Если mediaData содержит File объекты, используем их
|
||||||
|
// Если это строки (URL), то файлы уже на сервере и повторная отправка не нужна
|
||||||
|
final firstItem = message.mediaData!.first;
|
||||||
|
if (firstItem is File) {
|
||||||
|
filesToResend = message.mediaData!.cast<File>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
(state as ChatRoomStateLoaded).copyWith(
|
(state as ChatRoomStateLoaded).copyWith(
|
||||||
chatMessages: [
|
chatMessages: [
|
||||||
@@ -341,12 +374,27 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await chatRepository.sendMessage(data: message.data, files: message.mediaData as List<File>?, slug: userSlug);
|
// Отправляем сообщение и получаем ответ от сервера
|
||||||
|
final serverMessage = await chatRepository.sendMessage(
|
||||||
|
data: message.data,
|
||||||
|
files: filesToResend,
|
||||||
|
slug: userSlug,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Заменяем локальное сообщение данными с сервера
|
||||||
emit(
|
emit(
|
||||||
(state as ChatRoomStateLoaded).copyWith(
|
(state as ChatRoomStateLoaded).copyWith(
|
||||||
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
|
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
|
||||||
if (elem.uuid == uuid) {
|
if (elem.uuid == uuid) {
|
||||||
return elem.copyWith(messageState: MessageState.sent);
|
return serverMessage.copyWith(
|
||||||
|
uuid: uuid,
|
||||||
|
messageState: MessageState.sent,
|
||||||
|
isCurrentUserAuthor: true,
|
||||||
|
isRead: elem.isRead,
|
||||||
|
roomId: elem.roomId ?? serverMessage.roomId,
|
||||||
|
authorId: serverMessage.authorId,
|
||||||
|
updatedAt: serverMessage.updatedAt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return elem;
|
return elem;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cub
|
|||||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
|
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
|
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/presentation/widgets/plan_order_message_widget.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
|
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
|
||||||
@@ -221,14 +222,36 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) ||
|
state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) ||
|
||||||
index + 1 == state.chatMessages.length) {
|
index + 1 == state.chatMessages.length) {
|
||||||
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
||||||
|
final message = state.chatMessages[index];
|
||||||
|
final isPlanOrderMessage = message.type == 'plan_order';
|
||||||
|
|
||||||
|
PacketOptions? messageOption;
|
||||||
|
if (isPlanOrderMessage && chosenPlan != null) {
|
||||||
|
if (chosenPlan.optionType == 'options1') {
|
||||||
|
messageOption = chosenPlan.plan.packet1Options;
|
||||||
|
} else if (chosenPlan.optionType == 'options2') {
|
||||||
|
messageOption = chosenPlan.plan.packet2Options;
|
||||||
|
} else if (chosenPlan.optionType == 'options3') {
|
||||||
|
messageOption = chosenPlan.plan.packet3Options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_messageDate(state.chatMessages[index].updatedAt),
|
_messageDate(message.updatedAt),
|
||||||
|
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
|
||||||
|
PlanOrderMessageWidget(
|
||||||
|
planName: chosenPlan.plan.name,
|
||||||
|
planSlug: chosenPlan.plan.slug,
|
||||||
|
price: messageOption.price,
|
||||||
|
sendTime: message.updatedAt,
|
||||||
|
isCurrentUser: isCurrentUserAuthor,
|
||||||
|
),
|
||||||
VisibilityDetector(
|
VisibilityDetector(
|
||||||
key: Key(state.chatMessages[index].id.toString()),
|
key: Key(message.id.toString()),
|
||||||
onVisibilityChanged: (VisibilityInfo info) {
|
onVisibilityChanged: (VisibilityInfo info) {
|
||||||
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
||||||
unreadMessagesIdsCopy.add((id: state.chatMessages[index].id, index: index));
|
unreadMessagesIdsCopy.add((id: message.id, index: index));
|
||||||
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
||||||
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy);
|
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
@@ -242,44 +265,76 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: ChatBubbleWidget(
|
child: ChatBubbleWidget(
|
||||||
isRead: state.chatMessages[index].isRead,
|
isRead: message.isRead,
|
||||||
sendTime: state.chatMessages[index].updatedAt,
|
sendTime: message.updatedAt,
|
||||||
text: state.chatMessages[index].data.trim(),
|
text: message.data.trim(),
|
||||||
isCurrentUser: isCurrentUserAuthor,
|
isCurrentUser: isCurrentUserAuthor,
|
||||||
mediaData: state.chatMessages[index].mediaData ?? [],
|
mediaData: message.mediaData ?? [],
|
||||||
messageState: state.chatMessages[index].messageState,
|
messageState: message.messageState,
|
||||||
messageUuid: state.chatMessages[index].uuid ?? '',
|
messageUuid: message.uuid ?? '',
|
||||||
userSlug: state.chatRoom.users.receiver.slug,
|
userSlug: state.chatRoom.users.receiver.slug,
|
||||||
|
messageType: message.type,
|
||||||
|
isConsultant: amIConsultant,
|
||||||
|
urlsMediaInfo: message.urlsMediaInfo,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
||||||
return VisibilityDetector(
|
final message = state.chatMessages[index];
|
||||||
key: Key(state.chatMessages[index].id.toString()),
|
final isPlanOrderMessage = message.type == 'plan_order';
|
||||||
onVisibilityChanged: (VisibilityInfo info) {
|
|
||||||
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
PacketOptions? messageOption;
|
||||||
unreadMessagesIds.add((id: state.chatMessages[index].id, index: index));
|
if (isPlanOrderMessage && chosenPlan != null) {
|
||||||
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
if (chosenPlan.optionType == 'options1') {
|
||||||
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
|
messageOption = chosenPlan.plan.packet1Options;
|
||||||
if (!result) return;
|
} else if (chosenPlan.optionType == 'options2') {
|
||||||
if (!context.mounted) return;
|
messageOption = chosenPlan.plan.packet2Options;
|
||||||
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
|
} else if (chosenPlan.optionType == 'options3') {
|
||||||
unreadMessagesIds.clear();
|
messageOption = chosenPlan.plan.packet3Options;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
child: ChatBubbleWidget(
|
return Column(
|
||||||
isRead: state.chatMessages[index].isRead,
|
children: [
|
||||||
sendTime: state.chatMessages[index].updatedAt,
|
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
|
||||||
text: state.chatMessages[index].data.trim(),
|
PlanOrderMessageWidget(
|
||||||
isCurrentUser: isCurrentUserAuthor,
|
planName: chosenPlan.plan.name,
|
||||||
mediaData: state.chatMessages[index].mediaData ?? [],
|
planSlug: chosenPlan.plan.slug,
|
||||||
messageState: state.chatMessages[index].messageState,
|
price: messageOption.price,
|
||||||
messageUuid: state.chatMessages[index].uuid ?? '',
|
sendTime: message.updatedAt,
|
||||||
userSlug: state.chatRoom.users.receiver.slug,
|
isCurrentUser: isCurrentUserAuthor,
|
||||||
),
|
),
|
||||||
|
VisibilityDetector(
|
||||||
|
key: Key(message.id.toString()),
|
||||||
|
onVisibilityChanged: (VisibilityInfo info) {
|
||||||
|
if (!message.isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
|
||||||
|
unreadMessagesIds.add((id: message.id, index: index));
|
||||||
|
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
||||||
|
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
|
||||||
|
if (!result) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
|
||||||
|
unreadMessagesIds.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ChatBubbleWidget(
|
||||||
|
isRead: message.isRead,
|
||||||
|
sendTime: message.updatedAt,
|
||||||
|
text: message.data.trim(),
|
||||||
|
isCurrentUser: isCurrentUserAuthor,
|
||||||
|
mediaData: message.mediaData ?? [],
|
||||||
|
messageState: message.messageState,
|
||||||
|
messageUuid: message.uuid ?? '',
|
||||||
|
userSlug: state.chatRoom.users.receiver.slug,
|
||||||
|
messageType: message.type,
|
||||||
|
isConsultant: amIConsultant,
|
||||||
|
urlsMediaInfo: message.urlsMediaInfo,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
|
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
|
||||||
@@ -407,34 +462,30 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SizedBox(
|
child: TextFormField(
|
||||||
height: 40,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
child: TextFormField(
|
controller: textEditingController,
|
||||||
expands: true,
|
maxLines: 3,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
minLines: 1,
|
||||||
controller: textEditingController,
|
keyboardType: TextInputType.multiline,
|
||||||
maxLines: null,
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
|
||||||
minLines: null,
|
decoration: InputDecoration(
|
||||||
keyboardType: TextInputType.multiline,
|
isDense: true,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
decoration: InputDecoration(
|
hintText: 'Сообщение',
|
||||||
isDense: true,
|
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
filled: true,
|
||||||
hintText: 'Сообщение',
|
border: OutlineInputBorder(
|
||||||
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
borderRadius: BorderRadius.circular(100),
|
||||||
filled: true,
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
border: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(100),
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
borderRadius: BorderRadius.circular(100),
|
||||||
),
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
enabledBorder: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(100),
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
borderRadius: BorderRadius.circular(100),
|
||||||
),
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(100),
|
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ class ChatsScreen extends StatefulWidget {
|
|||||||
class _ChatsScreenState extends State<ChatsScreen> {
|
class _ChatsScreenState extends State<ChatsScreen> {
|
||||||
final searchController = TextEditingController();
|
final searchController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Инициализация ChatsCubit при первом открытии экрана
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final cubit = context.read<ChatsCubit>();
|
||||||
|
final state = cubit.state;
|
||||||
|
if (state.state == BaseState.initial) {
|
||||||
|
cubit.getChatRoomsWithLoading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ChatsCubit, ChatsState>(
|
return BlocBuilder<ChatsCubit, ChatsState>(
|
||||||
|
|||||||
@@ -79,34 +79,30 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SizedBox(
|
child: TextFormField(
|
||||||
height: 40,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
child: TextFormField(
|
controller: textEditingController,
|
||||||
expands: true,
|
maxLines: 3,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
minLines: 1,
|
||||||
controller: textEditingController,
|
keyboardType: TextInputType.multiline,
|
||||||
maxLines: null,
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
|
||||||
minLines: null,
|
decoration: InputDecoration(
|
||||||
keyboardType: TextInputType.multiline,
|
isDense: true,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
decoration: InputDecoration(
|
hintText: 'Сообщение',
|
||||||
isDense: true,
|
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
filled: true,
|
||||||
hintText: 'Сообщение',
|
border: OutlineInputBorder(
|
||||||
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
borderRadius: BorderRadius.circular(100),
|
||||||
filled: true,
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
border: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(100),
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
borderRadius: BorderRadius.circular(100),
|
||||||
),
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
enabledBorder: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(100),
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
borderRadius: BorderRadius.circular(100),
|
||||||
),
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(100),
|
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class AboutOrderButton extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: appTheme.gray50,
|
color: const Color(0xFFFFFDD8),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
|||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.dart';
|
import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.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';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
@@ -24,7 +25,10 @@ class ChatBubbleWidget extends StatelessWidget {
|
|||||||
required this.isRead,
|
required this.isRead,
|
||||||
required this.messageState,
|
required this.messageState,
|
||||||
required this.messageUuid,
|
required this.messageUuid,
|
||||||
required this.userSlug
|
required this.userSlug,
|
||||||
|
this.messageType,
|
||||||
|
this.isConsultant,
|
||||||
|
this.urlsMediaInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
final DateTime sendTime;
|
final DateTime sendTime;
|
||||||
@@ -35,6 +39,9 @@ class ChatBubbleWidget extends StatelessWidget {
|
|||||||
final MessageState messageState;
|
final MessageState messageState;
|
||||||
final String messageUuid;
|
final String messageUuid;
|
||||||
final String userSlug;
|
final String userSlug;
|
||||||
|
final String? messageType;
|
||||||
|
final bool? isConsultant;
|
||||||
|
final List<Map<String, dynamic>>? urlsMediaInfo;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
@@ -102,21 +109,104 @@ class ChatBubbleWidget extends StatelessWidget {
|
|||||||
slivers: [
|
slivers: [
|
||||||
SliverList.separated(
|
SliverList.separated(
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
if (!mediaData[index].toString().contains('.png') &&
|
final mediaItem = mediaData[index];
|
||||||
!mediaData[index].toString().contains('.jpg') &&
|
final isImage = mediaItem is File
|
||||||
!mediaData[index].toString().contains('.jpeg')) {
|
? (mediaItem.path.toLowerCase().endsWith('.png') ||
|
||||||
|
mediaItem.path.toLowerCase().endsWith('.jpg') ||
|
||||||
|
mediaItem.path.toLowerCase().endsWith('.jpeg') ||
|
||||||
|
mediaItem.path.toLowerCase().endsWith('.gif') ||
|
||||||
|
mediaItem.path.toLowerCase().endsWith('.webp'))
|
||||||
|
: (mediaItem.toString().toLowerCase().contains('.png') ||
|
||||||
|
mediaItem.toString().toLowerCase().contains('.jpg') ||
|
||||||
|
mediaItem.toString().toLowerCase().contains('.jpeg') ||
|
||||||
|
mediaItem.toString().toLowerCase().contains('.gif') ||
|
||||||
|
mediaItem.toString().toLowerCase().contains('.webp'));
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
// Получаем имя файла
|
||||||
|
String fileName;
|
||||||
|
if (mediaItem is File) {
|
||||||
|
fileName = mediaItem.path.split('/').last;
|
||||||
|
} else {
|
||||||
|
final urlString = mediaItem.toString();
|
||||||
|
// Получаем расширение файла из URL
|
||||||
|
String? fileExtension;
|
||||||
|
final urlParts = urlString.split('/');
|
||||||
|
if (urlParts.isNotEmpty) {
|
||||||
|
final lastPart = urlParts.last.split('?').first;
|
||||||
|
final dotIndex = lastPart.lastIndexOf('.');
|
||||||
|
if (dotIndex != -1 && dotIndex < lastPart.length - 1) {
|
||||||
|
fileExtension = lastPart.substring(dotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся найти имя файла в urls_media_info
|
||||||
|
String? fileNameFromInfo;
|
||||||
|
final mediaInfo = urlsMediaInfo;
|
||||||
|
if (mediaInfo != null && mediaInfo.isNotEmpty) {
|
||||||
|
for (final mediaInfoMap in mediaInfo) {
|
||||||
|
// Каждый элемент urls_media_info - это Map, где ключ - это URL
|
||||||
|
if (mediaInfoMap.isNotEmpty) {
|
||||||
|
for (final entry in mediaInfoMap.entries) {
|
||||||
|
final infoUrl = entry.key.toString();
|
||||||
|
// Сравниваем полные URL или только имя файла
|
||||||
|
if (infoUrl == urlString ||
|
||||||
|
urlString.contains(infoUrl) ||
|
||||||
|
infoUrl.contains(urlString) ||
|
||||||
|
urlString.split('/').last == infoUrl.split('/').last) {
|
||||||
|
final fileInfo = entry.value;
|
||||||
|
if (fileInfo is Map<String, dynamic> && fileInfo.containsKey('name')) {
|
||||||
|
final nameValue = fileInfo['name'];
|
||||||
|
if (nameValue != null) {
|
||||||
|
fileNameFromInfo = nameValue.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileNameFromInfo != null && fileNameFromInfo.isNotEmpty) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileNameFromInfo != null && fileNameFromInfo.isNotEmpty) {
|
||||||
|
// Добавляем расширение к названию файла, если его там нет
|
||||||
|
if (fileExtension != null && !fileNameFromInfo.endsWith(fileExtension)) {
|
||||||
|
fileName = '$fileNameFromInfo$fileExtension';
|
||||||
|
} else {
|
||||||
|
fileName = fileNameFromInfo;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback к старой логике, если информации нет
|
||||||
|
fileName = urlString.contains('/') ? urlString.split('/').last.split('?').first : urlString.replaceAll('chat_files/', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.all(4),
|
contentPadding: const EdgeInsets.all(4),
|
||||||
horizontalTitleGap: 8,
|
horizontalTitleGap: 8,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (!await launchUrl(Uri.parse('${mediaData[index]}'))) {
|
if (mediaItem is File) {
|
||||||
throw Exception('Could not launch ${mediaData[index].toString()}');
|
// Для локальных файлов используем file:// URI
|
||||||
|
final fileUri = Uri.file(mediaItem.path);
|
||||||
|
if (await canLaunchUrl(fileUri)) {
|
||||||
|
await launchUrl(fileUri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Для URL файлов используем обычный launchUrl
|
||||||
|
final urlString = mediaItem.toString();
|
||||||
|
final uri = urlString.startsWith('http://') || urlString.startsWith('https://')
|
||||||
|
? Uri.parse(urlString)
|
||||||
|
: Uri.parse('https://$urlString');
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leading: const Icon(Icons.file_copy_outlined, color: Colors.black),
|
leading: const Icon(Icons.file_copy_outlined, color: Colors.black),
|
||||||
trailing: const Icon(Icons.download_outlined, color: Colors.black),
|
trailing: const Icon(Icons.download_outlined, color: Colors.black),
|
||||||
title: Text(
|
title: Text(
|
||||||
mediaData[index].toString().replaceAll('chat_files/', ''),
|
fileName,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis),
|
style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis),
|
||||||
),
|
),
|
||||||
@@ -124,16 +214,16 @@ class ChatBubbleWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushTransparentRoute(
|
// Определяем, все ли изображения локальные
|
||||||
PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false),
|
final allLocal = mediaData.every((item) => item is File);
|
||||||
);
|
context.pushTransparentRoute(PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: allLocal));
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: '${mediaData[index]}',
|
tag: '${mediaData[index]}',
|
||||||
transitionOnUserGestures: true,
|
transitionOnUserGestures: true,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: mediaData[index] is File ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'),
|
child: mediaItem is File ? Image.file(mediaItem) : CachedNetworkImage(imageUrl: '$mediaItem'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -144,8 +234,8 @@ class ChatBubbleWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (mediaData.isNotEmpty && text.isNotEmpty) const SizedBox(height: 8),
|
if (mediaData.isNotEmpty && _getDisplayText().isNotEmpty) const SizedBox(height: 8),
|
||||||
if (text.isNotEmpty) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)),
|
if (_getDisplayText().isNotEmpty) _buildLinkableText(_getDisplayText()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -163,4 +253,77 @@ class ChatBubbleWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getDisplayText() {
|
||||||
|
if (messageType == 'plan_order') {
|
||||||
|
if (isConsultant == true) {
|
||||||
|
// Текст для консультанта
|
||||||
|
return '1. Уточните у подопечного его цели и пожелания\n'
|
||||||
|
'2. Согласуйте и выберите дату старта плана\n'
|
||||||
|
'3. Ожидайте уведомления об успешной оплате\n'
|
||||||
|
'4. После оплаты предоставьте необходимые данные, согласно вашему плану питания';
|
||||||
|
} else {
|
||||||
|
// Текст для пользователя
|
||||||
|
return 'Укажите вашу цель в работе с консультантом\n'
|
||||||
|
'Согласуйте удобную дату старта плана, после чего вам будет направлена ссылка на оплату.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLinkableText(String text) {
|
||||||
|
final urlRegex = RegExp(
|
||||||
|
r'(?:(?:https?|ftp):\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final textStyle = TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16);
|
||||||
|
final linkStyle = textStyle.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline);
|
||||||
|
|
||||||
|
final matches = urlRegex.allMatches(text);
|
||||||
|
if (matches.isEmpty) {
|
||||||
|
return Text(text, style: textStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
final spans = <TextSpan>[];
|
||||||
|
int lastEnd = 0;
|
||||||
|
|
||||||
|
for (final match in matches) {
|
||||||
|
// Добавляем текст до ссылки
|
||||||
|
if (match.start > lastEnd) {
|
||||||
|
spans.add(TextSpan(text: text.substring(lastEnd, match.start), style: textStyle));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем кликабельную ссылку
|
||||||
|
final url = match.group(0)!;
|
||||||
|
final fullUrl = url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://') ? url : 'https://$url';
|
||||||
|
|
||||||
|
spans.add(
|
||||||
|
TextSpan(
|
||||||
|
text: url,
|
||||||
|
style: linkStyle,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () async {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(fullUrl);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Игнорируем ошибки открытия ссылок
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
lastEnd = match.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем оставшийся текст после последней ссылки
|
||||||
|
if (lastEnd < text.length) {
|
||||||
|
spans.add(TextSpan(text: text.substring(lastEnd), style: textStyle));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RichText(text: TextSpan(children: spans));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
if (widget.hasPersonalDiet) {
|
if (widget.hasPersonalDiet) {
|
||||||
startDate = [DateTime.now().add(const Duration(days: 2))];
|
startDate = [DateTime.now().add(const Duration(days: 2))];
|
||||||
|
} else {
|
||||||
|
startDate = [DateTime.now().add(const Duration(days: 1))];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,23 +51,12 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(planName ?? '', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
planName ?? '',
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(
|
|
||||||
color: appTheme.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.hasPersonalDiet) ...[
|
if (widget.hasPersonalDiet) ...[
|
||||||
const SizedBox(
|
const SizedBox(width: 8),
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
PortalTarget(
|
PortalTarget(
|
||||||
closeDuration: const Duration(milliseconds: 20),
|
closeDuration: const Duration(milliseconds: 20),
|
||||||
anchor: const Aligned(
|
anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomLeft),
|
||||||
shiftToWithinBound: AxisFlag(x: true, y: true),
|
|
||||||
follower: Alignment.topRight,
|
|
||||||
target: Alignment.bottomLeft,
|
|
||||||
),
|
|
||||||
portalFollower: Padding(
|
portalFollower: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -73,24 +64,12 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: visible ? appTheme.whiteA700 : Colors.transparent,
|
color: visible ? appTheme.whiteA700 : Colors.transparent,
|
||||||
boxShadow: visible
|
boxShadow: visible ? [BoxShadow(color: appTheme.black900.withValues(alpha: 0.15), blurRadius: 4, offset: const Offset(0, 4))] : null,
|
||||||
? [
|
|
||||||
BoxShadow(
|
|
||||||
color: appTheme.black900.withValues(alpha: 0.15),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
child: AnimatedDefaultTextStyle(
|
child: AnimatedDefaultTextStyle(
|
||||||
style: TextStyles.textMMediumAuto.copyWith(
|
style: TextStyles.textMMediumAuto.copyWith(color: visible ? appTheme.black : Colors.transparent),
|
||||||
color: visible ? appTheme.black : Colors.transparent,
|
|
||||||
),
|
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: Text(
|
child: Text(s.chosenPlanStartDateDescr),
|
||||||
s.chosenPlanStartDateDescr,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -104,14 +83,11 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
|||||||
'assets/icons/question_mark.svg',
|
'assets/icons/question_mark.svg',
|
||||||
width: 22,
|
width: 22,
|
||||||
height: 22,
|
height: 22,
|
||||||
colorFilter: ColorFilter.mode(
|
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -120,19 +96,12 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
s.chose_plan_start_date_2,
|
s.chose_plan_start_date_2,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyles.textMMedium.copyWith(
|
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||||
color: appTheme.black,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Padding(
|
const Padding(padding: EdgeInsets.symmetric(horizontal: 16.0), child: CustomDivider()),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
CalendarWidget(startDate: startDate),
|
||||||
child: CustomDivider(),
|
|
||||||
),
|
|
||||||
CalendarWidget(
|
|
||||||
startDate: startDate,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
|
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
|
||||||
child: CustomOutlinedButton(
|
child: CustomOutlinedButton(
|
||||||
@@ -150,13 +119,8 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
|
|||||||
},
|
},
|
||||||
height: 60,
|
height: 60,
|
||||||
text: s.confirm,
|
text: s.confirm,
|
||||||
buttonTextStyle: TextStyles.textLSemibold.copyWith(
|
buttonTextStyle: TextStyles.textLSemibold.copyWith(color: appTheme.whiteA700),
|
||||||
color: appTheme.whiteA700,
|
buttonStyle: ButtonStyle(backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), side: const WidgetStatePropertyAll(BorderSide.none)),
|
||||||
),
|
|
||||||
buttonStyle: ButtonStyle(
|
|
||||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
|
||||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
|||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: appTheme.gray50,
|
color: const Color(0xFFFFFDD8),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class PlanOrderMessageWidget extends StatelessWidget {
|
||||||
|
const PlanOrderMessageWidget({
|
||||||
|
super.key,
|
||||||
|
required this.planName,
|
||||||
|
required this.planSlug,
|
||||||
|
required this.price,
|
||||||
|
required this.sendTime,
|
||||||
|
required this.isCurrentUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String planName;
|
||||||
|
final String planSlug;
|
||||||
|
final dynamic price;
|
||||||
|
final DateTime sendTime;
|
||||||
|
final bool isCurrentUser;
|
||||||
|
|
||||||
|
String _formatPrice(dynamic price) {
|
||||||
|
if (price == null) return '0₽';
|
||||||
|
if (price is num) {
|
||||||
|
return '${price.toStringAsFixed(0)}₽';
|
||||||
|
}
|
||||||
|
if (price is String) {
|
||||||
|
final numPrice = num.tryParse(price);
|
||||||
|
if (numPrice != null) {
|
||||||
|
return '${numPrice.toStringAsFixed(0)}₽';
|
||||||
|
}
|
||||||
|
return price.endsWith('₽') ? price : '$price₽';
|
||||||
|
}
|
||||||
|
return '0₽';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
final formattedPrice = _formatPrice(price);
|
||||||
|
final textStyle = TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16);
|
||||||
|
final linkStyle = textStyle.copyWith(
|
||||||
|
color: appTheme.blueA400,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: size.width,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(isCurrentUser ? 64 : 16, 10, isCurrentUser ? 16 : 64, 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (isCurrentUser)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4, bottom: 4),
|
||||||
|
child: Text(
|
||||||
|
sendTime.format('HH:mm', 'ru'),
|
||||||
|
style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: size.width * 0.7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isCurrentUser ? appTheme.gray50 : appTheme.gray60004,
|
||||||
|
borderRadius: BorderRadius.horizontal(
|
||||||
|
left: isCurrentUser ? const Radius.circular(32) : const Radius.circular(16),
|
||||||
|
right: isCurrentUser ? const Radius.circular(16) : const Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
left: isCurrentUser ? 16 : 8,
|
||||||
|
right: isCurrentUser ? 8 : 16,
|
||||||
|
),
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: textStyle,
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: 'Я хочу заказать '),
|
||||||
|
TextSpan(
|
||||||
|
text: planName,
|
||||||
|
style: linkStyle,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'aboutPlanScreen',
|
||||||
|
extra: {
|
||||||
|
'slug': planSlug,
|
||||||
|
'index': null,
|
||||||
|
'navigateToComments': false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextSpan(text: ' по цене $formattedPrice'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isCurrentUser)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4, bottom: 4),
|
||||||
|
child: Text(
|
||||||
|
sendTime.format('HH:mm', 'ru'),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF7B8794),
|
||||||
|
height: 1,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user