diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..c647d81 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..2bb4769 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..e8a2188 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..21bfa5e 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..ab31fc9 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/news_def.png b/assets/news_def.png new file mode 100644 index 0000000..2102c55 Binary files /dev/null and b/assets/news_def.png differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 09b8034..48894e7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -427,7 +427,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -484,7 +484,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..d0d98aa 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "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" - } -} +{"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"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4..1dd276b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 7353c41..34458af 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452..3711926 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d93..d625354 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cd7b00..fe907cf 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe73094..d31bb50 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773c..61706cb 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452..3711926 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463..4a0b2de 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec3034..6c2fb29 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..36d18eb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..e1ac9e4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..3bbefb0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..f437fd4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 0ec3034..6c2fb29 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea..54e93a6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..c647d81 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..21bfa5e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 84ac32a..87b2bc7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba..4ef52a2 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf1..7a28ee6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib.rar b/lib.rar deleted file mode 100644 index 02c830e..0000000 Binary files a/lib.rar and /dev/null differ diff --git a/lib/data/network/api_service.dart b/lib/data/network/api_service.dart index f67e822..1be610c 100644 --- a/lib/data/network/api_service.dart +++ b/lib/data/network/api_service.dart @@ -1038,6 +1038,55 @@ class ApiService { return controller.stream; } + Future>> 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>(); + } else if (data is Map) { + final list = data['data']; + if (list is List) { + return list.cast>(); + } 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> getCertificates() async { try { final response = await _dio.get( diff --git a/lib/data/repositories/notification_repository_impl.dart b/lib/data/repositories/notification_repository_impl.dart index e75d066..f7f0cdf 100644 --- a/lib/data/repositories/notification_repository_impl.dart +++ b/lib/data/repositories/notification_repository_impl.dart @@ -33,4 +33,22 @@ class NotificationRepositoryImpl implements NotificationRepository { void closeStream() { // соединение закрывается автоматически при отписке от stream } + + @override + Future> getNotifications() async { + try { + final List> 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'); + } + } } diff --git a/lib/di/service_locator.dart b/lib/di/service_locator.dart index 602cd0f..77a9d8e 100644 --- a/lib/di/service_locator.dart +++ b/lib/di/service_locator.dart @@ -92,6 +92,7 @@ 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'; @@ -100,6 +101,7 @@ 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'; @@ -286,6 +288,13 @@ Future setupDependencies() async { getIt.registerSingleton( GetScooterByTitleUsecase(getIt()), ); + getIt.registerSingleton( + GetNotificationsUsecase(getIt()), + ); + getIt.registerFactory( + () => NotificationsBloc(getIt()), + ); + // Blocs getIt.registerLazySingleton(() => SplashBloc(getIt())); diff --git a/lib/domain/repositories/notification_repository.dart b/lib/domain/repositories/notification_repository.dart index 9f3b044..7d58dac 100644 --- a/lib/domain/repositories/notification_repository.dart +++ b/lib/domain/repositories/notification_repository.dart @@ -9,4 +9,7 @@ abstract class NotificationRepository { /// Закрывает SSE-соединение void closeStream(); + + /// получить список уведомлений + Future> getNotifications(); } diff --git a/lib/domain/usecase/get_notifications_usecase.dart b/lib/domain/usecase/get_notifications_usecase.dart new file mode 100644 index 0000000..f67f238 --- /dev/null +++ b/lib/domain/usecase/get_notifications_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/client_notification.dart'; +import '../repositories/notification_repository.dart'; + +class GetNotificationsUsecase { + final NotificationRepository repository; + + GetNotificationsUsecase(this.repository); + + Future> call() { + return repository.getNotifications(); + } +} \ No newline at end of file diff --git a/lib/presentation/event/notifications_event.dart b/lib/presentation/event/notifications_event.dart new file mode 100644 index 0000000..b4eb4b1 --- /dev/null +++ b/lib/presentation/event/notifications_event.dart @@ -0,0 +1,3 @@ +sealed class NotificationsEvent {} + +class NotificationsFetchRequested extends NotificationsEvent {} \ No newline at end of file diff --git a/lib/presentation/navigation/app_router.dart b/lib/presentation/navigation/app_router.dart index e1ed39c..95799f7 100644 --- a/lib/presentation/navigation/app_router.dart +++ b/lib/presentation/navigation/app_router.dart @@ -75,6 +75,7 @@ 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'; @@ -456,6 +457,10 @@ class AppRouter { builder: (context, state) => const OrderHistoryScreen(), routes: [] ), + GoRoute( + path: 'notifications', + builder: (context, state) => const NotificationsScreen(), + ), ], ), ], diff --git a/lib/presentation/screens/add_card_screen.dart b/lib/presentation/screens/add_card_screen.dart index e6f55f7..27381db 100644 --- a/lib/presentation/screens/add_card_screen.dart +++ b/lib/presentation/screens/add_card_screen.dart @@ -35,6 +35,7 @@ class AddCardScreen extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ + const SizedBox(height: 16), const CustomAppBar(title: 'Добавление карты'), const SizedBox(height: 24), diff --git a/lib/presentation/screens/license_agreement_screen.dart b/lib/presentation/screens/license_agreement_screen.dart index c8d3599..526c58b 100644 --- a/lib/presentation/screens/license_agreement_screen.dart +++ b/lib/presentation/screens/license_agreement_screen.dart @@ -14,7 +14,7 @@ class LicenseAgreementScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ - // 🔹 APPBAR С КНОПКОЙ НАЗАД + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: ' '), diff --git a/lib/presentation/screens/map_screen.dart b/lib/presentation/screens/map_screen.dart index f7d35b2..0871cf3 100644 --- a/lib/presentation/screens/map_screen.dart +++ b/lib/presentation/screens/map_screen.dart @@ -696,13 +696,13 @@ class _MapScreenState extends State { children: [ _RoundIconButton( icon: Icons.notifications_sharp, - onPressed: _onNotificationTap, + onPressed: () => context.push("/home/notifications"), ), const SizedBox(height: 12), - _RoundIconButton( - icon: Icons.directions_run, + _RoundButton( + imagePath: 'assets/icons/scooter_placemark.png', onPressed: () => context.push("/home/current-rides-sheet"), - ), + ) ], ), ), @@ -810,6 +810,40 @@ 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; diff --git a/lib/presentation/screens/news_detail_screen.dart b/lib/presentation/screens/news_detail_screen.dart index cdc853c..1a4a8a7 100644 --- a/lib/presentation/screens/news_detail_screen.dart +++ b/lib/presentation/screens/news_detail_screen.dart @@ -61,7 +61,7 @@ class _NewsDetailScreenState extends State { child: SafeArea( child: Column( children: [ - // 🔹 Заголовок в AppBar + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: widget.title), diff --git a/lib/presentation/screens/news_screen.dart b/lib/presentation/screens/news_screen.dart index 965f7c8..8555a5f 100644 --- a/lib/presentation/screens/news_screen.dart +++ b/lib/presentation/screens/news_screen.dart @@ -4,6 +4,7 @@ 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'; @@ -14,11 +15,8 @@ class NewsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - dev.log('🔍 NewsScreen: Создание экрана новостей'); - return BlocProvider( create: (context) { - dev.log('🔍 NewsScreen: Создание NewsBloc'); return getIt()..add(const NewsFetchRequested()); }, child: const NewsView(), @@ -31,7 +29,6 @@ class NewsView extends StatelessWidget { @override Widget build(BuildContext context) { - dev.log('🔍 NewsView: Построение UI'); return Scaffold( body: Container( @@ -48,7 +45,6 @@ class NewsView extends StatelessWidget { Expanded( child: BlocBuilder( builder: (context, state) { - dev.log('🔍 NewsView: Состояние ${state.status}, новостей: ${state.news.length}'); if (state.status == NewsStatus.initial || state.status == NewsStatus.loading) { return const Center( @@ -84,7 +80,6 @@ class NewsView extends StatelessWidget { const SizedBox(height: 24), ElevatedButton( onPressed: () { - dev.log('🔍 NewsView: Повторная загрузка'); context.read().add(const NewsFetchRequested()); }, style: ElevatedButton.styleFrom( @@ -173,7 +168,7 @@ class _NewsCard extends StatelessWidget { margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF141530), + color: const Color(0xFF0A0F2E).withOpacity(0.7), borderRadius: BorderRadius.circular(16), ), child: Column( @@ -196,6 +191,18 @@ 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( @@ -205,51 +212,28 @@ class _NewsCard extends StatelessWidget { ), ), const SizedBox(height: 16), - SizedBox( - height: 40, - child: OutlinedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NewsDetailScreen( - newsId: news.id, - title: news.title, + + 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, + ), ), - ), - ); - }, - 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), - ), - ], + ); + }, + showArrows: true, + fontSize: 14, + height: 40, + width: double.infinity, ), ), ), diff --git a/lib/presentation/screens/notifications_screen.dart b/lib/presentation/screens/notifications_screen.dart new file mode 100644 index 0000000..d3777d0 --- /dev/null +++ b/lib/presentation/screens/notifications_screen.dart @@ -0,0 +1,350 @@ +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()..add(NotificationsFetchRequested()), + child: NotificationsView( + filter: _filter, + onFilterChanged: _setFilter, + ), + ); + } +} + +class NotificationsView extends StatelessWidget { + final NotificationFilter filter; + final ValueChanged 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( + 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().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().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 'Самокат'; + } + } +} \ No newline at end of file diff --git a/lib/presentation/screens/order_history_detail_screen.dart b/lib/presentation/screens/order_history_detail_screen.dart index e39d1fb..66f2e06 100644 --- a/lib/presentation/screens/order_history_detail_screen.dart +++ b/lib/presentation/screens/order_history_detail_screen.dart @@ -32,7 +32,7 @@ class OrderHistoryDetailScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ - // 🔹 HEADER + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'Поездка $date'), diff --git a/lib/presentation/screens/order_history_screen.dart b/lib/presentation/screens/order_history_screen.dart index 612b3f1..4257a5c 100644 --- a/lib/presentation/screens/order_history_screen.dart +++ b/lib/presentation/screens/order_history_screen.dart @@ -32,6 +32,7 @@ class OrderHistoryView extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'История поездок'), diff --git a/lib/presentation/screens/payment_confirm_screen.dart b/lib/presentation/screens/payment_confirm_screen.dart index 8b05cba..4eb8dbb 100644 --- a/lib/presentation/screens/payment_confirm_screen.dart +++ b/lib/presentation/screens/payment_confirm_screen.dart @@ -62,6 +62,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'Завершение поездки'), diff --git a/lib/presentation/screens/payment_methods_screen.dart b/lib/presentation/screens/payment_methods_screen.dart index 0e2f13d..e231908 100644 --- a/lib/presentation/screens/payment_methods_screen.dart +++ b/lib/presentation/screens/payment_methods_screen.dart @@ -19,6 +19,7 @@ class PaymentMethodsScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'Способы оплаты'), diff --git a/lib/presentation/screens/privacy_policy_screen.dart b/lib/presentation/screens/privacy_policy_screen.dart index c4c6430..18838e0 100644 --- a/lib/presentation/screens/privacy_policy_screen.dart +++ b/lib/presentation/screens/privacy_policy_screen.dart @@ -14,6 +14,7 @@ class PrivacyPolicyScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: ''), diff --git a/lib/presentation/screens/promo_code_screen.dart b/lib/presentation/screens/promo_code_screen.dart index d9a510b..46836e7 100644 --- a/lib/presentation/screens/promo_code_screen.dart +++ b/lib/presentation/screens/promo_code_screen.dart @@ -45,6 +45,7 @@ class _PromoCodeScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ + const SizedBox(height: 16), CustomAppBar(title: 'Промокоды'), const SizedBox(height: 32), diff --git a/lib/presentation/screens/qr_scan_info_screen.dart b/lib/presentation/screens/qr_scan_info_screen.dart index 972c7d3..21868a6 100644 --- a/lib/presentation/screens/qr_scan_info_screen.dart +++ b/lib/presentation/screens/qr_scan_info_screen.dart @@ -17,6 +17,7 @@ class QRScanInfoScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: "Сканирование QR-кода"), diff --git a/lib/presentation/screens/qr_scan_screen.dart b/lib/presentation/screens/qr_scan_screen.dart index 6130a8a..11ae6fa 100644 --- a/lib/presentation/screens/qr_scan_screen.dart +++ b/lib/presentation/screens/qr_scan_screen.dart @@ -6,6 +6,7 @@ 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 { @@ -149,15 +150,17 @@ class _QrScanScreenState extends State { ), ), + // ✅ ИЗМЕНЕНО: прижимаем аппбар к левому краю 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-код — номер будет распознан автоматически', - textAlign: TextAlign.center, + textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 16, @@ -170,7 +173,6 @@ class _QrScanScreenState extends State { ], ), ), - ), SafeArea( child: Align( diff --git a/lib/presentation/screens/scooter_code_input_screen.dart b/lib/presentation/screens/scooter_code_input_screen.dart index 6fddc3f..774fd1b 100644 --- a/lib/presentation/screens/scooter_code_input_screen.dart +++ b/lib/presentation/screens/scooter_code_input_screen.dart @@ -58,6 +58,7 @@ class _ScooterCodeInputScreenState extends State { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: "Ввод QR-кода"), diff --git a/lib/presentation/screens/scooter_detail_screen.dart b/lib/presentation/screens/scooter_detail_screen.dart index 8ed64b8..ea286eb 100644 --- a/lib/presentation/screens/scooter_detail_screen.dart +++ b/lib/presentation/screens/scooter_detail_screen.dart @@ -70,6 +70,7 @@ class ScooterDetailScreen extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ + const SizedBox(height: 16), CustomAppBar( title: scooter?.title != null ? 'Самокат ${scooter!.title}' : 'Самокат', ), diff --git a/lib/presentation/screens/send_photo_screen.dart b/lib/presentation/screens/send_photo_screen.dart index af080eb..041ff6c 100644 --- a/lib/presentation/screens/send_photo_screen.dart +++ b/lib/presentation/screens/send_photo_screen.dart @@ -123,6 +123,11 @@ class _SendPhotoViewState extends State { 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( diff --git a/lib/presentation/screens/subscription_details_screen.dart b/lib/presentation/screens/subscription_details_screen.dart index 6efe23e..ffde1ce 100644 --- a/lib/presentation/screens/subscription_details_screen.dart +++ b/lib/presentation/screens/subscription_details_screen.dart @@ -2,7 +2,9 @@ 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'; @@ -17,66 +19,74 @@ class SubscriptionDetailsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - 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( - builder: (context, state) { - if (state is DetailsContentState) { - return Text(state.subscription.title); - } - return const Text("Загрузка..."); - }, + 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: BlocBuilder( + 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( + 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(); + }, + ), + ], + ), + ), + ], + ), ), ), - body: Stack( - children: [ - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Opacity( - opacity: 0.5, - child: Image.asset('assets/wave.png'), - ), - ), - BlocBuilder( - 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) { return SingleChildScrollView( - padding: const EdgeInsets.all(20.0), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -104,6 +114,7 @@ class SubscriptionDetailsScreen extends StatelessWidget { fontSize: 16, showArrows: true, ), + const SizedBox(height: 20), ], ), ); @@ -125,9 +136,6 @@ class _ActionCard extends StatelessWidget { state.selectedPeriod, ); - context.read().add( - SelectPeriodEvent(state.subscription.options[selectedIndex])); - return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( @@ -205,7 +213,7 @@ class _PriceRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset("assets/icons/money_icon.png", width: 72, height: 72), - SizedBox(width: 15), + const SizedBox(width: 15), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -224,5 +232,4 @@ class _PriceRow extends StatelessWidget { ), ); } -} - +} \ No newline at end of file diff --git a/lib/presentation/screens/subscription_list_screen.dart b/lib/presentation/screens/subscription_list_screen.dart index c176b95..d5094c6 100644 --- a/lib/presentation/screens/subscription_list_screen.dart +++ b/lib/presentation/screens/subscription_list_screen.dart @@ -21,6 +21,7 @@ class SubscriptionsListScreen extends StatelessWidget { child: SafeArea( child: Column( children: [ + const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomAppBar(title: 'Абонементы'), diff --git a/lib/presentation/screens/top_up_screen.dart b/lib/presentation/screens/top_up_screen.dart index 8caba4d..ba83f86 100644 --- a/lib/presentation/screens/top_up_screen.dart +++ b/lib/presentation/screens/top_up_screen.dart @@ -21,53 +21,55 @@ class TopUpScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - 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), + 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), - BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const Center(child: CircularProgressIndicator()); - } + BlocBuilder( + 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), + ], + ), + ], + ), + ); + }, + ), + ], + ), ), - ), + ) ), ); } diff --git a/lib/presentation/state/notifications_state.dart b/lib/presentation/state/notifications_state.dart new file mode 100644 index 0000000..ea5728f --- /dev/null +++ b/lib/presentation/state/notifications_state.dart @@ -0,0 +1,27 @@ +import '../../domain/entities/client_notification.dart'; + +enum NotificationsStatus { initial, loading, success, failure } + +class NotificationsState { + final NotificationsStatus status; + final List notifications; + final String? errorMessage; + + const NotificationsState({ + this.status = NotificationsStatus.initial, + this.notifications = const [], + this.errorMessage, + }); + + NotificationsState copyWith({ + NotificationsStatus? status, + List? notifications, + String? errorMessage, + }) { + return NotificationsState( + status: status ?? this.status, + notifications: notifications ?? this.notifications, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} \ No newline at end of file diff --git a/lib/presentation/viewmodel/notifications_bloc.dart b/lib/presentation/viewmodel/notifications_bloc.dart new file mode 100644 index 0000000..1bcec78 --- /dev/null +++ b/lib/presentation/viewmodel/notifications_bloc.dart @@ -0,0 +1,34 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'dart:developer' as dev; +import '../../domain/usecase/get_notifications_usecase.dart'; +import '../event/notifications_event.dart'; +import '../state/notifications_state.dart'; + +class NotificationsBloc extends Bloc { + final GetNotificationsUsecase _getNotificationsUsecase; + + NotificationsBloc(this._getNotificationsUsecase) : super(const NotificationsState()) { + on(_onFetchRequested); + } + + Future _onFetchRequested( + NotificationsFetchRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: NotificationsStatus.loading)); + + try { + final notifications = await _getNotificationsUsecase(); + emit(state.copyWith( + status: NotificationsStatus.success, + notifications: notifications, + )); + } catch (e) { + dev.log('NotificationsBloc: Ошибка загрузки: $e'); + emit(state.copyWith( + status: NotificationsStatus.failure, + errorMessage: e.toString(), + )); + } + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index d3d0be7..72cc1fc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: "direct main" description: name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.13.0" bloc: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.9" + version: "1.0.8" device_info_plus: dependency: "direct main" description: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" url: "https://pub.dev" source: hosted - version: "0.9.5" + version: "0.9.4+4" file_selector_platform_interface: dependency: transitive description: @@ -274,10 +274,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 url: "https://pub.dev" source: hosted - version: "2.0.34" + version: "2.0.31" flutter_secure_storage: dependency: "direct main" description: @@ -436,18 +436,18 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" url: "https://pub.dev" source: hosted - version: "0.8.13+17" + version: "0.8.13+1" image_picker_for_web: dependency: transitive description: @@ -460,10 +460,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e url: "https://pub.dev" source: hosted - version: "0.8.13+3" + version: "0.8.13" image_picker_linux: dependency: transitive description: @@ -476,10 +476,10 @@ packages: dependency: transitive description: name: image_picker_macos - sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 url: "https://pub.dev" source: hosted - version: "0.2.2+1" + version: "0.2.2" image_picker_platform_interface: dependency: transitive description: @@ -504,22 +504,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - jni: - dependency: transitive - description: - name: jni - sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.dev" - source: hosted - version: "1.0.0" - jni_flutter: - dependency: transitive - description: - name: jni_flutter - sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" js: dependency: transitive description: @@ -532,34 +516,34 @@ packages: dependency: transitive description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -632,14 +616,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" path: dependency: "direct main" description: @@ -660,18 +636,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.2.19" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -740,26 +716,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e url: "https://pub.dev" source: hosted - version: "2.4.23" + version: "2.4.13" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -772,10 +748,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: @@ -801,10 +777,10 @@ packages: dependency: transitive description: name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.2" + version: "1.10.1" stack_trace: dependency: transitive description: @@ -841,10 +817,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.4" typed_data: dependency: transitive description: @@ -865,18 +841,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.20" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.3.4" url_launcher_linux: dependency: transitive description: @@ -889,10 +865,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.3" url_launcher_platform_interface: dependency: transitive description: @@ -929,18 +905,18 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "15.2.0" + version: "15.0.0" web: dependency: transitive description: @@ -998,5 +974,5 @@ packages: source: hosted version: "4.2.1" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.6" + dart: ">=3.8.1 <4.0.0" + flutter: ">=3.32.0"