Compare commits
3 Commits
fix/bug-fi
...
fix/ios-la
| Author | SHA1 | Date | |
|---|---|---|---|
| bb49bbd0f7 | |||
| 7779803ccc | |||
| 713cc2520b |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 336 B |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 67 KiB |
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
#include? "Secrets.xcconfig"
|
||||
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
|
||||
// Переопределение Pods/ML Kit: arm64 для симулятора на Apple Silicon (M1/M4)
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||
|
||||
5
ios/Flutter/Profile.xcconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
#include? "Secrets.xcconfig"
|
||||
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||
@@ -1 +1,5 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
#include? "Secrets.xcconfig"
|
||||
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||
|
||||
2
ios/Flutter/Secrets.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
// Локальные переопределения (опционально). Не задавайте YANDEX_MAPKIT_API_KEY плейсхолдером —
|
||||
// рабочий ключ задаётся в конце Debug/Release/Profile.xcconfig после этого include.
|
||||
3
ios/Flutter/Secrets.xcconfig.example
Normal file
@@ -0,0 +1,3 @@
|
||||
// Скопируй в ios/Flutter/Secrets.xcconfig (файл в .gitignore) для своих локальных xcconfig-переменных.
|
||||
// Ключ MapKit задаётся в конце Debug/Release/Profile.xcconfig — не подставляйте туда тестовый плейсхолдер вместо реального ключа.
|
||||
// https://developer.tech.yandex.ru/
|
||||
48
ios/Podfile
Normal file
@@ -0,0 +1,48 @@
|
||||
# image_picker_ios и др. требуют минимум iOS 13
|
||||
platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
# ML Kit исключает arm64 для симулятора → сборка только x86_64.
|
||||
# На M1 это ещё могло работать через Rosetta; на M4 симулятор только arm64.
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'i386'
|
||||
end
|
||||
end
|
||||
end
|
||||
81
ios/Podfile.lock
Normal file
@@ -0,0 +1,81 @@
|
||||
PODS:
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- yandex_mapkit (0.0.1):
|
||||
- Flutter
|
||||
- YandexMapsMobile (= 4.22.0-lite)
|
||||
- YandexMapsMobile (4.22.0-lite)
|
||||
|
||||
DEPENDENCIES:
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- yandex_mapkit (from `.symlinks/plugins/yandex_mapkit/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- YandexMapsMobile
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
geolocator_apple:
|
||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
yandex_mapkit:
|
||||
:path: ".symlinks/plugins/yandex_mapkit/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
yandex_mapkit: bac34ca1bdf97e87252a8c6d09a95b1fe39ba103
|
||||
YandexMapsMobile: c73844c6096bbb240d1491adc511c2ef4a88d88d
|
||||
|
||||
PODFILE CHECKSUM: 715b1068ab8815ca4115a24e305e836709652b69
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
23
ios/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Запуск на симуляторе (Mac)
|
||||
|
||||
**Терминал** (⌘ + `):
|
||||
|
||||
```bash
|
||||
cd /Users/commercesparkit.by/Desktop/projekt/be_happy_public
|
||||
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
||||
open -a Simulator
|
||||
flutter run
|
||||
```
|
||||
|
||||
Первый раз или после обновления кода:
|
||||
|
||||
```bash
|
||||
cd /Users/commercesparkit.by/Desktop/projekt/be_happy_public
|
||||
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
||||
flutter pub get
|
||||
cd ios && pod install && cd ..
|
||||
open -a Simulator
|
||||
flutter run
|
||||
```
|
||||
|
||||
Важно: команды только из папки `be_happy_public`, не из `~`.
|
||||
@@ -10,10 +10,12 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
6D33173F8CCB65895D564988 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BB22D9E6003D7254B27B141 /* Pods_RunnerTests.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
FB3C1019114E649A3758FC7D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 151F40E0A8A0B88D7F9715D8 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -40,14 +42,20 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0EB91AA8815BC4C23E9C769C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
151F40E0A8A0B88D7F9715D8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4BB22D9E6003D7254B27B141 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4D0302F803C0949E93DDCE44 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
525F1415727A8E2A6F5CA828 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB41CF90195004384FC /* Profile.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Profile.xcconfig; path = Flutter/Profile.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -55,13 +63,25 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BAF1C86965EF66AE2F0839BD /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
BE9A8C77364E79EA4C6EE36B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
D24CB1920C4748F67C854B22 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
83F4E53737A5B4793588BFEB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6D33173F8CCB65895D564988 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FB3C1019114E649A3758FC7D /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -82,6 +102,7 @@
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB41CF90195004384FC /* Profile.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
@@ -94,6 +115,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
991E98147265C7C44D711731 /* Pods */,
|
||||
E94B50B8FA90842D98EF918A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -121,6 +144,29 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
991E98147265C7C44D711731 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4D0302F803C0949E93DDCE44 /* Pods-Runner.debug.xcconfig */,
|
||||
0EB91AA8815BC4C23E9C769C /* Pods-Runner.release.xcconfig */,
|
||||
525F1415727A8E2A6F5CA828 /* Pods-Runner.profile.xcconfig */,
|
||||
BAF1C86965EF66AE2F0839BD /* Pods-RunnerTests.debug.xcconfig */,
|
||||
D24CB1920C4748F67C854B22 /* Pods-RunnerTests.release.xcconfig */,
|
||||
BE9A8C77364E79EA4C6EE36B /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E94B50B8FA90842D98EF918A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
151F40E0A8A0B88D7F9715D8 /* Pods_Runner.framework */,
|
||||
4BB22D9E6003D7254B27B141 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -128,8 +174,10 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
1D742F73AAFC432DF3836D7E /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
83F4E53737A5B4793588BFEB /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -145,12 +193,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
8734FCBECF65F25CB259DF82 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
25F6FF3856E913FFAF963081 /* [CP] Embed Pods Frameworks */,
|
||||
FF12327738E91E5AA87207B8 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -222,6 +273,45 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
1D742F73AAFC432DF3836D7E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
25F6FF3856E913FFAF963081 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -238,6 +328,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
8734FCBECF65F25CB259DF82 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -253,6 +365,23 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
FF12327738E91E5AA87207B8 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -346,7 +475,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -357,7 +486,7 @@
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
baseConfigurationReference = 9740EEB41CF90195004384FC /* Profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
@@ -378,6 +507,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BAF1C86965EF66AE2F0839BD /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -395,6 +525,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D24CB1920C4748F67C854B22 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -410,6 +541,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BE9A8C77364E79EA4C6EE36B /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -427,7 +559,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -472,7 +604,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -484,7 +616,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -523,7 +655,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import YandexMapsMobile
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
let apiKey =
|
||||
(Bundle.main.object(forInfoDictionaryKey: "YandexMapKitApiKey") as? String ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
#if DEBUG
|
||||
if apiKey.isEmpty || apiKey.contains("$(") {
|
||||
assertionFailure(
|
||||
"YandexMapKitApiKey пустой или не подставился из xcconfig — проверь YANDEX_MAPKIT_API_KEY в ios/Flutter/*.xcconfig"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
YMKMapKit.setLocale(Locale.current.identifier)
|
||||
YMKMapKit.setApiKey(apiKey)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,122 @@
|
||||
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -24,6 +26,29 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -41,9 +66,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>YandexMapKitApiKey</key>
|
||||
<string>$(YANDEX_MAPKIT_API_KEY)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'package:path/path.dart';
|
||||
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
|
||||
import 'package:flutter_client_sse/flutter_client_sse.dart';
|
||||
|
||||
import '../../domain/entities/client_subscription.dart';
|
||||
import '../../domain/entities/point.dart';
|
||||
import '../../domain/entities/user_profile.dart';
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
@@ -138,8 +137,7 @@ class ApiService {
|
||||
if (avatarId != null && profileData["avatar"] != null) {
|
||||
final String? avatarPath = profileData["avatar"]["path"];
|
||||
if (avatarPath != null && avatarPath.isNotEmpty) {
|
||||
avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString();
|
||||
}
|
||||
avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); }
|
||||
}
|
||||
|
||||
dynamic balanceRaw = profileData["balance"];
|
||||
@@ -396,7 +394,10 @@ class ApiService {
|
||||
final url = "$baseUrl/scooter/$title/code";
|
||||
|
||||
try {
|
||||
final response = await _dio.get(url, options: await _getAuthOptions());
|
||||
final response = await _dio.get(
|
||||
url,
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return Scooter.fromJson(response.data);
|
||||
@@ -462,7 +463,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ClientSubscription>> getClientSubscriptions() async {
|
||||
Future<List<Subscription>> getClientSubscriptions() async {
|
||||
const url = "$baseUrl/scootersubscription/client";
|
||||
|
||||
try {
|
||||
@@ -473,7 +474,14 @@ class ApiService {
|
||||
final List<dynamic> items = responseData['data'] ?? [];
|
||||
|
||||
return items.map((item) {
|
||||
return ClientSubscription.fromJson(item);
|
||||
final Map<String, dynamic> subscriptionMap =
|
||||
Map<String, dynamic>.from(item['subscription'] ?? {});
|
||||
|
||||
if (item['expiredAt'] != null) {
|
||||
subscriptionMap['activeTo'] = item['expiredAt'];
|
||||
}
|
||||
|
||||
return Subscription.fromJson(subscriptionMap);
|
||||
}).toList();
|
||||
}
|
||||
return [];
|
||||
@@ -539,7 +547,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addPaymentCard({
|
||||
Future<int> addPaymentCard({
|
||||
required String cardNumber,
|
||||
required String cardHolder,
|
||||
required int expirationMonth,
|
||||
@@ -563,9 +571,8 @@ class ApiService {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return;
|
||||
return response.data['id'] as int;
|
||||
}
|
||||
|
||||
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
|
||||
} on DioException catch (e) {
|
||||
final data = e.response?.data;
|
||||
@@ -667,9 +674,7 @@ class ApiService {
|
||||
final firstError = data['message'][0]['message'].toString();
|
||||
|
||||
if (firstError.contains("Wrong start zone")) {
|
||||
throw WrongZoneException(
|
||||
message: "Некорректная зона для начала поездки.",
|
||||
);
|
||||
throw WrongZoneException(message: "Некорректная зона для начала поездки.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,9 +705,7 @@ class ApiService {
|
||||
final firstError = data['message'][0]['message'].toString();
|
||||
|
||||
if (firstError.contains("Wrong start zone")) {
|
||||
throw WrongZoneException(
|
||||
message: "Некорректная зона для начала поездки.",
|
||||
);
|
||||
throw WrongZoneException(message: "Некорректная зона для начала поездки.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,9 +791,7 @@ class ApiService {
|
||||
final firstError = data['message'][0]['message'].toString();
|
||||
|
||||
if (firstError.contains("Wrong start zone")) {
|
||||
throw WrongZoneException(
|
||||
message: "Некорректная зона для завершения поездки.",
|
||||
);
|
||||
throw WrongZoneException(message: "Некорректная зона для завершения поездки.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,7 +803,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payRide(int orderId) async {
|
||||
Future<ScooterOrder?> payRide(int orderId) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
"$baseUrl/scooterorder/$orderId/pay",
|
||||
@@ -810,12 +811,12 @@ class ApiService {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
// return ScooterOrder.fromJson(response.data);
|
||||
return;
|
||||
return ScooterOrder.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
} on DioException catch (e) {
|
||||
_handleDioError(e);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,7 +878,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payScooterOrderWithPhotos({
|
||||
Future<ScooterOrder?> payScooterOrderWithPhotos({
|
||||
required int orderId,
|
||||
required int? cardId,
|
||||
required bool isBalance,
|
||||
@@ -890,10 +891,12 @@ class ApiService {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return;
|
||||
return ScooterOrder.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
} on DioException catch (e) {
|
||||
_handleDioError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,6 +918,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<List<Point>> getScooterOrderRouteHistory({required int id}) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
@@ -926,19 +930,13 @@ class ApiService {
|
||||
final String routeString = response.data['route'] ?? '[]';
|
||||
final List<dynamic> routeList = json.decode(routeString);
|
||||
|
||||
return routeList
|
||||
.map(
|
||||
(item) => Point(
|
||||
(item[1] as num).toDouble(),
|
||||
(item[0] as num).toDouble(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return routeList.map((item) => Point(
|
||||
(item[1] as num).toDouble(),
|
||||
(item[0] as num).toDouble(),
|
||||
)).toList();
|
||||
}
|
||||
|
||||
throw RouteHistoryNotFoundException(
|
||||
message: "История маршрута не найдена",
|
||||
);
|
||||
throw RouteHistoryNotFoundException(message: "История маршрута не найдена");
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 401) throw UnauthorizedException();
|
||||
if (e.response?.statusCode == 403) throw AuthBlockException();
|
||||
@@ -1040,57 +1038,6 @@ class ApiService {
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getNotifications() async {
|
||||
final url = Uri.parse('$baseUrl/notification/client');
|
||||
|
||||
final accessToken = await _securityService.getAccessToken();
|
||||
if (accessToken == null) {
|
||||
print("APISERVICE Error: Access token is null.");
|
||||
throw UnauthorizedException();
|
||||
}
|
||||
|
||||
print("GET NOTIFICATIONS REQUEST:");
|
||||
print("URL: $url");
|
||||
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer $accessToken",
|
||||
},
|
||||
);
|
||||
|
||||
print("GET NOTIFICATIONS RESPONSE:");
|
||||
print("STATUS: ${response.statusCode}");
|
||||
print("BODY: ${response.body}");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
|
||||
// ✅ Проверяем, является ли ответ массивом или объектом с data[]
|
||||
if (data is List) {
|
||||
return data.cast<Map<String, dynamic>>();
|
||||
} else if (data is Map<String, dynamic>) {
|
||||
final list = data['data'];
|
||||
if (list is List) {
|
||||
return list.cast<Map<String, dynamic>>();
|
||||
} else {
|
||||
throw Exception(
|
||||
'Expected a List under "data" but got ${list.runtimeType}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception('Expected a List or Map but got ${data.runtimeType}');
|
||||
}
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 403) {
|
||||
throw AuthBlockException();
|
||||
}
|
||||
|
||||
throw Exception('Ошибка сервера: ${response.statusCode}');
|
||||
}
|
||||
|
||||
Future<List<Certificate>> getCertificates() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
|
||||
@@ -33,22 +33,4 @@ class NotificationRepositoryImpl implements NotificationRepository {
|
||||
void closeStream() {
|
||||
// соединение закрывается автоматически при отписке от stream
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ClientNotification>> getNotifications() async {
|
||||
try {
|
||||
final List<Map<String, dynamic>> data = await _apiService.getNotifications();
|
||||
final notifications = data.map((json) {
|
||||
final dto = ClientNotificationDto.fromJson(json);
|
||||
return dto.toEntity();
|
||||
}).toList();
|
||||
|
||||
// dev.log('NotificationRepository: Загружено ${notifications.length} уведомлений');
|
||||
|
||||
return notifications;
|
||||
} catch (e, stackTrace) {
|
||||
// dev.log('NotificationRepository: Ошибка: $e', stackTrace: stackTrace);
|
||||
throw Exception('Не удалось загрузить уведомления: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
||||
required String cvv,
|
||||
}) async {
|
||||
try {
|
||||
await apiService.addPaymentCard(
|
||||
final cardId = await apiService.addPaymentCard(
|
||||
cardNumber: cardNumber,
|
||||
cardHolder: cardHolder,
|
||||
expirationMonth: int.parse(expiryMonth),
|
||||
@@ -48,7 +48,8 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
||||
cvv: cvv,
|
||||
);
|
||||
|
||||
// await securityService.saveCardFullNumber(cardId, cardNumber);
|
||||
// Сохраняем полный номер карты локально
|
||||
await securityService.saveCardFullNumber(cardId, cardNumber);
|
||||
|
||||
return Success(null);
|
||||
} on AuthException catch (e) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:be_happy/domain/repositories/scooter_repository.dart';
|
||||
import '../../core/failures.dart';
|
||||
import '../../core/result.dart';
|
||||
import '../../domain/entities/active_scooter_order.dart';
|
||||
import '../../domain/entities/client_subscription.dart';
|
||||
import '../../domain/entities/scooter.dart';
|
||||
import '../../domain/entities/tariff.dart';
|
||||
import '../../domain/entities/subscription.dart';
|
||||
@@ -128,8 +127,8 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<ClientSubscription>>> getClientSubscriptions() async {
|
||||
late final Result<List<ClientSubscription>> result;
|
||||
Future<Result<List<Subscription>>> getClientSubscriptions() async {
|
||||
late final Result<List<Subscription>> result;
|
||||
try {
|
||||
final subscriptions = await _apiService.getClientSubscriptions();
|
||||
result = Success(subscriptions);
|
||||
@@ -269,12 +268,15 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> payRide(int orderId) async {
|
||||
late final Result<void> result;
|
||||
Future<Result<ScooterOrder>> payRide(int orderId) async {
|
||||
late final Result<ScooterOrder> result;
|
||||
try {
|
||||
await _apiService.payRide(orderId);
|
||||
result = Success(null);
|
||||
|
||||
final order = await _apiService.payRide(orderId);
|
||||
if (order != null) {
|
||||
result = Success(order);
|
||||
} else {
|
||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
||||
}
|
||||
} on AuthException catch (e) {
|
||||
result = Failure(AuthFailure(e.attemptsLeft));
|
||||
} catch (e) {
|
||||
@@ -333,19 +335,23 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> payScooterOrderWithPhotos({
|
||||
Future<Result<ScooterOrder>> payScooterOrderWithPhotos({
|
||||
required int orderId,
|
||||
required int? cardId,
|
||||
required bool isBalance,
|
||||
}) async {
|
||||
late final Result<void> result;
|
||||
late final Result<ScooterOrder> result;
|
||||
try {
|
||||
final order = await _apiService.payScooterOrderWithPhotos(
|
||||
orderId: orderId,
|
||||
cardId: cardId,
|
||||
isBalance: isBalance,
|
||||
);
|
||||
result = Success(null);
|
||||
if (order != null) {
|
||||
result = Success(order);
|
||||
} else {
|
||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
||||
}
|
||||
} on AuthException catch (e) {
|
||||
result = Failure(AuthFailure(e.attemptsLeft));
|
||||
} catch (e) {
|
||||
|
||||
@@ -92,7 +92,6 @@ import '../domain/service/device_info_service.dart';
|
||||
import '../domain/usecase/activate_subscription_usecase.dart';
|
||||
import '../domain/usecase/get_client_subscriptions_usecase.dart';
|
||||
import '../domain/usecase/get_news_by_id_usecase.dart';
|
||||
import '../domain/usecase/get_notifications_usecase.dart';
|
||||
import '../domain/usecase/get_scooter_by_title_usecase.dart';
|
||||
import '../domain/usecase/get_scooter_order_history_usecase.dart';
|
||||
import '../domain/usecase/remove_payment_card_usecase.dart';
|
||||
@@ -101,7 +100,6 @@ import '../presentation/viewmodel/auth_bloc.dart';
|
||||
import '../presentation/viewmodel/edit_profile_bloc.dart';
|
||||
import '../presentation/viewmodel/map_bloc.dart';
|
||||
import '../presentation/viewmodel/news_bloc.dart';
|
||||
import '../presentation/viewmodel/notifications_bloc.dart';
|
||||
import '../presentation/viewmodel/order_history_bloc.dart';
|
||||
import '../presentation/viewmodel/scooter_detail_modal_bloc.dart';
|
||||
import '../presentation/viewmodel/subscription_list_bloc.dart';
|
||||
@@ -112,7 +110,7 @@ final getIt = GetIt.instance;
|
||||
Future<void> setupDependencies() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final dio = Dio();
|
||||
dio.interceptors.add(LogInterceptor(/*requestHeader: false, responseHeader:false, */responseBody: true, requestBody: true));
|
||||
dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
|
||||
dio.interceptors.add(AuthInterceptor());
|
||||
// HTTP
|
||||
getIt.registerSingleton<http.Client>(http.Client());
|
||||
@@ -288,13 +286,6 @@ Future<void> setupDependencies() async {
|
||||
getIt.registerSingleton<GetScooterByTitleUsecase>(
|
||||
GetScooterByTitleUsecase(getIt()),
|
||||
);
|
||||
getIt.registerSingleton<GetNotificationsUsecase>(
|
||||
GetNotificationsUsecase(getIt<NotificationRepository>()),
|
||||
);
|
||||
getIt.registerFactory<NotificationsBloc>(
|
||||
() => NotificationsBloc(getIt<GetNotificationsUsecase>()),
|
||||
);
|
||||
|
||||
|
||||
// Blocs
|
||||
getIt.registerLazySingleton<SplashBloc>(() => SplashBloc(getIt()));
|
||||
@@ -319,7 +310,6 @@ Future<void> setupDependencies() async {
|
||||
getIt(),
|
||||
getIt(),
|
||||
getIt(),
|
||||
getIt(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:be_happy/domain/entities/subscription.dart';
|
||||
|
||||
class ClientSubscription {
|
||||
final int id;
|
||||
final int subscriptionId;
|
||||
final Subscription subscription;
|
||||
final DateTime? expiredAt;
|
||||
|
||||
ClientSubscription({
|
||||
required this.id,
|
||||
required this.subscriptionId,
|
||||
required this.subscription,
|
||||
this.expiredAt,
|
||||
});
|
||||
|
||||
factory ClientSubscription.fromJson(Map<String, dynamic> json) {
|
||||
return ClientSubscription(
|
||||
id: json['id'] ?? 0,
|
||||
subscriptionId: json['subscriptionId'] ?? 0,
|
||||
subscription: Subscription.fromJson(json['subscription'] as Map<String, dynamic>),
|
||||
expiredAt: json['expiredAt'] != null ? DateTime.parse(json['expiredAt']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,8 @@ class Scooter {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Scooter{id: $id, title: $title, status: $status, latitude: $latitude, longitude: $longitude, batteryLevel: $batteryLevel, isOnline: $isOnline}';
|
||||
return 'Scooter{id: $id, title: $title}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'scooter.dart';
|
||||
class ScooterOrder {
|
||||
final int id;
|
||||
final int scooterId;
|
||||
final Scooter scooter;
|
||||
final Scooter? scooter;
|
||||
final int? planId;
|
||||
final ScooterPlan? plan;
|
||||
final int clientId;
|
||||
@@ -33,7 +33,7 @@ class ScooterOrder {
|
||||
ScooterOrder({
|
||||
required this.id,
|
||||
required this.scooterId,
|
||||
required this.scooter,
|
||||
this.scooter,
|
||||
this.planId,
|
||||
this.plan,
|
||||
required this.clientId,
|
||||
@@ -65,7 +65,7 @@ class ScooterOrder {
|
||||
return ScooterOrder(
|
||||
id: json['id'] ?? 0,
|
||||
scooterId: json['scooterId'] ?? 0,
|
||||
scooter: Scooter.fromJson(json['scooter']),
|
||||
scooter: json['scooter'] != null ? Scooter.fromJson(json['scooter']) : null,
|
||||
planId: json['planId'],
|
||||
plan: json['plan'] != null ? ScooterPlan.fromJson(json['plan']) : null,
|
||||
clientId: json['clientId'] ?? 0,
|
||||
|
||||
@@ -8,7 +8,6 @@ class Subscription {
|
||||
final String fullDescription;
|
||||
final int planId;
|
||||
final bool isActive;
|
||||
final bool isCurrent;
|
||||
final String currency;
|
||||
final DateTime? activeFrom;
|
||||
final DateTime? activeTo;
|
||||
@@ -29,7 +28,6 @@ class Subscription {
|
||||
this.activeTo,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.isCurrent,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
@@ -50,7 +48,6 @@ class Subscription {
|
||||
activeTo: json['activeTo'] != null ? DateTime.parse(json['activeTo']) : null,
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : DateTime.now(),
|
||||
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : DateTime.now(),
|
||||
isCurrent: json['isCurrent'] ?? false,
|
||||
options: optionsData.map((e) => SubscriptionPeriod.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,4 @@ abstract class NotificationRepository {
|
||||
|
||||
/// Закрывает SSE-соединение
|
||||
void closeStream();
|
||||
|
||||
/// получить список уведомлений
|
||||
Future<List<ClientNotification>> getNotifications();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:be_happy/domain/entities/active_scooter_order.dart';
|
||||
|
||||
import '../../core/result.dart';
|
||||
import '../entities/client_subscription.dart';
|
||||
import '../entities/point.dart';
|
||||
import '../entities/scooter.dart';
|
||||
import '../entities/subscription.dart';
|
||||
@@ -17,7 +16,7 @@ abstract class ScooterRepository {
|
||||
Future<Result<List<Tariff>>> getAvailableTariffs(int scooterId);
|
||||
Future<Result<List<Subscription>>> getAvailableSubscriptions();
|
||||
Future<Result<Subscription>> getSubscriptionById(int id);
|
||||
Future<Result<List<ClientSubscription>>> getClientSubscriptions();
|
||||
Future<Result<List<Subscription>>> getClientSubscriptions();
|
||||
Future<Result<ScooterOrder>> bookScooter({
|
||||
required int scooterId,
|
||||
required int planId,
|
||||
@@ -31,13 +30,13 @@ abstract class ScooterRepository {
|
||||
Future<Result<ScooterOrder>> pauseRide(int orderId);
|
||||
Future<Result<ScooterOrder>> resumeRide(int orderId);
|
||||
Future<Result<ScooterOrder>> finishRide(int orderId, List<int> files);
|
||||
Future<Result<void>> payRide(int orderId);
|
||||
Future<Result<ScooterOrder>> payRide(int orderId);
|
||||
Future<Result<List<ScooterOrder>>> getClientOrders();
|
||||
Future<Result<List<int>>> uploadScooterPhotos(List<File> images);
|
||||
Future<Result<ActiveScooterOrder>> updateScooterOrderData({
|
||||
required int orderId,
|
||||
});
|
||||
Future<Result<void>> payScooterOrderWithPhotos({
|
||||
Future<Result<ScooterOrder>> payScooterOrderWithPhotos({
|
||||
required int orderId,
|
||||
required int? cardId,
|
||||
required bool isBalance,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:be_happy/core/result.dart';
|
||||
import 'package:be_happy/domain/entities/scooter_order.dart';
|
||||
|
||||
import '../entities/client_subscription.dart';
|
||||
import '../repositories/scooter_repository.dart';
|
||||
|
||||
|
||||
@@ -15,7 +14,7 @@ class GetClientSubscriptionsUsecase {
|
||||
|
||||
GetClientSubscriptionsUsecase(this.repository);
|
||||
|
||||
Future<Result<List<ClientSubscription>>> call() {
|
||||
Future<Result<List<Subscription>>> call() {
|
||||
return repository.getClientSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import '../entities/client_notification.dart';
|
||||
import '../repositories/notification_repository.dart';
|
||||
|
||||
class GetNotificationsUsecase {
|
||||
final NotificationRepository repository;
|
||||
|
||||
GetNotificationsUsecase(this.repository);
|
||||
|
||||
Future<List<ClientNotification>> call() {
|
||||
return repository.getNotifications();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ class PayRideUsecase {
|
||||
|
||||
PayRideUsecase(this.repository);
|
||||
|
||||
Future<Result<void>> call(int orderId, int? cardId,
|
||||
Future<Result<ScooterOrder>> call(int orderId, int? cardId,
|
||||
bool isBalance) {
|
||||
return repository.payScooterOrderWithPhotos(orderId: orderId, cardId: cardId, isBalance: isBalance);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ class PayScooterOrderWithPhotosUsecase {
|
||||
|
||||
PayScooterOrderWithPhotosUsecase(this.repository);
|
||||
|
||||
Future<Result<void>> call({
|
||||
Future<Result<ScooterOrder>> call({
|
||||
required int orderId,
|
||||
required int cardId,
|
||||
required bool isBalance,
|
||||
|
||||
@@ -23,21 +23,9 @@ import 'di/service_locator.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: [SystemUiOverlay.top],
|
||||
);
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
|
||||
await setupDependencies();
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,11 @@ import 'package:flutter/material.dart';
|
||||
class CancelBookingDialog extends StatelessWidget {
|
||||
const CancelBookingDialog({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool result = false;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
@@ -39,7 +42,6 @@ class CancelBookingDialog extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔹 Кнопка "Отменить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
@@ -50,7 +52,10 @@ class CancelBookingDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true), // ✅ true = отменить
|
||||
onPressed: () => {
|
||||
result = false,
|
||||
Navigator.pop(context, result)
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
@@ -59,9 +64,9 @@ class CancelBookingDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Отменить",
|
||||
"Оставить",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||
color: Color(0xFF1D273A),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -69,13 +74,14 @@ class CancelBookingDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Кнопка "Оставить" — ВТОРАЯ, тёмная, возвращает false
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, false), // ❌ false = оставить
|
||||
onPressed: () {
|
||||
result = true;
|
||||
Navigator.pop(context, result);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D1024),
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -83,7 +89,7 @@ class CancelBookingDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Оставить",
|
||||
"Отменить",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FinishRideConfirmationDialog extends StatelessWidget {
|
||||
const FinishRideConfirmationDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2E3253).withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Завершить поездку?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Вы действительно хотите завершить поездку?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔹 Кнопка "Завершить" — ПЕРВАЯ, зелёная (градиент), возвращает true
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
|
||||
),
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true), // ✅ true = завершить
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Завершить",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Кнопка "Отмена" — ВТОРАЯ, тёмная, возвращает false
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, false), // ❌ false = отмена
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0D1024),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Отмена",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
141
lib/presentation/components/map_settings_sheet.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/presentation/event/map_event.dart';
|
||||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
||||
import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../viewmodel/map_bloc.dart';
|
||||
|
||||
class MapSettingsSheet extends StatelessWidget {
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const MapSettingsSheet({super.key, this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
|
||||
builder: (context, state) {
|
||||
final List<_SettingItemData> items = [
|
||||
_SettingItemData(
|
||||
label: 'Геоточки',
|
||||
icon: Icons.location_on_outlined,
|
||||
color: const Color(0xFF66E3C4),
|
||||
isActive: state.isAllGeomarksActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeomarksToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Геозоны',
|
||||
icon: Icons.gps_fixed_outlined,
|
||||
color: const Color(0xFF86EFAC),
|
||||
isActive: state.isAllGeozonesActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeozonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка',
|
||||
icon: Icons.home_outlined,
|
||||
color: const Color(0xFFA78BFA),
|
||||
isActive: state.isParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка запрещена',
|
||||
icon: Icons.block_outlined,
|
||||
color: const Color(0xFFF59E0B),
|
||||
isActive: state.isRestrictedParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Запрещено кататься',
|
||||
icon: Icons.warning_amber_outlined,
|
||||
color: const Color(0xFFEF4444),
|
||||
isActive: state.isRestrictedDrivingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedDrivingZonesToggled(val)),
|
||||
),
|
||||
];
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
height: 365,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.88),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Параметры карты',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<MapSettingsModalBloc>().add(ApllyButtonClick());
|
||||
context.read<MapBloc>().add(UpdateMap());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Готово',
|
||||
style: TextStyle(color: Color(0xFF66E3C4), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
leading: Icon(item.icon, color: item.color),
|
||||
title: Text(item.label, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Switch.adaptive(
|
||||
value: item.isActive,
|
||||
onChanged: item.onChanged,
|
||||
activeTrackColor: const Color(0xFF66E3C4),
|
||||
inactiveThumbColor: Colors.white,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательный класс для описания строк
|
||||
class _SettingItemData {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isActive;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
_SettingItemData({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.isActive,
|
||||
required this.onChanged,
|
||||
});
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:be_happy/domain/entities/scooter.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../di/service_locator.dart';
|
||||
import '../../event/active_ride_event.dart';
|
||||
import '../../event/map_event.dart';
|
||||
import '../../state/active_ride_state.dart';
|
||||
import '../../viewmodel/active_ride_bloc.dart';
|
||||
import '../../viewmodel/map_bloc.dart';
|
||||
import '../dialog/finish_ride_confirmation_dialog.dart';
|
||||
import '../notification_toast.dart';
|
||||
|
||||
class ActiveRideSheet extends StatefulWidget {
|
||||
@@ -59,7 +55,7 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: BlocConsumer<ActiveRideBloc, ActiveRideState>(
|
||||
listenWhen: (previous, current) => previous.inZone != current.inZone || previous.status != current.status,
|
||||
listenWhen: (previous, current) => previous.inZone != current.inZone,
|
||||
listener: (context, state) {
|
||||
if (!state.inZone) {
|
||||
BotToast.showCustomNotification(
|
||||
@@ -74,15 +70,6 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == ActiveRideStatus.success && state.order != null) {
|
||||
final scooter = state.order!.scooter;
|
||||
context.read<MapBloc>().add(FocusOnScooter(Scooter(id: scooter.id,
|
||||
title: scooter.title, status: scooter.status,
|
||||
latitude: state.longitude, longitude: state.latitude,
|
||||
batteryLevel: scooter.batteryLevel, isOnline: scooter.isOnline,
|
||||
maxSpeed: scooter.maxSpeed, number: scooter.number)));
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// Логика отображения загрузки и ошибок остается прежней
|
||||
@@ -215,8 +202,8 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: 'DigitalNumbers',
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: 'Digital Numbers',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -303,23 +290,13 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
|
||||
child: InkWell(
|
||||
onTap: state.status == ActiveRideStatus.loading
|
||||
? null
|
||||
: () async {
|
||||
// 🔹 Показываем диалог подтверждения
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => const FinishRideConfirmationDialog(),
|
||||
);
|
||||
|
||||
// 🔹 Если пользователь подтвердил — завершаем и переходим
|
||||
if (result == true) {
|
||||
_bloc.add(FinishRide(widget.orderId));
|
||||
Navigator.pop(context); // закрываем ActiveRideSheet
|
||||
context.go("/home/order-photos/${widget.orderId}");
|
||||
}
|
||||
// 🔹 Если отменил — ничего не делаем, диалог уже закрылся
|
||||
},
|
||||
: () {
|
||||
_bloc.add(FinishRide(widget.orderId));
|
||||
Navigator.pop(context);
|
||||
context.go("/home/order-photos/${widget.orderId}");
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column( // ✅ Вернули Column с иконкой и текстом
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
|
||||
@@ -219,11 +219,11 @@ class _RideCardState extends State<_RideCard> {
|
||||
displayTime = _elapsedTime;
|
||||
}
|
||||
final timeString = _formatDuration(displayTime);
|
||||
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
|
||||
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
|
||||
final statusText = _getStatusText(widget.order.status);
|
||||
final statusColor = _getStatusColor(widget.order.status);
|
||||
|
||||
final scooterNumber =
|
||||
widget.order.scooter.number ?? widget.order.scooterId.toString();
|
||||
widget.order.scooter?.number ?? widget.order.scooterId.toString();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -261,7 +261,7 @@ class _RideCardState extends State<_RideCard> {
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -269,51 +269,22 @@ class _RideCardState extends State<_RideCard> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset("assets/icons/qr_icon_order.png"),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
scooterNumber,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
scooterNumber,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getLocationText(),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
|
||||
if (isReserved)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Тариф",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8bffaa),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset("assets/icons/timer.png", width: 14,),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.order.plan?.title ?? "Название тарифа",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8bffaa),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -330,10 +301,10 @@ class _RideCardState extends State<_RideCard> {
|
||||
timeString,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 20,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: 'DigitalNumbers',
|
||||
fontFamily: 'Digital Numbers',
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -389,7 +360,7 @@ class _RideCardState extends State<_RideCard> {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'reserved':
|
||||
case 'holding':
|
||||
return 'Забронирован';
|
||||
return 'Забронировано';
|
||||
case 'active':
|
||||
case 'in_progress':
|
||||
return 'Активно';
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../viewmodel/map_bloc.dart';
|
||||
|
||||
class MapSettingsSheet extends StatelessWidget {
|
||||
@@ -17,6 +19,13 @@ class MapSettingsSheet extends StatelessWidget {
|
||||
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
|
||||
builder: (context, state) {
|
||||
final List<_SettingItemData> items = [
|
||||
_SettingItemData(
|
||||
label: 'Геоточки',
|
||||
icon: Icons.location_on_outlined,
|
||||
color: const Color(0xFF66E3C4),
|
||||
isActive: state.isAllGeomarksActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeomarksToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Геозоны',
|
||||
icon: Icons.gps_fixed_outlined,
|
||||
@@ -32,9 +41,9 @@ class MapSettingsSheet extends StatelessWidget {
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
|
||||
),
|
||||
_SettingItemData(
|
||||
label: 'Парковка запрещена',
|
||||
label: 'Разрешено кататься',
|
||||
icon: Icons.block_outlined,
|
||||
color: const Color(0xFFF59E0B),
|
||||
color: const Color(0xFF5ECD4C),
|
||||
isActive: state.isRestrictedParkingZoneActive,
|
||||
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
|
||||
),
|
||||
|
||||
@@ -12,13 +12,11 @@ import '../../state/payment_method_sheet_state.dart';
|
||||
import '../../viewmodel/payment_method_sheet_bloc.dart';
|
||||
|
||||
class PaymentMethodSheet extends StatefulWidget {
|
||||
final PaymentCard? initialSelectedCard;
|
||||
final bool showBalance;
|
||||
final PaymentCard? initialSelectedCard; // Добавляем это поле
|
||||
|
||||
const PaymentMethodSheet({
|
||||
super.key,
|
||||
this.initialSelectedCard,
|
||||
this.showBalance = true,
|
||||
this.initialSelectedCard, // Инициализируем в конструкторе
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -94,7 +92,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
|
||||
} else {
|
||||
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
|
||||
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0);
|
||||
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,20 +169,19 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.showBalance) ...[
|
||||
PaymentOption(
|
||||
title: 'Баланс',
|
||||
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
|
||||
isSelected: _selectedPaymentMethod == -1,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = -1;
|
||||
});
|
||||
Navigator.pop(context, 'balance');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
PaymentOption(
|
||||
title: 'Баланс',
|
||||
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
|
||||
isSelected: _selectedPaymentMethod == -1,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = -1;
|
||||
});
|
||||
Navigator.pop(context, 'balance');
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
...state.cards.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
|
||||
@@ -60,286 +60,264 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.5),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(30),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF000032).withOpacity(0.5),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// HEADER
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.arrow_back_ios_sharp,
|
||||
color: const Color(0x99FFFFFF),
|
||||
size: 20,
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_back_ios_sharp,
|
||||
color: const Color(0x66FFFFFF),
|
||||
size: 20,
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_back_ios_sharp,
|
||||
color: const Color(0x22FFFFFF),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Бесплатное бронирование',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// HEADER
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
// Таймер
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_formatDuration(_reservationTime),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: 'Digital Numbers',
|
||||
),
|
||||
),
|
||||
),
|
||||
// Иконка и информация (ВЫСОКИЙ БЛОК)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.arrow_back_ios_sharp,
|
||||
color: const Color(0x99FFFFFF),
|
||||
size: 20,
|
||||
// Иконка самоката (ВЫШЕ)
|
||||
SizedBox(
|
||||
width: 44,
|
||||
height: 56,
|
||||
child: Image.asset(
|
||||
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_back_ios_sharp,
|
||||
color: const Color(0x66FFFFFF),
|
||||
size: 20,
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_back_ios_sharp,
|
||||
color: const Color(0x22FFFFFF),
|
||||
size: 20,
|
||||
const SizedBox(width: 12),
|
||||
// Инфо
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFB800),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Забронирован',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'№${widget.scooterNumber}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Бесплатное бронирование',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
// Таймер
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_formatDuration(_reservationTime),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: 'Digital Numbers',
|
||||
),
|
||||
),
|
||||
),
|
||||
// Иконка и информация (ВЫСОКИЙ БЛОК)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Иконка самоката (ВЫШЕ)
|
||||
SizedBox(
|
||||
width: 44,
|
||||
height: 56,
|
||||
child: Image.asset(
|
||||
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Инфо
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFB800),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Забронирован',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'№${widget.scooterNumber}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideStarted) {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ActiveRideSheet(
|
||||
scooterNumber: widget.scooterNumber,
|
||||
initialElapsedTime: Duration.zero,
|
||||
orderId: widget.orderId,
|
||||
),
|
||||
);
|
||||
} else if (state.status ==
|
||||
ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: GradientButton(
|
||||
text: 'Начать поездку',
|
||||
showArrows: true,
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
fontSize: 15,
|
||||
onTap: () {
|
||||
_bloc.add(StartRide(widget.orderId));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideStarted) {
|
||||
Navigator.pop(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ActiveRideSheet(
|
||||
scooterNumber: widget.scooterNumber,
|
||||
initialElapsedTime: Duration.zero,
|
||||
orderId: widget.orderId,
|
||||
),
|
||||
);
|
||||
} else if (state.status == ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: GradientButton(
|
||||
text: 'Начать поездку',
|
||||
showArrows: true,
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
fontSize: 15,
|
||||
onTap: () {
|
||||
_bloc.add(StartRide(widget.orderId));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideCancelled) {
|
||||
Navigator.pop(context);
|
||||
} else if (state.status ==
|
||||
ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Ошибка'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
// КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocListener<ReservedRideBloc, ReservedRideState>(
|
||||
listener: (context, state) {
|
||||
if (state.rideCancelled) {
|
||||
Navigator.pop(context);
|
||||
} else if (state.status == ReservedRideStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => const CancelBookingDialog(),
|
||||
);
|
||||
if (result != null && result) {
|
||||
_bloc.add(CancelRide(widget.orderId));
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const CancelBookingDialog(),
|
||||
);
|
||||
if (result != null && result) {
|
||||
_bloc.add(CancelRide(widget.orderId));
|
||||
child: BlocBuilder<ReservedRideBloc, ReservedRideState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ReservedRideStatus.loading) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child:
|
||||
BlocBuilder<
|
||||
ReservedRideBloc,
|
||||
ReservedRideState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
if (state.status ==
|
||||
ReservedRideStatus.loading) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Отменить бронирование',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Отменить бронирование',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import 'package:be_happy/presentation/components/gradient_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../domain/entities/subscription.dart';
|
||||
import '../event/subscription_list_event.dart';
|
||||
|
||||
class SubscriptionCard extends StatelessWidget {
|
||||
final Subscription subscription;
|
||||
final bool isActive;
|
||||
final DateTime? expiredAt;
|
||||
final VoidCallback? onRefresh;
|
||||
|
||||
const SubscriptionCard({super.key, required this.subscription, required this.isActive, this.expiredAt, this.onRefresh});
|
||||
const SubscriptionCard({super.key, required this.subscription, required this.isActive});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -20,9 +15,9 @@ class SubscriptionCard extends StatelessWidget {
|
||||
? subscription.options.reduce((a, b) => a.price < b.price ? a : b)
|
||||
: null;
|
||||
|
||||
/*final maxDaysOption = subscription.options.isNotEmpty
|
||||
final maxDaysOption = subscription.options.isNotEmpty
|
||||
? subscription.options.reduce((a, b) => a.days > b.days ? a : b)
|
||||
: null;*/
|
||||
: null;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
@@ -48,7 +43,7 @@ class SubscriptionCard extends StatelessWidget {
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(12), // Опционально: скругление углов
|
||||
),
|
||||
child: Text(
|
||||
"АКТИВНА",
|
||||
@@ -69,19 +64,12 @@ class SubscriptionCard extends StatelessWidget {
|
||||
subscription.shortDescription,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14),
|
||||
),
|
||||
if (isActive && expiredAt != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
if (maxDaysOption != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final day = expiredAt!.day.toString().padLeft(2, '0');
|
||||
final month = expiredAt!.month.toString().padLeft(2, '0');
|
||||
final year = expiredAt!.year;
|
||||
|
||||
return Text(
|
||||
"Период действия: до $day.$month.$year",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
);
|
||||
},
|
||||
Text(
|
||||
"Период действия: до ${maxDaysOption.days} дней",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
@@ -95,17 +83,15 @@ class SubscriptionCard extends StatelessWidget {
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
GradientButton(
|
||||
onTap: () async {
|
||||
final isSubscribed = await context.push<bool>("/home/subscriptions/${subscription.id}");
|
||||
if (isSubscribed == true && onRefresh != null) {
|
||||
onRefresh!();
|
||||
}
|
||||
},
|
||||
text: "Подробнее",
|
||||
enabled: true,
|
||||
width: 120,
|
||||
height: 40,
|
||||
ElevatedButton(
|
||||
onPressed: () => context.push("/home/subscriptions/${subscription.id}"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF80FFD1),
|
||||
foregroundColor: Colors.black,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
child: const Text("Подробнее", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -30,14 +30,4 @@ class NotificationReceived extends ScooterEvent {
|
||||
NotificationReceived(this.notification);
|
||||
}
|
||||
|
||||
class FocusOnScooter extends ScooterEvent {
|
||||
final Scooter scooter;
|
||||
FocusOnScooter(this.scooter);
|
||||
}
|
||||
|
||||
class ClearMapPlacemarks extends ScooterEvent {}
|
||||
class ClearMapFocus extends ScooterEvent {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
sealed class NotificationsEvent {}
|
||||
|
||||
class NotificationsFetchRequested extends NotificationsEvent {}
|
||||
@@ -58,9 +58,9 @@ import '../../domain/usecase/remove_payment_card_usecase.dart';
|
||||
import '../../domain/usecase/save_map_settings_usecase.dart';
|
||||
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../../domain/usecase/verify_pin_usecase.dart';
|
||||
import '../components/map_settings_sheet.dart';
|
||||
import '../components/scooter_bottom_sheet.dart';
|
||||
import '../components/sheet/current_rides_sheet.dart';
|
||||
import '../components/sheet/map_settings_sheet.dart';
|
||||
import '../components/sheet/payment_method_sheet.dart';
|
||||
import '../components/sheet/reserved_ride_sheet.dart';
|
||||
import '../components/sheet/tariff_sheet.dart';
|
||||
@@ -75,7 +75,6 @@ import '../event/tariff_sheet_event.dart';
|
||||
import '../event/top_up_event.dart';
|
||||
import '../screens/add_card_screen.dart'; // ← новый импорт
|
||||
import '../screens/license_agreement_screen.dart';
|
||||
import '../screens/notifications_screen.dart';
|
||||
import '../screens/order_history_screen.dart';
|
||||
import '../screens/payment_methods_screen.dart';
|
||||
import '../screens/phone_login_screen.dart';
|
||||
@@ -372,7 +371,6 @@ class AppRouter {
|
||||
SubscriptionDetailsBloc(
|
||||
getIt<GetSubscriptionByIdUsecase>(),
|
||||
getIt<ActivateSubscriptionUsecase>(),
|
||||
getIt<GetClientSubscriptionsUsecase>(),
|
||||
)
|
||||
..add(
|
||||
LoadDetailsEvent(
|
||||
@@ -458,10 +456,6 @@ class AppRouter {
|
||||
builder: (context, state) => const OrderHistoryScreen(),
|
||||
routes: []
|
||||
),
|
||||
GoRoute(
|
||||
path: 'notifications',
|
||||
builder: (context, state) => const NotificationsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -19,14 +19,11 @@ class AddCardScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: BlocListener<AddCardBloc, AddCardState>(
|
||||
listenWhen: (previous, current) {
|
||||
print(
|
||||
'Смена статуса: ${previous.status} -> ${current.status} ${current.errorMessage}');
|
||||
return previous.status != current.status &&
|
||||
current.status == AddCardStatus.success;
|
||||
},
|
||||
listenWhen: (previous, current) =>
|
||||
previous.status != current.status &&
|
||||
current.status == AddCardStatus.success,
|
||||
listener: (context, state) {
|
||||
context.pop(true);
|
||||
context.pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
@@ -38,7 +35,6 @@ class AddCardScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const CustomAppBar(title: 'Добавление карты'),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -152,7 +148,10 @@ class AddCardScreen extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: state.isFormValid
|
||||
? () => {
|
||||
context.read<AddCardBloc>().add(AddCardSubmitted()),
|
||||
context.read<AddCardBloc>().add(
|
||||
AddCardSubmitted()),
|
||||
context.read<PaymentMethodsBloc>()..add(PaymentMethodsStarted()),
|
||||
context.pop()
|
||||
}
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
@@ -18,10 +17,12 @@ class DocumentsScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
// ✅ Используем общий AppBar
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: 'Документы'),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Список ссылок
|
||||
LinkRow(
|
||||
icon: 'assets/icons/doc.png',
|
||||
title: 'Договор аренды',
|
||||
@@ -32,14 +33,14 @@ class DocumentsScreen extends StatelessWidget {
|
||||
LinkRow(
|
||||
icon: 'assets/icons/doc.png',
|
||||
title: 'Политика конфиденциальности',
|
||||
onTap: () => context.push('/privacy-policy')
|
||||
onTap: () => openLink('https://...'),
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
const SizedBox(height: 12),
|
||||
LinkRow(
|
||||
icon: 'assets/icons/doc.png',
|
||||
title: 'Правила вождения',
|
||||
onTap: () => openLink('https://behappybel.by/#rule'),
|
||||
onTap: () => openLink('https://...'),
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -14,7 +14,7 @@ class LicenseAgreementScreen extends StatelessWidget {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// 🔹 APPBAR С КНОПКОЙ НАЗАД
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: ' '),
|
||||
|
||||
@@ -93,9 +93,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
listenWhen: (previous, current) {
|
||||
return current.lastNotification !=
|
||||
previous.lastNotification ||
|
||||
current.flags != previous.flags ||
|
||||
previous.selectedScooterForFocus?.id
|
||||
!= current.selectedScooterForFocus?.id;
|
||||
current.flags != previous.flags;
|
||||
},
|
||||
|
||||
listener: (context, state) {
|
||||
@@ -166,39 +164,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.selectedScooterForFocus != null) {
|
||||
final targetScooter = state.selectedScooterForFocus!;
|
||||
|
||||
print("RESERVED SCOOTER: $targetScooter");
|
||||
|
||||
_moveCameraToPoint(
|
||||
targetScooter.longitude,
|
||||
targetScooter.latitude,
|
||||
zoom: 17,
|
||||
);
|
||||
|
||||
context.read<MapBloc>().add(ClearMapFocus());
|
||||
}
|
||||
},
|
||||
buildWhen: (previous, current) {
|
||||
return previous.scooters != current.scooters ||
|
||||
previous.reservedScooters != current.reservedScooters ||
|
||||
previous.zones != current.zones ||
|
||||
previous.status != current.status;
|
||||
},
|
||||
|
||||
buildWhen: (previous, current) =>
|
||||
previous.scooters != current.scooters ||
|
||||
previous.zones != current.zones,
|
||||
builder: (context, state) {
|
||||
final freeScooters = _buildScooterPlacemarks(
|
||||
scooters: state.scooters,
|
||||
iconAsset: 'assets/icons/scooter_placemark_fill.png',
|
||||
isClickable: true,
|
||||
);
|
||||
|
||||
final reservedScooters = _buildScooterPlacemarks(
|
||||
scooters: state.reservedScooters ?? [],
|
||||
iconAsset: 'assets/icons/scooter_reserved_placemark_fill.png',
|
||||
isClickable: false,
|
||||
final scooters = _buildScooterPlacemarks(
|
||||
state.scooters,
|
||||
state.address ?? "Unknown address",
|
||||
);
|
||||
|
||||
final zonePolygons = _buildZonePolygons(state.zones);
|
||||
@@ -220,10 +193,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
mapObjects: [
|
||||
...zonePolygons,
|
||||
...reservedScooters,
|
||||
ClusterizedPlacemarkCollection(
|
||||
mapId: const MapObjectId('scooters_cluster'),
|
||||
placemarks: freeScooters,
|
||||
placemarks: scooters,
|
||||
radius: 30,
|
||||
minZoom: 15,
|
||||
consumeTapEvents: true,
|
||||
@@ -260,6 +232,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
),
|
||||
|
||||
// Индикатор загрузки (отдельный строитель для статуса)
|
||||
BlocBuilder<MapBloc, ScooterState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.status != current.status,
|
||||
@@ -405,42 +378,70 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
void _onMarkerTap(List<Scooter> scooters) async {
|
||||
context.read<MapBloc>().add(CheckUser());
|
||||
final flags = context.read<MapBloc>().state.flags;
|
||||
|
||||
if (!flags.hasCard) {
|
||||
_showExistingNotification(
|
||||
child: PaymentNotificationCard(
|
||||
onBindCard: () {
|
||||
BotToast.cleanAll();
|
||||
context.push("/home/payment-methods");
|
||||
},
|
||||
onClose: () => BotToast.cleanAll(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (flags.hasUnpaidOrder) {
|
||||
_showExistingNotification(
|
||||
child: UnpaidOrderNotificationCard(
|
||||
onClose: () => BotToast.cleanAll(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (flags.hasFine) {
|
||||
_showExistingNotification(
|
||||
child: FineNotificationCard(
|
||||
onClose: () => BotToast.cleanAll(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.push(
|
||||
"/home/scooter-sheet",
|
||||
extra: {'scooters': scooters, 'currentLocation': _currentPosition},
|
||||
);
|
||||
|
||||
/*final scoot = await showModalBottomSheet<Scooter>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
isDismissible: true,
|
||||
builder: (context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
ScooterDetailModalBloc(
|
||||
getIt<GetAddressByPointUsecase>(),
|
||||
getIt<GetScooterUsecase>(),
|
||||
getIt<GetPedestrianRoutesUsecase>(),
|
||||
)..add(
|
||||
ScooterDetailModalStarted(
|
||||
scooters,
|
||||
_currentPosition!.latitude,
|
||||
_currentPosition!.longitude,
|
||||
),
|
||||
),
|
||||
child: ScooterBottomSheet(),
|
||||
);
|
||||
},
|
||||
);*/
|
||||
/*bool? isBooking = false;
|
||||
if (scoot != null) {
|
||||
final result = await context.push('/home/scooter/${scoot.id}');
|
||||
|
||||
if (result == true) {
|
||||
// Даем небольшую задержку, чтобы навигация завершилась корректно
|
||||
await Future.delayed(Duration(milliseconds: 300), () async {
|
||||
isBooking = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
isDismissible: true,
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => TariffSheetBloc(
|
||||
getIt<GetAvailableTariffsUsecase>(),
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<BookScooterUsecase>(),
|
||||
),
|
||||
child: TariffSheet(scooter: scoot),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isBooking ?? false) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) =>
|
||||
CurrentRidesBloc(getIt<GetClientOrdersUsecase>())
|
||||
..add(LoadClientOrders(1)),
|
||||
child: CurrentRidesSheet(clientId: 1),
|
||||
),
|
||||
);
|
||||
}*/
|
||||
}
|
||||
|
||||
void _onMapSettingsTap() {
|
||||
@@ -581,26 +582,26 @@ class _MapScreenState extends State<MapScreen> {
|
||||
await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png');
|
||||
}
|
||||
|
||||
List<PlacemarkMapObject> _buildScooterPlacemarks({
|
||||
required List<Scooter> scooters,
|
||||
required String iconAsset,
|
||||
required bool isClickable,
|
||||
}) {
|
||||
List<PlacemarkMapObject> _buildScooterPlacemarks(
|
||||
List<Scooter> scooters,
|
||||
String address,
|
||||
) {
|
||||
return scooters.map((scooter) {
|
||||
return PlacemarkMapObject(
|
||||
mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты
|
||||
mapId: MapObjectId('${scooter.id}'),
|
||||
point: Point(latitude: scooter.longitude, longitude: scooter.latitude),
|
||||
icon: PlacemarkIcon.single(
|
||||
PlacemarkIconStyle(
|
||||
image: BitmapDescriptor.fromAssetImage(iconAsset),
|
||||
image: BitmapDescriptor.fromAssetImage(
|
||||
'assets/icons/scooter_placemark_fill.png',
|
||||
),
|
||||
scale: 0.2,
|
||||
),
|
||||
),
|
||||
opacity: 1.0,
|
||||
consumeTapEvents: isClickable,
|
||||
onTap: isClickable
|
||||
? (object, point) async => _onMarkerTap([scooter])
|
||||
: null,
|
||||
onTap: (object, point) async => {
|
||||
_onMarkerTap([scooter]),
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@@ -695,13 +696,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
_RoundIconButton(
|
||||
icon: Icons.notifications_sharp,
|
||||
onPressed: () => context.push("/home/notifications"),
|
||||
onPressed: _onNotificationTap,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_RoundButton(
|
||||
imagePath: 'assets/icons/scooter_placemark.png',
|
||||
_RoundIconButton(
|
||||
icon: Icons.directions_run,
|
||||
onPressed: () => context.push("/home/current-rides-sheet"),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -716,42 +717,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
final flags = context.read<MapBloc>().state.flags;
|
||||
|
||||
// 🔹 Проверка флагов — показываем те же карточки, что и в listener
|
||||
if (!flags.hasCard) {
|
||||
_showExistingNotification(
|
||||
child: PaymentNotificationCard(
|
||||
onBindCard: () {
|
||||
BotToast.cleanAll();
|
||||
context.push("/home/payment-methods");
|
||||
},
|
||||
onClose: () => BotToast.cleanAll(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (flags.hasUnpaidOrder) {
|
||||
_showExistingNotification(
|
||||
child: UnpaidOrderNotificationCard(
|
||||
onClose: () => BotToast.cleanAll(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (flags.hasFine) {
|
||||
_showExistingNotification(
|
||||
child: FineNotificationCard(
|
||||
onClose: () => BotToast.cleanAll(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Все проверки пройдены — переход на сканирование
|
||||
context.push("/home/qr-info");
|
||||
},
|
||||
onTap: () => context.push("/home/qr-info"),
|
||||
child: Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
@@ -844,40 +810,6 @@ class _RoundIconButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RoundButton extends StatelessWidget {
|
||||
final String imagePath;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _RoundButton({
|
||||
required this.imagePath,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkBlue,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
imagePath,
|
||||
width: 20,
|
||||
height: 20,
|
||||
fit: BoxFit.contain,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CircleIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
@@ -900,18 +832,3 @@ class _CircleIconButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showExistingNotification({required Widget child}) {
|
||||
BotToast.showCustomNotification(
|
||||
duration: null, // не исчезает, пока пользователь не закроет
|
||||
toastBuilder: (_) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 120), // тот же отступ, что в listener
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// 🔹 Заголовок в AppBar
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: widget.title),
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:developer' as dev;
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
import '../event/news_event.dart';
|
||||
import '../state/news_state.dart';
|
||||
import '../viewmodel/news_bloc.dart';
|
||||
@@ -15,8 +14,11 @@ class NewsScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
dev.log('🔍 NewsScreen: Создание экрана новостей');
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
dev.log('🔍 NewsScreen: Создание NewsBloc');
|
||||
return getIt<NewsBloc>()..add(const NewsFetchRequested());
|
||||
},
|
||||
child: const NewsView(),
|
||||
@@ -29,6 +31,7 @@ class NewsView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
dev.log('🔍 NewsView: Построение UI');
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
@@ -45,6 +48,7 @@ class NewsView extends StatelessWidget {
|
||||
Expanded(
|
||||
child: BlocBuilder<NewsBloc, NewsState>(
|
||||
builder: (context, state) {
|
||||
dev.log('🔍 NewsView: Состояние ${state.status}, новостей: ${state.news.length}');
|
||||
|
||||
if (state.status == NewsStatus.initial || state.status == NewsStatus.loading) {
|
||||
return const Center(
|
||||
@@ -80,6 +84,7 @@ class NewsView extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
dev.log('🔍 NewsView: Повторная загрузка');
|
||||
context.read<NewsBloc>().add(const NewsFetchRequested());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -168,7 +173,7 @@ class _NewsCard extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0A0F2E).withOpacity(0.7),
|
||||
color: const Color(0xFF141530),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
@@ -191,18 +196,6 @@ class _NewsCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/news_def.png'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
news.previewText,
|
||||
style: const TextStyle(
|
||||
@@ -212,28 +205,51 @@ class _NewsCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 150),
|
||||
child: GradientButton(
|
||||
text: 'Подробнее',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NewsDetailScreen(
|
||||
newsId: news.id,
|
||||
title: news.title,
|
||||
),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NewsDetailScreen(
|
||||
newsId: news.id,
|
||||
title: news.title,
|
||||
),
|
||||
);
|
||||
},
|
||||
showArrows: true,
|
||||
fontSize: 14,
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
side: BorderSide(color: AppColors.smsDigit.withOpacity(0.3)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Подробнее',
|
||||
style: TextStyle(color: AppColors.smsDigit),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_sharp,
|
||||
size: 12,
|
||||
color: AppColors.smsDigit,
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_sharp,
|
||||
size: 12,
|
||||
color: AppColors.smsDigit.withOpacity(0.6),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_sharp,
|
||||
size: 12,
|
||||
color: AppColors.smsDigit.withOpacity(0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,350 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:developer' as dev;
|
||||
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../di/service_locator.dart';
|
||||
import '../../domain/entities/client_notification.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
import '../event/notifications_event.dart';
|
||||
import '../state/notifications_state.dart';
|
||||
import '../viewmodel/notifications_bloc.dart';
|
||||
|
||||
enum NotificationFilter {
|
||||
all,
|
||||
auth,
|
||||
payment,
|
||||
order,
|
||||
}
|
||||
|
||||
class NotificationsScreen extends StatelessWidget {
|
||||
const NotificationsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _NotificationsScreenContent();
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationsScreenContent extends StatefulWidget {
|
||||
const _NotificationsScreenContent();
|
||||
|
||||
@override
|
||||
State<_NotificationsScreenContent> createState() => _NotificationsScreenContentState();
|
||||
}
|
||||
|
||||
class _NotificationsScreenContentState extends State<_NotificationsScreenContent> {
|
||||
NotificationFilter _filter = NotificationFilter.all;
|
||||
|
||||
void _setFilter(NotificationFilter filter) {
|
||||
setState(() {
|
||||
_filter = filter;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<NotificationsBloc>()..add(NotificationsFetchRequested()),
|
||||
child: NotificationsView(
|
||||
filter: _filter,
|
||||
onFilterChanged: _setFilter,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationsView extends StatelessWidget {
|
||||
final NotificationFilter filter;
|
||||
final ValueChanged<NotificationFilter> onFilterChanged;
|
||||
|
||||
const NotificationsView({
|
||||
super.key,
|
||||
this.filter = NotificationFilter.all,
|
||||
required this.onFilterChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Уведомления'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildFilterBar(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Expanded(
|
||||
child: BlocBuilder<NotificationsBloc, NotificationsState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == NotificationsStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator(color: Colors.white));
|
||||
}
|
||||
|
||||
if (state.status == NotificationsStatus.failure) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Ошибка загрузки уведомлений',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Text(
|
||||
state.errorMessage ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
||||
},
|
||||
child: const Text('Повторить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filtered = state.notifications.where((n) {
|
||||
switch (filter) {
|
||||
case NotificationFilter.all:
|
||||
return true;
|
||||
case NotificationFilter.auth:
|
||||
return n.category == NotificationCategory.auth;
|
||||
case NotificationFilter.payment:
|
||||
return n.category == NotificationCategory.payment;
|
||||
case NotificationFilter.order:
|
||||
return n.category == NotificationCategory.scooter;
|
||||
// || n.category == NotificationCategory.adminInfo
|
||||
// || n.category == NotificationCategory.companyInfo;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
if (filtered.isEmpty) {
|
||||
return const _EmptyState();
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _NotificationCard(notification: filtered[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterBar() {
|
||||
final items = [
|
||||
{'label': 'Все', 'value': NotificationFilter.all},
|
||||
{'label': 'Авторизация', 'value': NotificationFilter.auth},
|
||||
{'label': 'Оплата', 'value': NotificationFilter.payment},
|
||||
{'label': 'Поездка', 'value': NotificationFilter.order},
|
||||
];
|
||||
|
||||
return Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: items.map((item) {
|
||||
final isActive = item['value'] == filter;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: GestureDetector(
|
||||
onTap: () => onFilterChanged(item['value'] as NotificationFilter),
|
||||
child: Container(
|
||||
height: 32,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: isActive ? AppColors.activeButtonGradient : null,
|
||||
color: isActive ? null : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isActive
|
||||
? Colors.transparent
|
||||
: Colors.white.withOpacity(0.4),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
item['label'] as String,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isActive
|
||||
? AppColors.activeButtonText
|
||||
: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/notification_empty.png',
|
||||
width: 280,
|
||||
height: 280,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.notifications_none_outlined,
|
||||
size: 120,
|
||||
color: Colors.white38,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Text(
|
||||
'У вас пока нет уведомлений.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationCard extends StatelessWidget {
|
||||
final ClientNotification notification;
|
||||
|
||||
const _NotificationCard({required this.notification});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final date = _formatDate(notification.createdAt);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF141530).withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
notification.content,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return 'Сегодня, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Вчера, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}';
|
||||
}
|
||||
}
|
||||
|
||||
String _getTypeLabel(NotificationType type) {
|
||||
switch (type) {
|
||||
case NotificationType.info:
|
||||
return 'Информация';
|
||||
case NotificationType.attention:
|
||||
return 'Внимание';
|
||||
case NotificationType.warning:
|
||||
return 'Предупреждение';
|
||||
}
|
||||
}
|
||||
|
||||
String _getCategoryLabel(NotificationCategory category) {
|
||||
switch (category) {
|
||||
case NotificationCategory.auth:
|
||||
return 'Авторизация';
|
||||
case NotificationCategory.zone:
|
||||
return 'Зоны';
|
||||
case NotificationCategory.payment:
|
||||
return 'Оплата';
|
||||
case NotificationCategory.companyInfo:
|
||||
return 'Акции';
|
||||
case NotificationCategory.adminInfo:
|
||||
return 'Админ';
|
||||
case NotificationCategory.scooter:
|
||||
return 'Самокат';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class OrderHistoryDetailScreen extends StatelessWidget {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// 🔹 HEADER
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Поездка $date'),
|
||||
|
||||
@@ -32,7 +32,6 @@ class OrderHistoryView extends StatelessWidget {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'История поездок'),
|
||||
|
||||
@@ -9,11 +9,9 @@ import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
import '../components/payment_option.dart';
|
||||
import '../components/sheet/payment_method_sheet.dart';
|
||||
import '../event/map_event.dart';
|
||||
import '../event/payment_confirm_event.dart';
|
||||
import '../event/payment_method_sheet_event.dart';
|
||||
import '../state/payment_confirm_state.dart';
|
||||
import '../viewmodel/map_bloc.dart';
|
||||
import '../viewmodel/payment_confirm_bloc.dart';
|
||||
import '../viewmodel/payment_method_sheet_bloc.dart';
|
||||
|
||||
@@ -64,7 +62,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Завершение поездки'),
|
||||
@@ -80,7 +77,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
||||
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) {
|
||||
context.read<MapBloc>().add(ClearMapPlacemarks());
|
||||
context.go('/home');
|
||||
} else if (state.status == PaymentConfirmStatus.failure) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
@@ -17,63 +17,42 @@ class PaymentMethodsScreen extends StatelessWidget {
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentMethodsStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isNetworkProcessing = state.status == PaymentMethodsStatus.loading ||
|
||||
(state.isDeleting ?? false) ||
|
||||
(state.isSettingMain ?? false);
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Способы оплаты'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == PaymentMethodsStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.status == PaymentMethodsStatus.loading && state.cards.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF00D4AA)));
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Способы оплаты'),
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildBalanceCard(context, state.balance),
|
||||
const SizedBox(height: 20),
|
||||
_buildCardsList(context, state),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: state.cards.isEmpty && state.status == PaymentMethodsStatus.loading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF00D4AA)),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildBalanceCard(context, state.balance),
|
||||
const SizedBox(height: 20),
|
||||
_buildCardsList(context, state),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (isNetworkProcessing && state.cards.isNotEmpty)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF00D4AA),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -183,13 +162,7 @@ class PaymentMethodsScreen extends StatelessWidget {
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
|
||||
|
||||
if (isCardAdded == true && context.mounted) {
|
||||
context.read<PaymentMethodsBloc>().add(PaymentMethodsStarted());
|
||||
}
|
||||
},
|
||||
onTap: () => context.go('/home/payment-methods/add-card'),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
|
||||
@@ -14,7 +14,6 @@ class PrivacyPolicyScreen extends StatelessWidget {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: ''),
|
||||
|
||||
@@ -50,9 +50,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: AppColors.phoneScreenBg,
|
||||
),
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: BlocBuilder<ProfileBloc, ProfileState>(
|
||||
builder: (context, state) {
|
||||
@@ -72,82 +70,56 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
|
||||
final profile = state.profile!;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: 'Профиль'),
|
||||
const SizedBox(height: 32),
|
||||
Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: 'Профиль'),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: AppColors.checkboxFill,
|
||||
backgroundImage:
|
||||
(profile.avatarUrl != null &&
|
||||
profile.avatarUrl!.isNotEmpty)
|
||||
? NetworkImage(
|
||||
"${profile.avatarUrl!}?v=${DateTime.now().minute}",
|
||||
)
|
||||
: null,
|
||||
child:
|
||||
(profile.avatarUrl == null ||
|
||||
profile.avatarUrl!.isEmpty)
|
||||
? Text(
|
||||
profile.name.isNotEmpty
|
||||
? profile.name[0].toUpperCase()
|
||||
: '',
|
||||
style: const TextStyle(
|
||||
fontSize: 50,
|
||||
color: AppColors.darkBlue,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Container(
|
||||
child: Image.asset(
|
||||
'assets/icons/edit.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: AppColors.checkboxFill,
|
||||
backgroundImage: (profile.avatarUrl != null && profile.avatarUrl!.isNotEmpty)
|
||||
? NetworkImage("${profile.avatarUrl!}?v=${DateTime.now().minute}")
|
||||
: null,
|
||||
child: (profile.avatarUrl == null || profile.avatarUrl!.isEmpty)
|
||||
? Text(
|
||||
profile.name.isNotEmpty ? profile.name[0].toUpperCase() : '',
|
||||
style: const TextStyle(fontSize: 50, color: AppColors.darkBlue),
|
||||
)
|
||||
: null,
|
||||
), GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 0, right: 0),
|
||||
child: Image.asset(
|
||||
'assets/icons/edit.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_ProfileInfoBlock(
|
||||
profile: profile,
|
||||
onEditTap: () => context.go("/home/profile/edit"),
|
||||
),
|
||||
|
||||
// const SizedBox(height: 24),
|
||||
// _SettingsBlock(
|
||||
// notificationsEnabled: notificationsEnabled,
|
||||
// onNotificationsChanged: (v) =>
|
||||
// setState(() => notificationsEnabled = v),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
const SizedBox(height: 32),
|
||||
_ProfileInfoBlock(
|
||||
profile: profile,
|
||||
onEditTap: () => context.go("/home/profile/edit"),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_SettingsBlock(
|
||||
notificationsEnabled: notificationsEnabled,
|
||||
onNotificationsChanged: (v) =>
|
||||
setState(() => notificationsEnabled = v),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -45,7 +45,6 @@ class _PromoCodeScreenState extends State<PromoCodeScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: 'Промокоды'),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ class QRScanInfoScreen extends StatelessWidget {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: "Сканирование QR-кода"),
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:be_happy/di/service_locator.dart';
|
||||
import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart';
|
||||
import '../components/custom_app_bar.dart';
|
||||
import '../components/gradient_button.dart';
|
||||
|
||||
class QrScanScreen extends StatefulWidget {
|
||||
@@ -150,13 +149,11 @@ class _QrScanScreenState extends State<QrScanScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// ✅ ИЗМЕНЕНО: прижимаем аппбар к левому краю
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, // ✅ Выравнивание по левому краю
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(title: "Сканирование QR-кода"),
|
||||
const SizedBox(height: 60),
|
||||
const Text(
|
||||
'Наведите рамку на QR-код — номер будет распознан автоматически',
|
||||
@@ -173,6 +170,7 @@ class _QrScanScreenState extends State<QrScanScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SafeArea(
|
||||
child: Align(
|
||||
|
||||
@@ -58,7 +58,6 @@ class _ScooterCodeInputScreenState extends State<ScooterCodeInputScreen> {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: "Ввод QR-кода"),
|
||||
|
||||
@@ -70,7 +70,6 @@ class ScooterDetailScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppBar(
|
||||
title: scooter?.title != null ? 'Самокат ${scooter!.title}' : 'Самокат',
|
||||
),
|
||||
@@ -102,7 +101,6 @@ class ScooterDetailScreen extends StatelessWidget {
|
||||
context.pushReplacement('/home/tarif-sheet', extra: scooter);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -123,11 +123,6 @@ class _SendPhotoViewState extends State<SendPhotoView> {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: "Отправить фото"),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 100),
|
||||
child: Text(
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:be_happy/presentation/event/spalsh_event.dart';
|
||||
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// Подключи сюда свои реальные экраны:
|
||||
import 'phone_screen.dart';
|
||||
import 'pin_login_screen.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
@@ -14,61 +19,35 @@ class SplashScreen extends StatefulWidget {
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _revealAnimation;
|
||||
|
||||
// Фаза 1: Заполнение цветом слева направо (0.0 -> 0.5)
|
||||
late final Animation<double> _fillProgress;
|
||||
|
||||
// Фаза 2: Укатывание вправо (0.6 -> 1.0)
|
||||
late final Animation<double> _rollTranslation;
|
||||
late final Animation<double> _rollRotation;
|
||||
|
||||
// Уменьшенный размер логотипа по вашему запросу
|
||||
static const double logoSize = 130;
|
||||
static const double logoSize = 300;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// контроллер анимации
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 3000),
|
||||
duration: const Duration(milliseconds: 2500),
|
||||
);
|
||||
|
||||
_fillProgress = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeInOut),
|
||||
),
|
||||
// анимация движения "затемняющего" прямоугольника
|
||||
_revealAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_rollTranslation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.6, 1.0, curve: Curves.easeInCubic),
|
||||
),
|
||||
);
|
||||
|
||||
_rollRotation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.6, 1.0, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
// Добавляем задержку перед стартом, чтобы пользователь успел увидеть экран
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!mounted) return;
|
||||
|
||||
// Ждем 500мс после того, как первый кадр отрисовался
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (!mounted) return;
|
||||
// Запускаем анимацию
|
||||
_controller.forward().then((_) {
|
||||
if (!mounted) return;
|
||||
context.read<SplashBloc>().add(AuthCheckRequested());
|
||||
});
|
||||
// запускаем анимацию
|
||||
_controller.forward().then((_) async {
|
||||
// небольшая пауза после анимации
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
context.read<SplashBloc>().add(AuthCheckRequested());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -77,102 +56,69 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final double endTranslation = screenWidth / 2 + logoSize;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF293A69),
|
||||
Color(0xFF202741),
|
||||
],
|
||||
backgroundColor: const Color(0xFF3A3A3A),
|
||||
body: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final double offset = _revealAnimation.value * (logoSize * 1.2);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Цветной логотип (на заднем плане)
|
||||
Image.asset(
|
||||
'assets/logo_color.png',
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Волна
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Image.asset(
|
||||
'assets/wave.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Image.asset(
|
||||
'assets/splash_map.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final double translationX = _rollTranslation.value * endTranslation;
|
||||
|
||||
return Transform(
|
||||
transform: Matrix4.translationValues(translationX, 0, 0)
|
||||
..rotateZ(_rollRotation.value),
|
||||
alignment: Alignment.center,
|
||||
// Используем ShaderMask для эффекта заполнения/проявления
|
||||
child: ShaderMask(
|
||||
shaderCallback: (bounds) {
|
||||
return LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
// Используем _fillProgress для сдвига жесткой границы градиента
|
||||
colors: const [Colors.white, Colors.transparent],
|
||||
stops: [_fillProgress.value, _fillProgress.value],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.dstIn, // Оставляет только ту часть логотипа, где градиент белый
|
||||
child: Image.asset(
|
||||
'assets/splash_logo.png',
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
fit: BoxFit.contain,
|
||||
// Прямоугольник, который "уезжает" вправо, открывая логотип
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: Container(
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
color: const Color(0xFF3A3A3A),
|
||||
transform: Matrix4.translationValues(offset, 0, 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 5. Версия приложения
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 24,
|
||||
child: Text(
|
||||
'Версия приложения 1.0',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
// Обводка логотипа (поверх)
|
||||
Image.asset(
|
||||
'assets/logo_outline.png',
|
||||
width: logoSize * 1.01,
|
||||
height: logoSize * 1.01,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Text(
|
||||
'Версия приложения 1.0',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/app_colors.dart';
|
||||
import '../components/app_checkbox.dart';
|
||||
import '../components/custom_app_bar.dart'; // ✅ Добавь импорт
|
||||
import '../components/gradient_button.dart';
|
||||
import '../components/period_selector.dart';
|
||||
import '../event/subscription_details_event.dart';
|
||||
@@ -19,91 +17,66 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child:
|
||||
BlocConsumer<
|
||||
SubscriptionDetailsBloc,
|
||||
SubscriptionDetailsState
|
||||
>(
|
||||
listenWhen: (previous, current) =>
|
||||
current is DetailsContentState && current.isSuccess,
|
||||
listener: (context, state) {
|
||||
if (state is DetailsContentState && state.isSuccess) {
|
||||
context.pop(true);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
String title = "Загрузка...";
|
||||
if (state is DetailsContentState) {
|
||||
title = state.subscription.title;
|
||||
}
|
||||
return CustomAppBar(title: title);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 🔹 Контент
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Волна снизу
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Image.asset('assets/wave.png'),
|
||||
),
|
||||
),
|
||||
BlocBuilder<
|
||||
SubscriptionDetailsBloc,
|
||||
SubscriptionDetailsState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
if (state is DetailsLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF80FFD1),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is DetailsError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
state.message,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is DetailsContentState) {
|
||||
return _buildContent(context, state);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF1A2355),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: BlocBuilder<SubscriptionDetailsBloc, SubscriptionDetailsState>(
|
||||
builder: (context, state) {
|
||||
if (state is DetailsContentState) {
|
||||
return Text(state.subscription.title);
|
||||
}
|
||||
return const Text("Загрузка...");
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Image.asset('assets/wave.png'),
|
||||
),
|
||||
),
|
||||
BlocBuilder<SubscriptionDetailsBloc, SubscriptionDetailsState>(
|
||||
builder: (context, state) {
|
||||
if (state is DetailsLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF80FFD1)),
|
||||
);
|
||||
}
|
||||
if (state is DetailsError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
state.message,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is DetailsContentState) {
|
||||
return _buildContent(context, state);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, DetailsContentState state) {
|
||||
final bool isAvailableForPurchase = state.subscription.isActive;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -115,29 +88,22 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
if (isAvailableForPurchase) ...[
|
||||
const SizedBox(height: 30),
|
||||
_ActionCard(state: state),
|
||||
|
||||
_ActionCard(state: state),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
GradientButton(
|
||||
text: state.isAlreadyPurchased ? 'Продлить' : 'Активировать',
|
||||
onTap: () {
|
||||
context.read<SubscriptionDetailsBloc>().add(
|
||||
ActivateSubscriptionPressed(),
|
||||
);
|
||||
},
|
||||
enabled: state.isAgreed,
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
fontSize: 16,
|
||||
showArrows: true,
|
||||
GradientButton(
|
||||
text: 'Активировать',
|
||||
onTap: () => context.read<SubscriptionDetailsBloc>().add(
|
||||
ActivateSubscriptionPressed(),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
fontSize: 16,
|
||||
showArrows: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -159,6 +125,9 @@ class _ActionCard extends StatelessWidget {
|
||||
state.selectedPeriod,
|
||||
);
|
||||
|
||||
context.read<SubscriptionDetailsBloc>().add(
|
||||
SelectPeriodEvent(state.subscription.options[selectedIndex]));
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
@@ -236,7 +205,7 @@ class _PriceRow extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset("assets/icons/money_icon.png", width: 72, height: 72),
|
||||
const SizedBox(width: 15),
|
||||
SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -256,3 +225,4 @@ class _PriceRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:be_happy/presentation/event/subscription_list_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -22,7 +21,6 @@ class SubscriptionsListScreen extends StatelessWidget {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomAppBar(title: 'Абонементы'),
|
||||
@@ -45,26 +43,17 @@ class SubscriptionsListScreen extends StatelessWidget {
|
||||
if (state is SubscriptionsLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is SubscriptionsLoaded) {
|
||||
final clientSubsMap = {
|
||||
for (var sub in state.activeSubscriptions) sub.subscriptionId: sub
|
||||
};
|
||||
|
||||
final activeIds = state.activeSubscriptions.map((e) => e.id).toSet();
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
itemCount: state.subscriptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final subscription = state.subscriptions[index];
|
||||
|
||||
final bool isPurchased = subscription.isCurrent;
|
||||
|
||||
final clientSub = clientSubsMap[subscription.id];
|
||||
final DateTime? expirationDate = isPurchased ? clientSub?.expiredAt : null;
|
||||
final bool isActive = activeIds.contains(subscription.id);
|
||||
|
||||
return SubscriptionCard(
|
||||
subscription: subscription,
|
||||
isActive: isPurchased,
|
||||
expiredAt: expirationDate,
|
||||
onRefresh: () { context.read<SubscriptionListBloc>().add(LoadSubscriptionsEvent());},
|
||||
isActive: isActive,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -21,55 +21,53 @@ class TopUpScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const CustomAppBar(title: 'Пополнение баланса'),
|
||||
const SizedBox(height: 20),
|
||||
backgroundColor: const Color(0xFF1A234E),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const CustomAppBar(title: 'Пополнение баланса'),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
BlocBuilder<TopUpBloc, TopUpState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
BlocBuilder<TopUpBloc, TopUpState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTariffList(state, context),
|
||||
const SizedBox(height: 30),
|
||||
_buildPriceInfo(state),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Способ оплаты',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_buildCardSelector(state, context),
|
||||
const SizedBox(height: 20),
|
||||
_buildAgreement(state, context),
|
||||
const Spacer(),
|
||||
_buildPayButton(state),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
return Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTariffList(state, context),
|
||||
const SizedBox(height: 30),
|
||||
_buildPriceInfo(state),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Способ оплаты',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_buildCardSelector(state, context),
|
||||
const SizedBox(height: 20),
|
||||
_buildAgreement(state, context),
|
||||
const Spacer(),
|
||||
_buildPayButton(state),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -170,7 +168,7 @@ class TopUpScreen extends StatelessWidget {
|
||||
create: (context) =>
|
||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>())
|
||||
..add(PaymentMethodSheetStarted()),
|
||||
child: const PaymentMethodSheet(showBalance: false,),
|
||||
child: const PaymentMethodSheet(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ class ActiveRideState {
|
||||
final double cost;
|
||||
final bool isPaused;
|
||||
final bool inZone;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
|
||||
const ActiveRideState({
|
||||
this.status = ActiveRideStatus.initial,
|
||||
@@ -22,8 +20,6 @@ class ActiveRideState {
|
||||
this.elapsedTime = Duration.zero,
|
||||
this.speed = 0.0,
|
||||
this.distance = 0.0,
|
||||
this.latitude = 0.0,
|
||||
this.longitude = 0.0,
|
||||
this.cost = 0.0,
|
||||
this.isPaused = false,
|
||||
this.inZone = true,
|
||||
@@ -37,8 +33,6 @@ class ActiveRideState {
|
||||
double? speed,
|
||||
double? distance,
|
||||
double? cost,
|
||||
double? longitude,
|
||||
double? latitude,
|
||||
bool? isPaused,
|
||||
bool? inZone,
|
||||
}) {
|
||||
@@ -50,8 +44,6 @@ class ActiveRideState {
|
||||
speed: speed ?? this.speed,
|
||||
distance: distance ?? this.distance,
|
||||
cost: cost ?? this.cost,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
isPaused: isPaused ?? this.isPaused,
|
||||
inZone: inZone ?? this.inZone,
|
||||
);
|
||||
|
||||