From 6f6e34e39154a8ca96ae796b71f02389a0b0d1f4 Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Sat, 26 Jul 2025 14:32:07 +0330 Subject: [PATCH] Finished Base --- .../positive-notification-alert-351299.mp3 | Bin 0 -> 34560 bytes lib/core/config/api_config.dart | 4 +- lib/core/config/http_overrides.dart | 12 + lib/data/models/offer_model.dart | 105 +-- lib/main.dart | 44 +- lib/presentation/auth/bloc/auth_bloc.dart | 35 +- .../bloc/notification_preferences_bloc.dart | 11 +- lib/presentation/offer/bloc/offer_bloc.dart | 36 +- .../bloc/widgets/category_offers_row.dart | 3 +- .../offer/bloc/widgets/offer_card.dart | 41 +- lib/presentation/pages/add_photo_screen.dart | 72 +- lib/presentation/pages/login_page.dart | 80 +- .../pages/notification_preferences_page.dart | 134 ++- lib/presentation/pages/offers_page.dart | 152 +++- lib/presentation/pages/onboarding_page.dart | 84 +- lib/presentation/pages/otp_page.dart | 38 +- .../pages/product_detail_page.dart | 777 ++++++++++-------- .../pages/reservation_details_screen.dart | 198 +++-- .../pages/reserved_list_page.dart | 124 ++- lib/presentation/pages/user_info_page.dart | 38 +- .../reservation/cubit/reservation_cubit.dart | 7 + .../widgets/reserved_list_item_card.dart | 96 ++- lib/services/mqtt_service.dart | 13 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 120 +++ pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 30 files changed, 1384 insertions(+), 861 deletions(-) create mode 100644 assets/sounds/positive-notification-alert-351299.mp3 create mode 100644 lib/core/config/http_overrides.dart diff --git a/assets/sounds/positive-notification-alert-351299.mp3 b/assets/sounds/positive-notification-alert-351299.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..19d743405197c5eecd9ecae9208491e685c765db GIT binary patch literal 34560 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

-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 N8S4q4qk_XZ1OVv$z(D{2 literal 0 HcmV?d00001 diff --git a/lib/core/config/api_config.dart b/lib/core/config/api_config.dart index 21eaf36..201f843 100644 --- a/lib/core/config/api_config.dart +++ b/lib/core/config/api_config.dart @@ -4,5 +4,7 @@ class ApiConfig { static const String verifyCode = "/login/getcode"; static const String updateUser = "/user/updateName"; static const String updateCategories = "/user/favoriteCategory"; - static const String getFavoriteCategories = "/user/getfavoriteCategory"; // این خط اضافه شد + static const String getFavoriteCategories = "/user/getfavoriteCategory"; + static const String addReservation = "/reservation/add"; + static const String getReservations = "/reservation/get"; // <-- این خط اضافه شد } \ No newline at end of file diff --git a/lib/core/config/http_overrides.dart b/lib/core/config/http_overrides.dart new file mode 100644 index 0000000..b4e6b32 --- /dev/null +++ b/lib/core/config/http_overrides.dart @@ -0,0 +1,12 @@ +// lib/core/config/http_overrides.dart + +import 'dart:io'; + +class MyHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) { + return super.createHttpClient(context) + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + } +} \ No newline at end of file diff --git a/lib/data/models/offer_model.dart b/lib/data/models/offer_model.dart index eb188a4..7a446be 100644 --- a/lib/data/models/offer_model.dart +++ b/lib/data/models/offer_model.dart @@ -1,11 +1,7 @@ -// lib/data/models/offer_model.dart - import 'package:equatable/equatable.dart'; import 'package:proxibuy/data/models/comment_model.dart'; -import 'package:proxibuy/data/models/discount_info_model.dart'; import 'package:proxibuy/data/models/working_hours.dart'; -// کلاس کمکی برای داده‌های فروشگاه class ShopData { final String id; final String name; @@ -31,8 +27,8 @@ class ShopData { name: json['Name'] ?? 'نام فروشگاه نامشخص', category: json['Category'] ?? 'بدون دسته‌بندی', address: json['Address'] ?? 'آدرس نامشخص', - latitude: (json['Map']['coordinates'][1] as num?)?.toDouble() ?? 0.0, - longitude: (json['Map']['coordinates'][0] as num?)?.toDouble() ?? 0.0, + latitude: (json['Map']?['coordinates']?[1] as num?)?.toDouble() ?? 0.0, + longitude: (json['Map']?['coordinates']?[0] as num?)?.toDouble() ?? 0.0, properties: List.from(json['Property'] ?? []), ); } @@ -58,9 +54,8 @@ class OfferModel extends Equatable { final double originalPrice; final double finalPrice; final List features; - final DiscountInfoModel? discountInfo; + final String discountInfo; final List comments; - final String qrCodeData; const OfferModel({ required this.id, @@ -82,92 +77,82 @@ class OfferModel extends Equatable { required this.originalPrice, required this.finalPrice, this.features = const [], - this.discountInfo, + required this.discountInfo, this.comments = const [], - required this.qrCodeData, }); - factory OfferModel.fromJson(Map json) { // <-- پارامتر calculatedDistance حذف شد - final shopData = ShopData.fromJson(json['shopData']); + factory OfferModel.fromJson(Map json) { + final shopDataJson = json['shopData'] as Map? ?? {}; + final shopData = ShopData.fromJson(shopDataJson); final now = DateTime.now(); bool checkIsOpen = false; try { - final startTimeParts = (json['StartTime'] as String).split(':'); - final endTimeParts = (json['EndTime'] as String).split(':'); - final startHour = int.parse(startTimeParts[0]); - final startMinute = int.parse(startTimeParts[1]); - final endHour = int.parse(endTimeParts[0]); - final endMinute = int.parse(endTimeParts[1]); + final startTimeString = json['StartTime'] as String?; + final endTimeString = json['EndTime'] as String?; + if (startTimeString != null && endTimeString != null) { + final startTimeParts = startTimeString.split(':'); + final endTimeParts = endTimeString.split(':'); + final startHour = int.parse(startTimeParts[0]); + final startMinute = int.parse(startTimeParts[1]); + final endHour = int.parse(endTimeParts[0]); + final endMinute = int.parse(endTimeParts[1]); - final startTime = DateTime(now.year, now.month, now.day, startHour, startMinute); - final endTime = DateTime(now.year, now.month, now.day, endHour, endMinute); + final startTime = DateTime(now.year, now.month, now.day, startHour, startMinute); + final endTime = DateTime(now.year, now.month, now.day, endHour, endMinute); - checkIsOpen = now.isAfter(startTime) && now.isBefore(endTime); + checkIsOpen = now.isAfter(startTime) && now.isBefore(endTime); + } } catch(e) { checkIsOpen = false; } final originalPriceValue = (json['Price'] as num?)?.toDouble() ?? 0.0; final finalPriceValue = (json['NPrice'] as num?)?.toDouble() ?? 0.0; - - // **رفع خطا: تبدیل امن عدد فاصله به int** - final distanceFromServer = (json['distance'] as num?)?.round() ?? 0; + // ******** شروع تغییر ۱ ******** + // مدیریت هوشمند فاصله و عکس‌ها از منابع مختلف + final distanceFromServer = (json['distance'] as num?)?.round() ?? (json['Distance'] as num?)?.round() ?? 0; + + List images = []; + if (json['imageData'] is List) { // برای سازگاری با پاسخ MQTT + images = (json['imageData'] as List) + .map((imageData) => imageData['Url']?.toString() ?? '') + .where((url) => url.isNotEmpty) + .toList(); + } else if (json['Images'] is List) { // برای پاسخ جدید API رزرو + images = (json['Images'] as List) + .map((imageData) => (imageData is Map) ? imageData['Url']?.toString() ?? '' : '') + .where((url) => url.isNotEmpty) + .toList(); + } + + final typeData = json['typeData'] ?? json['Type']; + final discountTypeName = (typeData is Map) ? typeData['Name']?.toString() : typeData?.toString(); + // ******** پایان تغییر ۱ ******** return OfferModel( id: json['ID'] ?? '', title: json['Name'] ?? 'بدون عنوان', discount: json['Description'] ?? '', - imageUrls: (json['Images'] as List?) - ?.map((imgId) => "$imgId") - .toList() ?? [], - category: json['shopData']['Category']?.toString() ?? 'بدون دسته‌بندی', + imageUrls: images, // <-- استفاده از لیست جدید + category: json['categoryData']?['Name']?.toString() ?? '', expiryTime: DateTime.tryParse(json['EndDate'] ?? '') ?? DateTime.now().add(const Duration(days: 1)), - discountType: json['Type']?.toString() ?? '', + discountType: discountTypeName ?? '', originalPrice: originalPriceValue, finalPrice: finalPriceValue, - qrCodeData: json['QRcode'] ?? '', storeName: shopData.name, address: shopData.address, latitude: shopData.latitude, longitude: shopData.longitude, features: shopData.properties, - distanceInMeters: distanceFromServer, // <-- **استفاده از مسافت سرور** + distanceInMeters: distanceFromServer, // <-- استفاده از متغیر جدید isOpen: checkIsOpen, workingHours: [], rating: 0.0, ratingCount: 0, comments: [], - discountInfo: null, - ); - } - - - OfferModel copyWith() { - return OfferModel( - id: id, - storeName: storeName, - title: title, - discount: discount, - imageUrls: imageUrls, - category: category, - distanceInMeters: distanceInMeters, - expiryTime: expiryTime, - address: address, - workingHours: workingHours, - discountType: discountType, - isOpen: isOpen, - rating: rating, - ratingCount: ratingCount, - latitude: latitude, - longitude: longitude, - originalPrice: originalPrice, - finalPrice: finalPrice, - features: features, - discountInfo: discountInfo, - comments: comments, - qrCodeData: qrCodeData, + discountInfo: json['Description'], ); } diff --git a/lib/main.dart b/lib/main.dart index 0093235..5461181 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,11 @@ +import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // ignore: depend_on_referenced_packages import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; -import 'package:proxibuy/data/repositories/offer_repository.dart'; +import 'package:proxibuy/core/config/http_overrides.dart'; // این خط را اضافه کنید import 'package:proxibuy/firebase_options.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; @@ -18,6 +18,8 @@ import 'package:proxibuy/presentation/pages/splash_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + HttpOverrides.global = MyHttpOverrides(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); Animate.restartOnHotReload = true; runApp(const MyApp()); @@ -36,7 +38,6 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => AuthBloc()..add(CheckAuthStatusEvent()), ), - // RepositoryProvider برای OfferRepository حذف شد BlocProvider( create: (context) => ReservationCubit(), ), @@ -46,6 +47,7 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => NotificationPreferencesBloc(), ), + ], child: MaterialApp( title: 'Proxibuy', @@ -124,38 +126,4 @@ class MyApp extends StatelessWidget { ), ); } -} - -// class AppRouter extends StatelessWidget { -// const AppRouter({super.key}); - -// @override -// Widget build(BuildContext context) { -// final authState = context.select((AuthBloc bloc) => bloc.state); - -// if (authState is AuthCodeSentSuccess) { -// return OtpPage( -// phoneNumber: "+${authState.countryCode}${authState.phone}", -// phone: authState.phone, -// countryCode: authState.countryCode, -// ); -// } - -// if (authState is AuthLoading) { -// final currentState = context.read().state; -// if (currentState is! AuthCodeSentSuccess) { -// return const Scaffold(body: Center(child: CircularProgressIndicator())); -// } -// } - -// if (authState is AuthSuccess) { -// return const OffersPage(); -// } - -// if (authState is AuthNeedsInfo) { -// return const UserInfoPage(); -// } - -// return const OnboardingPage(); -// } -// } +} \ No newline at end of file diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart index 0cdb4d9..1696767 100644 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ b/lib/presentation/auth/bloc/auth_bloc.dart @@ -3,6 +3,7 @@ import 'package:bloc/bloc.dart'; import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:meta/meta.dart'; import 'package:proxibuy/core/config/api_config.dart'; @@ -49,6 +50,7 @@ class AuthBloc extends Bloc { ApiConfig.baseUrl + ApiConfig.sendCode, data: {'Phone': event.phoneNumber, 'Code': event.countryCode}, ); + if (isClosed) return; if (response.statusCode == 200) { emit(AuthCodeSentSuccess( phone: event.phoneNumber, @@ -58,6 +60,7 @@ class AuthBloc extends Bloc { emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد')); } } on DioException catch (e) { + if (isClosed) return; emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); } } @@ -73,46 +76,74 @@ class AuthBloc extends Bloc { 'OTP': event.otp, }, ); + + if (isClosed) return; // FIX: Add check here + if (response.statusCode == 200) { final accessToken = response.data['data']['accessToken']; final refreshToken = response.data['data']['refreshToken']; - final userID = response.data['data']['ID']; // <-- خط جدید: استخراج ID + final userID = response.data['data']['ID']; await _storage.write(key: 'accessToken', value: accessToken); await _storage.write(key: 'refreshToken', value: refreshToken); - await _storage.write(key: 'userID', value: userID); // <-- خط جدید: ذخیره ID + await _storage.write(key: 'userID', value: userID); emit(AuthNeedsInfo()); } else { emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست')); } } on DioException catch (e) { + if (isClosed) return; // FIX: Add check here emit(AuthFailure(e.response?.data['message'] ?? 'خطایی در سرور رخ داد')); } } Future _onUpdateUserInfo( UpdateUserInfoEvent event, Emitter emit) async { + // ******** لاگ ۱: شروع پردازش ******** + debugPrint("AuthBloc: 🔵 ایونت UpdateUserInfoEvent دریافت شد با نام: ${event.name}"); emit(AuthLoading()); try { final token = await _storage.read(key: 'accessToken'); if (token == null) { + // ******** لاگ خطا: توکن وجود ندارد ******** + debugPrint("AuthBloc: 🔴 خطا: توکن کاربر یافت نشد."); emit(const AuthFailure("شما وارد نشده‌اید.")); return; } + + // ******** لاگ ۲: ارسال درخواست به سرور ******** + debugPrint("AuthBloc: 🟡 در حال ارسال درخواست آپدیت به سرور..."); final response = await _dio.post( ApiConfig.baseUrl + ApiConfig.updateUser, data: {'Name': event.name, 'Gender': event.gender}, options: Options(headers: {'Authorization': 'Bearer $token'}), ); + + // ******** لاگ ۳: پاسخ سرور ******** + debugPrint("AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}"); + + if (isClosed) { + debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است."); + return; + } + if (response.statusCode == 200) { + // ******** لاگ ۴: موفقیت‌آمیز بودن عملیات ******** + debugPrint("AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess..."); emit(AuthSuccess()); } else { + // ******** لاگ خطا: پاسخ ناموفق از سرور ******** + debugPrint("AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}"); emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات')); } } on DioException catch (e) { + // ******** لاگ خطا: خطای Dio ******** + debugPrint("AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}"); + if (isClosed) return; emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); } } + Future _onLogout(LogoutEvent event, Emitter emit) async { await _storage.deleteAll(); emit(AuthInitial()); diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart index 760ea47..9756857 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart @@ -16,7 +16,7 @@ class NotificationPreferencesBloc on(_onLoadCategories); on(_onToggleCategorySelection); on(_onSubmitPreferences); - on(_onLoadFavoriteCategories); // این خط اضافه شد + on(_onLoadFavoriteCategories); add(LoadCategories()); } @@ -55,6 +55,7 @@ class NotificationPreferencesBloc try { final token = await _storage.read(key: 'accessToken'); if (token == null) { + if (isClosed) return; // بررسی قبل از emit emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید.")); return; } @@ -65,6 +66,8 @@ class NotificationPreferencesBloc options: Options(headers: {'Authorization': 'Bearer $token'}), ); + if (isClosed) return; // بررسی قبل از emit + if (response.statusCode == 200) { emit(state.copyWith(isLoading: false, submissionSuccess: true)); } else { @@ -73,19 +76,20 @@ class NotificationPreferencesBloc errorMessage: response.data['message'] ?? 'خطا در ثبت اطلاعات')); } } on DioException catch (e) { + if (isClosed) return; // بررسی قبل از emit emit(state.copyWith( isLoading: false, errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); } } - // این متد اضافه شد Future _onLoadFavoriteCategories( LoadFavoriteCategories event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { final token = await _storage.read(key: 'accessToken'); if (token == null) { + if (isClosed) return; // بررسی قبل از emit emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید.")); return; } @@ -95,6 +99,8 @@ class NotificationPreferencesBloc options: Options(headers: {'Authorization': 'Bearer $token'}), ); + if (isClosed) return; // بررسی قبل از emit + if (response.statusCode == 200) { final List fCategory = response.data['data']['FCategory']; final Set favoriteCategoryIds = @@ -109,6 +115,7 @@ class NotificationPreferencesBloc errorMessage: response.data['message'] ?? 'خطا در دریافت اطلاعات')); } } on DioException catch (e) { + if (isClosed) return; // بررسی قبل از emit emit(state.copyWith( isLoading: false, errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); diff --git a/lib/presentation/offer/bloc/offer_bloc.dart b/lib/presentation/offer/bloc/offer_bloc.dart index ff13e5a..36ec583 100644 --- a/lib/presentation/offer/bloc/offer_bloc.dart +++ b/lib/presentation/offer/bloc/offer_bloc.dart @@ -1,5 +1,3 @@ -// lib/presentation/offer/bloc/offer_bloc.dart - import 'package:bloc/bloc.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; @@ -7,35 +5,21 @@ import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; class OffersBloc extends Bloc { OffersBloc() : super(OffersInitial()) { on(_onOffersReceivedFromMqtt); - on(_onClearOffers); // رویداد جدید برای پاک کردن دیتا + on(_onClearOffers); } void _onOffersReceivedFromMqtt( OffersReceivedFromMqtt event, Emitter emit, ) { - // فقط در صورتی که لیست جدید خالی نباشد، آن را جایگزین کن - if (event.offers.isNotEmpty) { - emit(OffersLoadSuccess(event.offers)); - } - // اگر لیست جدید خالی بود، و قبلا دیتایی داشتیم، حالت را تغییر نده - // این کار از نمایش صفحه خالی جلوگیری می‌کند - else if (state is! OffersLoadSuccess) { - // اگر اولین بار است و لیست خالی است، حالت موفقیت با لیست خالی را نشان بده - emit(const OffersLoadSuccess([])); - } - } - - // برای زمانی که مثلا کاربر GPS را خاموش می‌کند - void _onClearOffers(ClearOffers event, Emitter emit) { - emit(OffersInitial()); - } -} - // مدیریت رویداد جدید - void _onOffersReceivedFromMqtt( - OffersReceivedFromMqtt event, - Emitter emit, - ) { - // جایگزین کردن لیست پیشنهادها با داده‌های جدید + // همیشه حالت موفقیت را با لیست دریافتی منتشر کن (حتی اگر خالی باشد). + // این کار تضمین می‌کند که صفحه از حالت لودینگ خارج می‌شود و رابط کاربری + // آخرین وضعیت (وجود یا عدم وجود تخفیف) را نمایش می‌دهد. emit(OffersLoadSuccess(event.offers)); } + + // برای زمانی که مثلا کاربر GPS را خاموش می‌کند + void _onClearOffers(ClearOffers event, Emitter emit) { + emit(OffersInitial()); + } +} \ No newline at end of file diff --git a/lib/presentation/offer/bloc/widgets/category_offers_row.dart b/lib/presentation/offer/bloc/widgets/category_offers_row.dart index 10a24c3..4554ff6 100644 --- a/lib/presentation/offer/bloc/widgets/category_offers_row.dart +++ b/lib/presentation/offer/bloc/widgets/category_offers_row.dart @@ -44,8 +44,7 @@ class CategoryOffersRow extends StatelessWidget { Navigator.of(context).push( MaterialPageRoute( builder: (_) { - // کل آبجکت offer پاس داده می‌شود - return ProductDetailPage(offer: offer); + return ProductDetailPage(offer: offer,); }, ), ); diff --git a/lib/presentation/offer/bloc/widgets/offer_card.dart b/lib/presentation/offer/bloc/widgets/offer_card.dart index 8e03cc9..ef382c9 100644 --- a/lib/presentation/offer/bloc/widgets/offer_card.dart +++ b/lib/presentation/offer/bloc/widgets/offer_card.dart @@ -33,7 +33,11 @@ class _OfferCardState extends State { padding: const EdgeInsets.only(bottom: 10), child: Column( children: [ - Stack(children: [_buildOfferImage()]), + // Hero ویجت به اینجا اضافه شد + Hero( + tag: 'offer_image_${widget.offer.id}', + child: _buildOfferImage(), + ), _buildInfoContainer(textTheme), ], ), @@ -53,18 +57,16 @@ class _OfferCardState extends State { height: 140, width: double.infinity, fit: BoxFit.cover, - placeholder: - (context, url) => Container( - height: 140, - color: Colors.grey[300], - child: const Center(child: CircularProgressIndicator()), - ), - errorWidget: - (context, url, error) => Container( - height: 140, - color: Colors.grey[300], - child: const Icon(Icons.broken_image, color: Colors.grey), - ), + placeholder: (context, url) => Container( + height: 140, + color: Colors.grey[300], + child: const Center(child: CircularProgressIndicator()), + ), + errorWidget: (context, url, error) => Container( + height: 140, + color: Colors.grey[300], + child: const Icon(Icons.broken_image, color: Colors.grey), + ), ), ); } @@ -80,7 +82,6 @@ class _OfferCardState extends State { ), boxShadow: [ BoxShadow( - // ignore: deprecated_member_use color: Colors.grey.withOpacity(0.2), spreadRadius: 2, blurRadius: 5, @@ -117,7 +118,6 @@ class _OfferCardState extends State { ], ), const SizedBox(height: 10), - Row( children: [ SvgPicture.asset(Assets.icons.location.path), @@ -127,26 +127,25 @@ class _OfferCardState extends State { widget.offer.address, style: textTheme.bodySmall, maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), Text( - '(${widget.offer.distanceInMeters.toString()}متر تا تخفیف)', + '(${widget.offer.distanceInMeters.toString()} متر تا تخفیف)', style: textTheme.bodySmall, ), ], ), - const SizedBox(height: 10), - Row( children: [ SvgPicture.asset(Assets.icons.routing.path), const SizedBox(width: 4), Text( - 'نوع تخفیف : ${widget.offer.discount} ${widget.offer.discountType}', - style: const TextStyle(color: Color.fromARGB(255, 183, 28, 28),fontSize: 14), + 'نوع تخفیف : ${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}% ${widget.offer.discountType}', + style: const TextStyle( + color: Color.fromARGB(255, 183, 28, 28), fontSize: 14), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/presentation/pages/add_photo_screen.dart b/lib/presentation/pages/add_photo_screen.dart index 4cd5a43..5ea248b 100644 --- a/lib/presentation/pages/add_photo_screen.dart +++ b/lib/presentation/pages/add_photo_screen.dart @@ -1,7 +1,10 @@ +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/data/models/offer_model.dart'; @@ -9,7 +12,6 @@ import 'package:proxibuy/features/add_photo/cubit/add_photo_cubit.dart'; import 'package:proxibuy/presentation/pages/reservation_details_screen.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/widgets/flutter_staggered_grid_view.dart'; -import 'package:image_picker/image_picker.dart'; // ✅ ایمپورت جدید class AddPhotoScreen extends StatelessWidget { final String storeName; @@ -23,6 +25,31 @@ class AddPhotoScreen extends StatelessWidget { required this.offer, }); + // متد ساخت توکن + Future _generateQrToken(BuildContext context) async { + const storage = FlutterSecureStorage(); + final userID = await storage.read(key: 'userID'); + + if (userID == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("خطا: کاربر شناسایی نشد.")), + ); + throw Exception("User ID not found"); + } + + final payload = { + 'userID': userID, + 'discountID': offer.id, + 'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + + const secretKey = 'your_super_secret_key_for_qr'; + final jwt = JWT(payload); + final token = jwt.sign(SecretKey(secretKey)); + + return token; + } + void _showImageSourceActionSheet(BuildContext context) { showModalBottomSheet( context: context, @@ -34,7 +61,8 @@ class AddPhotoScreen extends StatelessWidget { child: Wrap( children: [ ListTile( - leading: const Icon(Icons.photo_library, color: AppColors.primary), + leading: + const Icon(Icons.photo_library, color: AppColors.primary), title: const Text('انتخاب از گالری'), onTap: () { Navigator.of(bottomSheetContext).pop(); @@ -42,7 +70,8 @@ class AddPhotoScreen extends StatelessWidget { }, ), ListTile( - leading: const Icon(Icons.camera_alt, color: AppColors.primary), + leading: + const Icon(Icons.camera_alt, color: AppColors.primary), title: const Text('گرفتن عکس با دوربین'), onTap: () { Navigator.of(bottomSheetContext).pop(); @@ -68,7 +97,9 @@ class AddPhotoScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox(height: 10,), + SizedBox( + height: 10, + ), _buildHeader() .animate() .fadeIn(duration: 500.ms) @@ -185,7 +216,6 @@ class AddPhotoScreen extends StatelessWidget { ), boxShadow: [ BoxShadow( - // ignore: deprecated_member_use color: Colors.black.withOpacity(0.08), blurRadius: 10, offset: const Offset(0, 4), @@ -288,18 +318,27 @@ class AddPhotoScreen extends StatelessWidget { ), ), onPressed: () async { - context.read().reserveProduct( - productId, - ); - Navigator.of(dialogContext).pop(); - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ReservationConfirmationPage( - offer: offer, + try { + final qrToken = await _generateQrToken(context); + + context + .read() + .reserveProduct(productId); + + Navigator.of(dialogContext).pop(); + + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ReservationConfirmationPage( + offer: offer, + qrCodeData: qrToken, + ), ), - ), - ); + ); + } catch (e) { + print("Error in reservation popup: $e"); + } }, child: const Text( "رزرو محصول", @@ -318,7 +357,6 @@ class AddPhotoScreen extends StatelessWidget { shape: BoxShape.circle, boxShadow: [ BoxShadow( - // ignore: deprecated_member_use color: Colors.black.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), diff --git a/lib/presentation/pages/login_page.dart b/lib/presentation/pages/login_page.dart index abf1a80..f131f0e 100644 --- a/lib/presentation/pages/login_page.dart +++ b/lib/presentation/pages/login_page.dart @@ -1,6 +1,9 @@ +// lib/presentation/pages/login_page.dart + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:country_picker/country_picker.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/pages/otp_page.dart'; import '../../core/gen/assets.gen.dart'; @@ -18,13 +21,12 @@ class _LoginPageState extends State { void _sendOtp() { context.read().add( - SendOTPEvent( - phoneNumber: _phoneController.text, - countryCode: _selectedCountry.phoneCode, - ), - ); + SendOTPEvent( + phoneNumber: _phoneController.text, + countryCode: _selectedCountry.phoneCode, + ), + ); } - // bool _keepSignedIn = false; @override void dispose() { @@ -46,7 +48,10 @@ class _LoginPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center(child: Assets.icons.logo.svg(height: 160)), + Hero( + tag: 'auth_logo', + child: Center(child: Assets.icons.logo.svg(height: 160)), + ), const SizedBox(height: 48), Text( "ورود", @@ -97,19 +102,6 @@ class _LoginPageState extends State { ), ), ), - const SizedBox(height: 16), - // Row( - // children: [ - // Checkbox( - // value: _keepSignedIn, - // onChanged: - // (value) => - // setState(() => _keepSignedIn = value ?? false), - // activeColor: AppColors.primary, - // ), - // Text("مرا به خاطر بسپار", style: textTheme.bodyMedium), - // ], - // ), const SizedBox(height: 24), BlocConsumer( listener: (context, state) { @@ -122,18 +114,20 @@ class _LoginPageState extends State { ); } if (state is AuthCodeSentSuccess) { + // ******** شروع تغییر ۱ ******** + // BlocProvider.value حذف شد Navigator.push( context, MaterialPageRoute( - builder: - (_) => OtpPage( - phoneNumber: - "${state.countryCode}${state.phone}", - phone: state.phone, - countryCode: state.countryCode, - ), + builder: (_) => OtpPage( + phoneNumber: + "${state.countryCode}${state.phone}", + phone: state.phone, + countryCode: state.countryCode, + ), ), ); + // ******** پایان تغییر ۱ ******** } }, builder: (context, state) { @@ -143,17 +137,16 @@ class _LoginPageState extends State { width: double.infinity, child: ElevatedButton( onPressed: isLoading ? null : _sendOtp, - child: - isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 3, - ), - ) - : const Text("کد یکبار مصرف"), + child: isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3, + ), + ) + : const Text("کد یکبار مصرف"), ), ); }, @@ -183,12 +176,13 @@ class _LoginPageState extends State { "ورود با حساب گوگل", style: TextStyle(color: Colors.black), ), - onPressed: () { - // TODO: Implement Google Sign-in - }, + onPressed: () {}, ), ), - ], + ] + .animate(interval: 100.ms) + .fadeIn(duration: 400.ms, curve: Curves.easeOut) + .slideY(begin: 0.2, duration: 400.ms, curve: Curves.easeOut), ), ), ), @@ -214,4 +208,4 @@ class _LoginPageState extends State { onSelect: (Country country) => setState(() => _selectedCountry = country), ); } -} +} \ No newline at end of file diff --git a/lib/presentation/pages/notification_preferences_page.dart b/lib/presentation/pages/notification_preferences_page.dart index 8f53888..210712c 100644 --- a/lib/presentation/pages/notification_preferences_page.dart +++ b/lib/presentation/pages/notification_preferences_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; @@ -9,40 +10,52 @@ import 'package:proxibuy/presentation/pages/offers_page.dart'; import 'package:proxibuy/presentation/pages/reserved_list_page.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/widgets/category_selection_card.dart'; +import 'package:proxibuy/services/mqtt_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class NotificationPreferencesPage extends StatefulWidget { - // This parameter is used to decide whether to fetch favorite categories on start final bool loadFavoritesOnStart; - // The constructor now accepts the 'loadFavoritesOnStart' parameter - const NotificationPreferencesPage({super.key, this.loadFavoritesOnStart = false}); - - static Route route({bool loadFavorites = false}) { - return MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => NotificationPreferencesBloc(), - // The widget is created here, passing the parameter correctly - child: NotificationPreferencesPage(loadFavoritesOnStart: loadFavorites), - ), - ); - } + const NotificationPreferencesPage( + {super.key, this.loadFavoritesOnStart = false}); @override State createState() => _NotificationPreferencesPageState(); } -class _NotificationPreferencesPageState extends State { +class _NotificationPreferencesPageState + extends State { @override void initState() { super.initState(); - // If the flag is true, dispatch the event to load favorites from the API if (widget.loadFavoritesOnStart) { context.read().add(LoadFavoriteCategories()); } } + void _showConnectingDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return const Dialog( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(width: 20), + Text("در حال اتصال..."), + ], + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -117,19 +130,41 @@ class _NotificationPreferencesPageState extends State( - listener: (context, state) { + body: BlocListener( + listener: (context, state) async { if (state.submissionSuccess) { - // Pop the page and return 'true' to signal a successful update if (Navigator.canPop(context)) { Navigator.of(context).pop(true); } else { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => const OffersPage(showDialogsOnLoad: true), - ), - ); + _showConnectingDialog(context); + + try { + final mqttService = context.read(); + const storage = FlutterSecureStorage(); + final token = await storage.read(key: 'accessToken'); + + if (token != null && token.isNotEmpty) { + await mqttService.connect(token); + + if (mounted && mqttService.isConnected) { + Navigator.of(context).pop(); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => OffersPage(), + ), + ); + } + } else { + if (mounted) Navigator.of(context).pop(); + } + } catch (e) { + if (mounted) Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("خطا در اتصال: $e")), + ); + } } } if (state.errorMessage != null) { @@ -168,7 +203,8 @@ class _NotificationPreferencesPageState extends State( + child: BlocBuilder( builder: (context, state) { if (state.categories.isEmpty && state.isLoading) { return const Center(child: CircularProgressIndicator()); @@ -186,7 +223,10 @@ class _NotificationPreferencesPageState extends State() - .add(ToggleCategorySelection(category.id)); + context.read().add( + ToggleCategorySelection(category.id)); }, ), ); @@ -218,9 +258,11 @@ class _NotificationPreferencesPageState extends State( + BlocBuilder( builder: (context, state) { - final areCategoriesSelected = state.selectedCategoryIds.isNotEmpty; + final areCategoriesSelected = + state.selectedCategoryIds.isNotEmpty; if (areCategoriesSelected) { return SizedBox( @@ -228,16 +270,23 @@ class _NotificationPreferencesPageState extends State(); - - final selectedCategoryNames = bloc.state.categories - .where((cat) => bloc.state.selectedCategoryIds.contains(cat.id)) + final bloc = + context.read(); + + final selectedCategoryNames = bloc + .state.categories + .where((cat) => bloc.state + .selectedCategoryIds + .contains(cat.id)) .map((cat) => cat.name) .toList(); - - final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList('user_selected_categories', selectedCategoryNames); - + + final prefs = + await SharedPreferences.getInstance(); + await prefs.setStringList( + 'user_selected_categories', + selectedCategoryNames); + bloc.add(SubmitPreferences()); } : null, @@ -251,7 +300,8 @@ class _NotificationPreferencesPageState extends State { List _selectedCategories = []; StreamSubscription? _locationServiceSubscription; StreamSubscription? _mqttMessageSubscription; + StreamSubscription? _connectivitySubscription; Timer? _locationTimer; bool _isSubscribedToOffers = false; bool _isGpsEnabled = false; @@ -43,19 +46,99 @@ class _OffersPageState extends State { void initState() { super.initState(); _initializePage(); + _initConnectivityListener(); + _fetchInitialReservations(); // <-- فراخوانی متد جدید } + // ******** شروع متد جدید ******** + // این متد تعداد اولیه رزروها را برای نمایش روی آیکون دریافت می‌کند + Future _fetchInitialReservations() async { + try { + const storage = FlutterSecureStorage(); + final token = await storage.read(key: 'accessToken'); + if (token == null) return; // کاربر لاگین نکرده است + + final dio = Dio(); + final response = await dio.get( + ApiConfig.baseUrl + ApiConfig.getReservations, + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); + + if (response.statusCode == 200 && mounted) { + final List reserves = response.data['reserves']; + final List reservedIds = + reserves + .map( + (reserveData) => + (reserveData['Discount']['ID'] as String?) ?? '', + ) + .where((id) => id.isNotEmpty) + .toList(); + + // به‌روزرسانی Cubit با لیست شناسه‌ها + context.read().setReservedIds(reservedIds); + } + } catch (e) { + // اگر خطایی رخ دهد، مشکلی نیست. فقط عدد نمایش داده نمی‌شود + debugPrint("Error fetching initial reservations: $e"); + } + } + // ******** پایان متد جدید ******** + Future _initializePage() async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + await showNotificationPermissionDialog(context); + await showGPSDialog(context); + } + }); + await _loadPreferences(); - _subscribeToUserOffersOnLoad(); + _checkAndConnectMqtt(); _initLocationListener(); } + void _initConnectivityListener() { + _connectivitySubscription = Connectivity().onConnectivityChanged.listen(( + List results, + ) { + if (!results.contains(ConnectivityResult.none)) { + print("ℹ️ Network connection restored."); + _checkAndConnectMqtt(); + } else { + print("ℹ️ Network connection lost."); + } + }); + } + + Future _checkAndConnectMqtt() async { + final mqttService = context.read(); + if (!mqttService.isConnected) { + print("--- OffersPage: MQTT not connected. Attempting to connect..."); + final storage = const FlutterSecureStorage(); + final token = await storage.read(key: 'accessToken'); + if (token != null && token.isNotEmpty) { + try { + await mqttService.connect(token); + if (mqttService.isConnected && mounted) { + _subscribeToUserOffersOnLoad(); + } + } catch (e) { + print("❌ OffersPage: Error connecting to MQTT: $e"); + } + } + } else { + print("--- OffersPage: MQTT already connected."); + _subscribeToUserOffersOnLoad(); + } + } + @override void dispose() { _locationServiceSubscription?.cancel(); _mqttMessageSubscription?.cancel(); _locationTimer?.cancel(); + _connectivitySubscription?.cancel(); super.dispose(); } @@ -69,8 +152,9 @@ class _OffersPageState extends State { void _initLocationListener() { _checkInitialGpsStatus(); - _locationServiceSubscription = - Geolocator.getServiceStatusStream().listen((status) { + _locationServiceSubscription = Geolocator.getServiceStatusStream().listen(( + status, + ) { final isEnabled = status == ServiceStatus.enabled; if (mounted && _isGpsEnabled != isEnabled) { setState(() { @@ -87,7 +171,7 @@ class _OffersPageState extends State { } }); } - + Future _checkInitialGpsStatus() async { final status = await Geolocator.isLocationServiceEnabled(); if (mounted) { @@ -103,7 +187,7 @@ class _OffersPageState extends State { void _startSendingLocationUpdates() { print("🚀 Starting periodic location updates."); _locationTimer?.cancel(); - _locationTimer = Timer.periodic(const Duration(seconds: 15), (timer) { + _locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) { _sendLocationUpdate(); }); _sendLocationUpdate(); @@ -121,7 +205,7 @@ class _OffersPageState extends State { if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } - + if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { print("🚫 Location permission denied by user."); @@ -140,14 +224,9 @@ class _OffersPageState extends State { return; } - final payload = { - "userID": userID, - "lat": 32.6685, - "lng": 51.6826, - }; + final payload = {"userID": userID, "lat": 32.6685, "lng": 51.6826}; mqttService.publish("proxybuy/sendGps", payload); - } catch (e) { print("❌ Error sending location update in OffersPage: $e"); } @@ -163,13 +242,22 @@ class _OffersPageState extends State { _mqttMessageSubscription = mqttService.messages.listen((message) { final data = message['data']; - if (data != null && data is List) { + + if (data == null) { + if (mounted) { + context.read().add(const OffersReceivedFromMqtt([])); + } + return; + } + + if (data is List) { try { - List offers = data - .whereType>() - .map((json) => OfferModel.fromJson(json)) - .toList(); - + List offers = + data + .whereType>() + .map((json) => OfferModel.fromJson(json)) + .toList(); + if (mounted) { context.read().add(OffersReceivedFromMqtt(offers)); } @@ -209,9 +297,14 @@ class _OffersPageState extends State { TextButton( onPressed: () async { final result = await Navigator.of(context).push( - NotificationPreferencesPage.route(loadFavorites: true), + MaterialPageRoute( + builder: + (context) => const NotificationPreferencesPage( + loadFavoritesOnStart: true, + ), + ), ); - + if (result == true && mounted) { _loadPreferences(); } @@ -348,7 +441,10 @@ class _OffersPageState extends State { body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [_buildFavoriteCategoriesSection(), OffersView(isGpsEnabled: _isGpsEnabled)], + children: [ + _buildFavoriteCategoriesSection(), + OffersView(isGpsEnabled: _isGpsEnabled), + ], ), ), ), @@ -384,7 +480,7 @@ class OffersView extends StatelessWidget { ), ); } - + if (state is OffersLoadSuccess) { if (state.offers.isEmpty) { return const SizedBox( @@ -426,14 +522,14 @@ class OffersView extends StatelessWidget { }, ); } - + if (state is OffersLoadFailure) { return SizedBox( height: 200, child: Center(child: Text("خطا در بارگذاری: ${state.error}")), ); } - + return const SizedBox.shrink(); }, ); @@ -482,4 +578,4 @@ class OffersView extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/pages/onboarding_page.dart b/lib/presentation/pages/onboarding_page.dart index 80fbbd9..e88bda7 100644 --- a/lib/presentation/pages/onboarding_page.dart +++ b/lib/presentation/pages/onboarding_page.dart @@ -23,12 +23,14 @@ final List onboardingPages = [ OnboardingEntity( imagePath: Assets.images.onboarding3.path, title: ' پیشنهادهای لحظه‌ای، تجربه‌ای نو از خرید', - description: 'Proxibuy همیشه باهاته؛ چه در مسیر رفتن به خونه، چه در مسیر کافه مورد علاقه‌ات. همین الان خریدت رو هوشمندتر کن!', + description: + 'Proxibuy همیشه باهاته؛ چه در مسیر رفتن به خونه، چه در مسیر کافه مورد علاقه‌ات. همین الان خریدت رو هوشمندتر کن!', ), OnboardingEntity( imagePath: Assets.images.onboarding4.path, title: 'دنیایی از تخفیف‌های اطرافت رو کشف کن', - description: 'می‌صرفه یه اپ هوشمنده که کمکت می‌کنه وقتی توی شهر حرکت می‌کنی، از تخفیف‌های اطرافت باخبر شی و به صرفه‌تر خرید کنی.', + description: + 'می‌صرفه یه اپ هوشمنده که کمکت می‌کنه وقتی توی شهر حرکت می‌کنی، از تخفیف‌های اطرافت باخبر شی و به صرفه‌تر خرید کنی.', ), ]; @@ -69,13 +71,15 @@ class _OnboardingPageState extends State { ); } else { Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => BlocProvider( - create: (context) => AuthBloc(), - child: const LoginPage(), + MaterialPageRoute( + builder: + (context) => BlocProvider( + // This creates a NEW AuthBloc + create: (context) => AuthBloc(), + child: const LoginPage(), + ), ), - ), - ); + ); } } @@ -120,42 +124,43 @@ class _OnboardingPageState extends State { left: 0, right: 0, child: SizedBox( - width: 80, - height: 80, - child: Stack( - alignment: Alignment.center, + width: 80, + height: 80, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 80, + height: 80, + child: CustomPaint( + painter: OnboardingIndicatorPainter( + pageCount: onboardingPages.length, + currentPage: _currentPage, + activeColor: AppColors.primary, + inactiveColor: AppColors.unselected, + ), + ), + ), + Stack( children: [ - SizedBox( - width: 80, - height: 80, - child: CustomPaint( - painter: OnboardingIndicatorPainter( - pageCount: onboardingPages.length, - currentPage: _currentPage, - activeColor: AppColors.primary, - inactiveColor: AppColors.unselected, - ), + Center( + child: GestureDetector( + onTap: _onArrowTap, + child: Assets.icons.ellipse1.svg(width: 60), ), ), - Stack( - children: [ - Center( - child: GestureDetector( - onTap: _onArrowTap, - child: Assets.icons.ellipse1.svg(width: 60), - ), - ), - Center( - child: GestureDetector( - onTap: _onArrowTap, - child: Assets.icons.arrowRight2.svg(width: 45), - ), - ), - ], + Center( + child: GestureDetector( + onTap: _onArrowTap, + child: Assets.icons.arrowRight2.svg(width: 45), + ), ), ], ), - ),), + ], + ), + ), + ), Positioned( bottom: 90, left: 24, @@ -173,8 +178,7 @@ class _OnboardingPageState extends State { ).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, fontSize: 18, - color: - Colors.white + color: Colors.white, ), ), const SizedBox(height: 16), diff --git a/lib/presentation/pages/otp_page.dart b/lib/presentation/pages/otp_page.dart index 37013cb..54ffbba 100644 --- a/lib/presentation/pages/otp_page.dart +++ b/lib/presentation/pages/otp_page.dart @@ -1,19 +1,13 @@ -// lib/presentation/pages/otp_page.dart - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -// import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; // حذف شد -// import 'package:proxibuy/data/repositories/offer_repository.dart'; // حذف شد import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; -import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; -import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; import 'package:proxibuy/presentation/pages/user_info_page.dart'; -import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import '../../core/config/app_colors.dart'; import '../../core/gen/assets.gen.dart'; import '../utils/otp_timer_helper.dart'; + class OtpPage extends StatefulWidget { final String phoneNumber; final String phone; @@ -154,33 +148,13 @@ class _OtpPageState extends State { }); } if (state is AuthNeedsInfo) { - // **تغییر اصلی در این قسمت است** - // دیگر نیازی به ساخت OfferRepository نیست + // ******** شروع تغییر ۲ ******** + // BlocProvider.value حذف شد Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: - (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - // OffersBloc دیگر به ریپازیتوری نیاز ندارد - BlocProvider( - create: (_) => OffersBloc(), - ), - BlocProvider( - create: (_) => ReservationCubit(), - ), - BlocProvider( - create: - (_) => NotificationPreferencesBloc(), - ), - ], - child: const UserInfoPage(), - ), - ), + MaterialPageRoute(builder: (_) => const UserInfoPage()), (route) => false, ); + // ******** پایان تغییر ۲ ******** } }, builder: (context, state) { @@ -342,4 +316,4 @@ class _OtpPageState extends State { ); _otpTimer.resetTimer(); } -} \ No newline at end of file +} diff --git a/lib/presentation/pages/product_detail_page.dart b/lib/presentation/pages/product_detail_page.dart index 312e127..fedea5c 100644 --- a/lib/presentation/pages/product_detail_page.dart +++ b/lib/presentation/pages/product_detail_page.dart @@ -1,17 +1,18 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:maps_launcher/maps_launcher.dart'; +import 'package:proxibuy/core/config/api_config.dart'; import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/data/models/offer_model.dart'; -import 'package:proxibuy/data/repositories/offer_repository.dart'; import 'package:proxibuy/presentation/pages/add_photo_screen.dart'; import 'package:proxibuy/presentation/pages/reservation_details_screen.dart'; -import 'package:proxibuy/presentation/product_detail/bloc/product_detail_bloc.dart'; -import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart'; -import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/widgets/comments_section.dart'; import 'package:slide_countdown/slide_countdown.dart'; @@ -21,34 +22,138 @@ class ProductDetailPage extends StatelessWidget { const ProductDetailPage({super.key, required this.offer}); + Future _generateQrToken(BuildContext context) async { + const storage = FlutterSecureStorage(); + final userID = await storage.read(key: 'userID'); + + if (userID == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("خطا: کاربر شناسایی نشد.")), + ); + throw Exception("User ID not found"); + } + + final payload = { + 'userID': userID, + 'discountID': offer.id, + 'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + + const secretKey = 'your_super_secret_key_for_qr'; + final jwt = JWT(payload); + final token = jwt.sign(SecretKey(secretKey)); + + return token; + } + @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.white, body: Stack( children: [ - // ویجت نمایش جزئیات مستقیما ساخته می‌شود - ProductDetailView(offer: offer) - .animate() - .fadeIn(duration: 400.ms, curve: Curves.easeOut) - .slideY( - begin: 0.2, - duration: 400.ms, - curve: Curves.easeOut, - ), + ProductDetailView(offer: offer), Positioned( bottom: 30, left: 24, right: 24, - // BlocBuilder حذف شد child: ElevatedButton( - onPressed: () { - context.read().reserveProduct(offer.id); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => - ReservationConfirmationPage(offer: offer), - ), + onPressed: () async { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => + const Center(child: CircularProgressIndicator()), ); + + try { + const storage = FlutterSecureStorage(); + final token = await storage.read(key: 'accessToken'); + + if (token == null) { + throw Exception("شما وارد حساب کاربری خود نشده‌اید."); + } + + final dio = Dio(); + final url = ApiConfig.baseUrl + ApiConfig.addReservation; + + final data = { + 'Discount': offer.id, + 'Distance': offer.distanceInMeters.toString(), + }; + + final options = Options( + headers: {'Authorization': 'Bearer $token'}, + ); + + debugPrint("----------- REQUEST-----------"); + debugPrint("URL: POST $url"); + debugPrint("Headers: ${options.headers}"); + debugPrint("Body: $data"); + debugPrint("-----------------------------"); + + final response = + await dio.post(url, data: data, options: options); + + debugPrint("---------- RESPONSE-----------"); + debugPrint("StatusCode: ${response.statusCode}"); + debugPrint("Data: ${response.data}"); + debugPrint("-----------------------------"); + + if (context.mounted) Navigator.of(context).pop(); + + if (response.statusCode == 200) { + final qrToken = await _generateQrToken(context); + context.read().reserveProduct(offer.id); + + if (context.mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ReservationConfirmationPage( + offer: offer, + qrCodeData: qrToken, + ), + ), + ); + } + } else { + final errorMessage = + response.data['message'] ?? 'خطا در رزرو تخفیف'; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(errorMessage), + backgroundColor: Colors.red), + ); + } + } + } on DioException catch (e) { + if (context.mounted) Navigator.of(context).pop(); + + debugPrint("---------- ERROR-----------"); + debugPrint("StatusCode: ${e.response?.statusCode}"); + debugPrint("Data: ${e.response?.data}"); + debugPrint("--------------------------"); + + final errorMessage = e.response?.data?['message'] ?? + 'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(errorMessage), + backgroundColor: Colors.red), + ); + } catch (e) { + if (context.mounted) Navigator.of(context).pop(); + debugPrint("---------- GENERAL ERROR -----------"); + debugPrint(e.toString()); + debugPrint("------------------------------------"); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + backgroundColor: Colors.red), + ); + } }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.confirm, @@ -73,7 +178,12 @@ class ProductDetailPage extends StatelessWidget { ), ], ), - ).animate().fadeIn(delay: 200.ms).slideY(begin: 2), + ).animate().slideY( + begin: 2, + delay: 400.ms, + duration: 600.ms, + curve: Curves.easeOutCubic, + ), ), ], ), @@ -81,6 +191,7 @@ class ProductDetailPage extends StatelessWidget { } } +// ... بقیه کدهای این فایل بدون تغییر باقی می‌ماند ... class ProductDetailView extends StatefulWidget { final OfferModel offer; @@ -120,9 +231,9 @@ class _ProductDetailViewState extends State { _buildMainImage(context), const SizedBox(height: 52.5), _buildProductInfo(), - const SizedBox(height: 100), + const SizedBox(height: 120), ], - ), + ).animate().fadeIn(duration: 600.ms, curve: Curves.easeOut), Positioned( top: 400 - 52.5, left: 0, @@ -138,38 +249,39 @@ class _ProductDetailViewState extends State { Widget _buildMainImage(BuildContext context) { return Stack( children: [ - SizedBox( - height: 400, - width: double.infinity, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - return FadeTransition(opacity: animation, child: child); - }, - child: Image.network( - selectedImage, - key: ValueKey(selectedImage), - fit: BoxFit.cover, - width: double.infinity, - height: 400, - loadingBuilder: (context, child, progress) { - return progress == null - ? child - : const Center(child: CircularProgressIndicator()); + Hero( + tag: 'offer_image_${widget.offer.id}', + child: SizedBox( + height: 400, + width: double.infinity, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); }, + child: CachedNetworkImage( + imageUrl: selectedImage, + key: ValueKey(selectedImage), + fit: BoxFit.cover, + width: double.infinity, + height: 400, + placeholder: (context, url) => + const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => + const Center(child: Icon(Icons.error)), + ), ), ), ), Positioned( top: 40, left: 16, - child: - GestureDetector( - child: SvgPicture.asset(Assets.icons.back.path), - onTap: () { - Navigator.pop(context); - }, - ).animate().fade(delay: 100.ms).scale(), + child: GestureDetector( + child: SvgPicture.asset(Assets.icons.back.path), + onTap: () { + Navigator.pop(context); + }, + ).animate().fade(delay: 200.ms).scale(), ), ], ); @@ -189,9 +301,10 @@ class _ProductDetailViewState extends State { itemBuilder: (context, index) { final img = imageList[index]; if (img == _uploadKey) { - return _buildUploadButton().animate().fade().scale( - delay: (index * 50).ms, - ); + return _buildUploadButton() + .animate() + .fade(delay: (index * 80).ms) + .scale(delay: (index * 80).ms); } final isSelected = selectedImage == img; return _buildThumbnail(img, isSelected, index); @@ -203,32 +316,19 @@ class _ProductDetailViewState extends State { Widget _buildThumbnail(String img, bool isSelected, int index) { const grayscaleMatrix = [ - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0, - 0, - 0, - 1, - 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, ]; return GestureDetector( onTap: () => setState(() => selectedImage = img), child: AnimatedContainer( duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + transform: Matrix4.identity()..scale(isSelected ? 1.0 : 0.9), + transformAlignment: Alignment.center, decoration: BoxDecoration( border: Border.all( color: isSelected ? AppColors.selectedImg : Colors.transparent, @@ -237,7 +337,6 @@ class _ProductDetailViewState extends State { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - // ignore: deprecated_member_use color: Colors.black.withOpacity(0.15), blurRadius: 7, spreadRadius: 0, @@ -250,46 +349,40 @@ class _ProductDetailViewState extends State { colorFilter: ColorFilter.matrix( isSelected ? [ - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ] + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + ] : grayscaleMatrix, ), - child: Image.network(img, width: 90, height: 90, fit: BoxFit.cover), + child: CachedNetworkImage( + imageUrl: img, + width: 90, + height: 90, + fit: BoxFit.cover, + placeholder: (context, url) => Container(color: Colors.grey[200]), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), ), ), ), - ).animate().fade().scale(delay: (index * 50).ms); + ).animate().fade().scale(delay: (index * 80).ms); } Widget _buildUploadButton() { return GestureDetector( onTap: () { Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AddPhotoScreen(storeName: widget.offer.storeName, productId: widget.offer.id, offer: widget.offer,), - ), - ); - + context, + MaterialPageRoute( + builder: (context) => AddPhotoScreen( + storeName: widget.offer.storeName, + productId: widget.offer.id, + offer: widget.offer, + ), + ), + ); }, child: Container( width: 90, @@ -308,47 +401,51 @@ class _ProductDetailViewState extends State { } Widget _buildProductInfo() { - final remainingDuration = - widget.offer.expiryTime.isAfter(DateTime.now()) - ? widget.offer.expiryTime.difference(DateTime.now()) - : Duration.zero; + final remainingDuration = widget.offer.expiryTime.isAfter(DateTime.now()) + ? widget.offer.expiryTime.difference(DateTime.now()) + : Duration.zero; - final animationList = [ - Row( - children: [ - SvgPicture.asset(Assets.icons.shop.path, height: 30), - const SizedBox(width: 6), - Expanded( - child: Text( - widget.offer.storeName, - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0) + .copyWith(bottom: 5.0, top: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset(Assets.icons.shop.path, height: 30), + const SizedBox(width: 6), + Expanded( + child: Text( + widget.offer.storeName, + style: const TextStyle( + fontSize: 24, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), - ], - ), - const SizedBox(height: 16), - _buildInfoRow(icon: Assets.icons.location, text: widget.offer.address), - const SizedBox(height: 12), - ExpandableInfoRow( - icon: Assets.icons.clock, - titleWidget: Row( - children: [ - Text( - widget.offer.isOpen ? "باز است" : "بسته است", - style: TextStyle( - fontSize: 16, - color: - widget.offer.isOpen + const SizedBox(height: 16), + _buildInfoRow( + icon: Assets.icons.location, text: widget.offer.address), + const SizedBox(height: 12), + ExpandableInfoRow( + icon: Assets.icons.clock, + titleWidget: Row( + children: [ + Text( + widget.offer.isOpen ? "باز است" : "بسته است", + style: TextStyle( + fontSize: 16, + color: widget.offer.isOpen ? Colors.green.shade700 : Colors.red.shade700, - fontWeight: FontWeight.normal, - ), + fontWeight: FontWeight.normal, + ), + ), + ], ), - ], - ), - children: - widget.offer.workingHours + children: widget.offer.workingHours .map( (wh) => Padding( padding: const EdgeInsets.only( @@ -368,235 +465,228 @@ class _ProductDetailViewState extends State { ), wh.isOpen ? Text( - wh.shifts - .map((s) => '${s.openAt} - ${s.closeAt}') - .join(' | '), - style: const TextStyle( - fontSize: 15, - color: AppColors.hint, - ), - ) + wh.shifts + .map((s) => '${s.openAt} - ${s.closeAt}') + .join(' | '), + style: const TextStyle( + fontSize: 15, + color: AppColors.hint, + ), + ) : const Text( - 'تعطیل', - style: TextStyle(fontSize: 15, color: Colors.red), - ), + 'تعطیل', + style: + TextStyle(fontSize: 15, color: Colors.red), + ), ], ), ), ) .toList(), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - InkWell( - onTap: - () => _launchMaps( + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () => _launchMaps( widget.offer.latitude, widget.offer.longitude, widget.offer.storeName, ), - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Row( + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + children: [ + SvgPicture.asset( + Assets.icons.map.path, + width: 25, + height: 25, + colorFilter: const ColorFilter.mode( + AppColors.button, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 8), + const Text( + 'مسیر فروشگاه روی نقشه', + style: + TextStyle(fontSize: 17, color: AppColors.button), + ), + ], + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - SvgPicture.asset( - Assets.icons.map.path, - width: 25, - height: 25, - colorFilter: const ColorFilter.mode( - AppColors.button, - BlendMode.srcIn, + Container( + width: 60, + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: const BoxDecoration( + color: AppColors.selectedImg, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.offer.rating.toString(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), + const SizedBox(width: 2), + SvgPicture.asset( + Assets.icons.star.path, + colorFilter: const ColorFilter.mode( + Colors.white, + BlendMode.srcIn, + ), + ), + ], ), ), - const SizedBox(width: 8), - const Text( - 'مسیر فروشگاه روی نقشه', - style: TextStyle(fontSize: 17, color: AppColors.button), + Container( + height: 30, + width: 60, + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + child: Center( + child: Text( + '${widget.offer.ratingCount} نفر', + style: const TextStyle( + color: Colors.black, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ), ), ], ), - ), + ], ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( - width: 60, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: const BoxDecoration( - color: AppColors.selectedImg, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 11), + decoration: BoxDecoration( + color: AppColors.singleOfferType, + borderRadius: BorderRadius.circular(20), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.offer.rating.toString(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 11, - ), - ), - const SizedBox(width: 2), - SvgPicture.asset( - Assets.icons.star.path, - colorFilter: const ColorFilter.mode( - Colors.white, - BlendMode.srcIn, - ), - ), - ], + child: Text( + "تخفیف ${widget.offer.discountType}", + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 17, + ), ), ), - Container( - height: 30, - width: 60, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(8), - bottomRight: Radius.circular(8), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)', + style: const TextStyle( + fontSize: 16, + color: AppColors.singleOfferType, + fontWeight: FontWeight.normal, + ), + ), + const SizedBox(width: 8), + Text( + widget.offer.originalPrice.toStringAsFixed(0), + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + decoration: TextDecoration.lineThrough, + ), + ), + ], ), - ), - child: Center( - child: Text( - '${widget.offer.ratingCount} نفر', + const SizedBox(height: 1), + Text( + '${widget.offer.finalPrice.toStringAsFixed(0)} تومان', style: const TextStyle( - color: Colors.black, - fontSize: 11, + color: AppColors.singleOfferType, + fontSize: 22, fontWeight: FontWeight.bold, ), ), - ), - ), - ], - ), - ], - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11), - decoration: BoxDecoration( - color: AppColors.singleOfferType, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - "تخفیف ${widget.offer.discountType}", - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 17, - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - '(${widget.offer.discount})', - style: const TextStyle( - fontSize: 16, - color: AppColors.singleOfferType, - fontWeight: FontWeight.normal, - ), - ), - const SizedBox(width: 8), - Text( - widget.offer.originalPrice.toStringAsFixed(0), - style: TextStyle( - fontSize: 16, - color: Colors.grey.shade600, - decoration: TextDecoration.lineThrough, - ), - ), ], ), - const SizedBox(height: 1), - Text( - '${widget.offer.finalPrice.toStringAsFixed(0)} تومان', - style: const TextStyle( - color: AppColors.singleOfferType, - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), ], ), - ], - ), - const SizedBox(height: 24), - _buildInfoRow( - icon: Assets.icons.timerPause, - text: "مهلت استفاده از تخفیف", - ), - const SizedBox(height: 10), - if (remainingDuration > Duration.zero) - Column( - children: [ - Localizations.override( - context: context, - locale: const Locale('en'), - child: SlideCountdown( - duration: remainingDuration, - slideDirection: SlideDirection.up, - separator: ':', - style: const TextStyle( - fontSize: 70, - fontWeight: FontWeight.bold, - color: AppColors.countdown, + const SizedBox(height: 24), + _buildInfoRow( + icon: Assets.icons.timerPause, + text: "مهلت استفاده از تخفیف", + ), + const SizedBox(height: 10), + if (remainingDuration > Duration.zero) + Column( + children: [ + Localizations.override( + context: context, + locale: const Locale('en'), + child: SlideCountdown( + duration: remainingDuration, + slideDirection: SlideDirection.up, + separator: ':', + style: const TextStyle( + fontSize: 50, + fontWeight: FontWeight.bold, + color: AppColors.countdown, + ), + separatorStyle: const TextStyle( + fontSize: 40, + color: AppColors.countdown, + ), + decoration: const BoxDecoration(color: Colors.white), + shouldShowDays: (d) => d.inDays > 0, + shouldShowHours: (d) => d.inHours > 0, + shouldShowMinutes: (d) => d.inSeconds > 0, + ), ), - separatorStyle: const TextStyle( - fontSize: 40, - color: AppColors.countdown, - ), - decoration: const BoxDecoration(color: Colors.white), - shouldShowDays: (d) => d.inDays > 0, - shouldShowHours: (d) => d.inHours > 0, - shouldShowMinutes: - (d) => - d.inSeconds > - 0, - ), + const SizedBox(height: 4), + _buildTimerLabels(remainingDuration), + ], + ), + const SizedBox(height: 24), + _buildFeaturesSection(), + const SizedBox(height: 24), + _buildDiscountTypeSection(), + const SizedBox(height: 24), + CommentsSection(comments: widget.offer.comments), + ].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn( + duration: 400.ms, + curve: Curves.easeOut, ), - const SizedBox(height: 4), - _buildTimerLabels(remainingDuration), - ], - ), - const SizedBox(height: 24), - _buildFeaturesSection(), - const SizedBox(height: 24), - _buildDiscountTypeSection(), - const SizedBox(height: 24), - CommentsSection(comments: widget.offer.comments), - ].animate(interval: 80.ms).fade(duration: 300.ms).slideX(begin: -0.1); - - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 24.0, - ).copyWith(bottom: 24.0, top: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: animationList - .animate(interval: 80.ms) - .fade(duration: 300.ms) - .slideX(begin: -0.1), ), ); } @@ -684,7 +774,7 @@ class _ProductDetailViewState extends State { width: 22, height: 22, colorFilter: const ColorFilter.mode( - AppColors.confirm, + AppColors.hint, BlendMode.srcIn, ), ), @@ -741,7 +831,7 @@ class _ProductDetailViewState extends State { const SizedBox(width: 10), Expanded( child: Text( - "${widget.offer.discount} تخفیف ${info.name}", + '${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}% تخفیف ${widget.offer.discountType}', style: const TextStyle( fontSize: 16, height: 1.4, @@ -760,12 +850,12 @@ class _ProductDetailViewState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Center( - child: SvgPicture.asset(Assets.icons.warning2.path, height: 24), - ), + child: + SvgPicture.asset(Assets.icons.warning2.path, height: 24)), const SizedBox(width: 10), Expanded( child: Text( - info.description, + widget.offer.discountInfo, style: const TextStyle(fontSize: 14, color: AppColors.hint), ), ), @@ -858,13 +948,12 @@ class _ExpandableInfoRowState extends State { AnimatedCrossFade( firstChild: Container(), secondChild: Column(children: widget.children), - crossFadeState: - _isExpanded - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: _isExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, duration: const Duration(milliseconds: 300), ), ], ); } -} +} \ No newline at end of file diff --git a/lib/presentation/pages/reservation_details_screen.dart b/lib/presentation/pages/reservation_details_screen.dart index 3d0aece..4cd1d3b 100644 --- a/lib/presentation/pages/reservation_details_screen.dart +++ b/lib/presentation/pages/reservation_details_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:audioplayers/audioplayers.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -11,27 +12,48 @@ import 'package:flutter_animate/flutter_animate.dart'; class ReservationConfirmationPage extends StatefulWidget { final OfferModel offer; + final String qrCodeData; - const ReservationConfirmationPage({super.key, required this.offer}); + const ReservationConfirmationPage({ + super.key, + required this.offer, + required this.qrCodeData, + }); @override State createState() => _ReservationConfirmationPageState(); } -class _ReservationConfirmationPageState extends State { +class _ReservationConfirmationPageState + extends State { Timer? _timer; Duration _remaining = Duration.zero; + final AudioPlayer _audioPlayer = AudioPlayer(); @override void initState() { super.initState(); + + _playSound(); + // ******** پایان تغییر اصلی ******** + _calculateRemainingTime(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { _calculateRemainingTime(); }); } + // متد جدید برای پخش صدا + void _playSound() async { + try { + // فایل صدایی که در پوشه assets قرار داده‌اید + await _audioPlayer.play(AssetSource('sounds/positive-notification-alert-351299.mp3')); + } catch (e) { + debugPrint("Error playing sound: $e"); + } + } + void _calculateRemainingTime() { final now = DateTime.now(); if (widget.offer.expiryTime.isAfter(now)) { @@ -53,9 +75,11 @@ class _ReservationConfirmationPageState extends State 0) _buildTimeBlock(days, 'روز'), + if (_remaining.inDays > 0) ...[ + const SizedBox(width: 10), + _buildTimeBlock(days, 'روز'), + ], ], ), - SizedBox(height: 20 ,), - Text("لطفا QR Code زیر رو به فروشنده نشون بده.",style: TextStyle(fontSize: 15),) + const SizedBox(height: 20), + const Text( + "لطفا QR Code زیر رو به فروشنده نشون بده.", + style: TextStyle(fontSize: 15), + ), ], ), ); } Widget _buildTimeBlock(String value, String label) { - return Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 3), - decoration: BoxDecoration( - border: Border.all(color: AppColors.countdownBorderRserve, width: 1.5), - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - value, - style: const TextStyle( - fontSize: 20, - fontFamily: 'Dana', - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - const SizedBox(height: 4), - Text( - label, - style: const TextStyle( - fontSize: 12, - fontFamily: 'Dana', - color: Colors.black, - ), - ), - ], + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 5, + ), + decoration: BoxDecoration( + border: Border.all(color: AppColors.countdownBorderRserve, width: 1.5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value, + style: const TextStyle( + fontSize: 20, // فونت کمی کوچک‌تر شد + fontFamily: 'Dana', + fontWeight: FontWeight.bold, + color: Colors.black, ), ), - ), - ], + const SizedBox(height: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + fontFamily: 'Dana', + color: Colors.black, + ), + ), + ], + ), ); } + Widget _buildQrCodeCard() { return Center( child: Container( width: 500, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Color.fromARGB(255, 246, 246, 246), + color: const Color.fromARGB(255, 246, 246, 246), borderRadius: BorderRadius.circular(16), ), child: Column( @@ -344,16 +393,15 @@ class _ReservationConfirmationPageState extends State { - late final List _reservedIds; - // دیگر نیازی به Future نیست - List _reservedOffers = []; + late Future> _reservedOffersFuture; @override void initState() { super.initState(); - _reservedIds = context.read().state.reservedProductIds; - // اطلاعات مستقیما از BLoC خوانده می‌شود - _fetchReservedOffersFromBloc(); + _reservedOffersFuture = _fetchReservedOffers(); } - void _fetchReservedOffersFromBloc() { - final offersState = context.read().state; - // بررسی می‌کند که آیا پیشنهادها قبلا بارگذاری شده‌اند یا خیر - if (offersState is OffersLoadSuccess) { - final allOffers = offersState.offers; - if (mounted) { - setState(() { - _reservedOffers = allOffers - .where((offer) => _reservedIds.contains(offer.id)) - .toList(); - }); + Future> _fetchReservedOffers() async { + try { + const storage = FlutterSecureStorage(); + final token = await storage.read(key: 'accessToken'); + if (token == null) { + throw Exception('شما وارد حساب کاربری خود نشده‌اید.'); } + + final dio = Dio(); + final response = await dio.get( + ApiConfig.baseUrl + ApiConfig.getReservations, + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); + + if (response.statusCode == 200) { + final List reserves = response.data['reserves']; + + // ******** شروع تغییر ۲ ******** + // تبدیل ساختار جدید JSON به فرمت مورد انتظار OfferModel + return reserves.map((reserveData) { + final discountData = reserveData['Discount'] as Map; + final shopData = reserveData['Shop'] as Map; + + // ترکیب اطلاعات برای سازگاری با مدل + final fullOfferData = { + ...discountData, + 'shopData': shopData, + 'Distance': reserveData['Distance'], // پاس دادن فاصله به مدل + }; + + return OfferModel.fromJson(fullOfferData); + }).toList(); + // ******** پایان تغییر ۲ ******** + } else { + throw Exception('خطا در دریافت اطلاعات از سرور'); + } + } on DioException catch (e) { + final errorMessage = e.response?.data?['message'] ?? 'خطای شبکه'; + throw Exception(errorMessage); + } catch (e) { + throw Exception('خطایی رخ داد: $e'); } } @@ -51,17 +74,36 @@ class _ReservedListPageState extends State { textDirection: TextDirection.rtl, child: Scaffold( appBar: _buildCustomAppBar(context), - // FutureBuilder با یک ویجت ساده جایگزین شد - body: _reservedOffers.isEmpty - ? const Center( + body: FutureBuilder>( + future: _reservedOffersFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text('خطا: ${snapshot.error}', textAlign: TextAlign.center), + ), + ); + } + + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center( child: Text('هیچ آیتم رزرو شده‌ای یافت نشد.'), - ) - : ListView.builder( - padding: const EdgeInsets.all(16.0), - itemCount: _reservedOffers.length, - itemBuilder: (context, index) { - final offer = _reservedOffers[index]; - return Padding( + ); + } + + final reservedOffers = snapshot.data!; + + return ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: reservedOffers.length, + itemBuilder: (context, index) { + final offer = reservedOffers[index]; + return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -79,9 +121,7 @@ class _ReservedListPageState extends State { const Divider(thickness: 1.5), const SizedBox(height: 2), ReservedListItemCard(offer: offer), - SizedBox( - height: 15, - ) + const SizedBox(height: 15), ], ), ).animate().fadeIn(delay: (100 * index).ms).slideY( @@ -90,9 +130,11 @@ class _ReservedListPageState extends State { curve: Curves.easeOutCubic, ); }, - ) - ) ); } - + ); + }, + ), + ), + ); } PreferredSizeWidget _buildCustomAppBar(BuildContext context) { @@ -106,7 +148,6 @@ class _ReservedListPageState extends State { ), boxShadow: [ BoxShadow( - // ignore: deprecated_member_use color: Colors.black.withOpacity(0.08), blurRadius: 10, offset: const Offset(0, 4), @@ -118,7 +159,7 @@ class _ReservedListPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ - SizedBox(height: 15), + const SizedBox(height: 15), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -147,3 +188,4 @@ class _ReservedListPageState extends State { ), ); } +} \ No newline at end of file diff --git a/lib/presentation/pages/user_info_page.dart b/lib/presentation/pages/user_info_page.dart index 601063b..3781097 100644 --- a/lib/presentation/pages/user_info_page.dart +++ b/lib/presentation/pages/user_info_page.dart @@ -1,3 +1,5 @@ +// lib/presentation/pages/user_info_page.dart + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; @@ -22,6 +24,18 @@ class _UserInfoPageState extends State { super.dispose(); } + // ******** شروع تغییر ۱ ******** + // متد ناوبری را ساده می‌کنیم. + // این متد دیگر Provider جدیدی نمی‌سازد و از Provider های سراسری استفاده می‌کند. + void _navigateToNextPage() { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const NotificationPreferencesPage(), + ), + ); + } + // ******** پایان تغییر ۱ ******** + Widget _buildGenderRadio(String title, String value) { return InkWell( onTap: () => setState(() => _selectedGender = value), @@ -69,7 +83,6 @@ class _UserInfoPageState extends State { Positioned.fill( child: Image.asset(Assets.images.userinfo.path, fit: BoxFit.cover), ), - DraggableScrollableSheet( initialChildSize: 0.50, minChildSize: 0.50, @@ -91,7 +104,6 @@ class _UserInfoPageState extends State { width: 50, height: 5, decoration: BoxDecoration( - // ignore: deprecated_member_use color: Colors.grey.withOpacity(0.5), borderRadius: BorderRadius.circular(12), ), @@ -115,7 +127,6 @@ class _UserInfoPageState extends State { ), ), const SizedBox(height: 24), - Text( "جنسیت", style: textTheme.titleMedium?.copyWith( @@ -133,9 +144,13 @@ class _UserInfoPageState extends State { ], ), const SizedBox(height: 55), - BlocConsumer( listener: (context, state) { + // ******** لاگ: state جدید دریافت شد ******** + debugPrint( + "UserInfoPage: 🟠 Listener یک State جدید دریافت کرد: ${state.runtimeType}", + ); + if (state is AuthFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -144,10 +159,16 @@ class _UserInfoPageState extends State { ), ); } + if (state is AuthSuccess) { + // ******** لاگ: state موفقیت آمیز بود ******** + debugPrint( + "UserInfoPage: ✅ State از نوع AuthSuccess است. فراخوانی _navigateToNextPage...", + ); + _navigateToNextPage(); + } }, builder: (context, state) { final isLoading = state is AuthLoading; - return Column( children: [ SizedBox( @@ -173,14 +194,9 @@ class _UserInfoPageState extends State { }, ), const SizedBox(height: 9), - Center( child: TextButton( - onPressed: () { - Navigator.of(context).pushReplacement( - NotificationPreferencesPage.route(), - ); - }, + onPressed: _navigateToNextPage, child: const Text( "رد شدن", style: TextStyle(color: Colors.black), diff --git a/lib/presentation/reservation/cubit/reservation_cubit.dart b/lib/presentation/reservation/cubit/reservation_cubit.dart index 7ef449d..e73f739 100644 --- a/lib/presentation/reservation/cubit/reservation_cubit.dart +++ b/lib/presentation/reservation/cubit/reservation_cubit.dart @@ -1,3 +1,5 @@ +// lib/presentation/reservation/cubit/reservation_cubit.dart + // ignore: depend_on_referenced_packages import 'package:bloc/bloc.dart'; @@ -13,6 +15,11 @@ class ReservationCubit extends Cubit { emit(state.copyWith(reservedProductIds: updatedList)); } + // متد جدید برای تنظیم کردن همه شناسه‌های رزرو شده + void setReservedIds(List productIds) { + emit(state.copyWith(reservedProductIds: productIds)); + } + bool isProductReserved(String productId) { return state.reservedProductIds.contains(productId); } diff --git a/lib/presentation/widgets/reserved_list_item_card.dart b/lib/presentation/widgets/reserved_list_item_card.dart index c65144b..f5a34a0 100644 --- a/lib/presentation/widgets/reserved_list_item_card.dart +++ b/lib/presentation/widgets/reserved_list_item_card.dart @@ -1,8 +1,8 @@ -// lib/presentation/widgets/reserved_list_item_card.dart - -import 'dart:async'; + import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -23,6 +23,7 @@ class _ReservedListItemCardState extends State { bool _isExpanded = false; Timer? _timer; Duration _remaining = Duration.zero; + Future? _qrTokenFuture; @override void initState() { @@ -33,6 +34,38 @@ class _ReservedListItemCardState extends State { }); } + Future _generateQrToken() async { + const storage = FlutterSecureStorage(); + final userID = await storage.read(key: 'userID'); + + if (userID == null) { + throw Exception("User ID not found"); + } + + final payload = { + 'userID': userID, + 'discountID': widget.offer.id, + 'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + + const secretKey = 'your_super_secret_key_for_qr'; + final jwt = JWT(payload); + final token = jwt.sign(SecretKey(secretKey)); + + return token; + } + + void _toggleExpansion() { + setState(() { + _isExpanded = !_isExpanded; + // توکن فقط زمانی ساخته می‌شود که کاربر پنل را باز می‌کند + if (_isExpanded && _qrTokenFuture == null) { + _qrTokenFuture = _generateQrToken(); + } + }); + } + + void _calculateRemainingTime() { final now = DateTime.now(); if (widget.offer.expiryTime.isAfter(now)) { @@ -55,18 +88,8 @@ class _ReservedListItemCardState extends State { super.dispose(); } - // ignore: unused_element - String _formatDuration(Duration duration) { - if (duration.inSeconds <= 0) return "پایان یافته"; - final hours = duration.inHours.toString().padLeft(2, '0'); - final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0'); - final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); - return '$hours:$minutes:$seconds'; - } - @override Widget build(BuildContext context) { - return Column( children: [ Card( @@ -194,7 +217,7 @@ class _ReservedListItemCardState extends State { ), SizedBox(width: 10), TextButton( - onPressed: () => setState(() => _isExpanded = !_isExpanded), + onPressed: _toggleExpansion, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -314,7 +337,7 @@ class _ReservedListItemCardState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - '(${widget.offer.discount})', + '(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)', style: const TextStyle( fontSize: 14, color: AppColors.singleOfferType, @@ -345,34 +368,37 @@ class _ReservedListItemCardState extends State { ), ], ), - SizedBox( - height: 20, - ), + const SizedBox(height: 20), Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Color.fromARGB(255, 246, 246, 246), borderRadius: BorderRadius.circular(16), ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(15.0), - child: QrImageView( - data: widget.offer.qrCodeData, + child: FutureBuilder( + future: _qrTokenFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 280.0, + child: Center(child: CircularProgressIndicator()), + ); + } + if (snapshot.hasError) { + return const SizedBox( + height: 280.0, + child: Center(child: Text("خطا در ساخت کد QR")), + ); + } + if (snapshot.hasData) { + return QrImageView( + data: snapshot.data!, version: QrVersions.auto, size: 280.0, - ), - ), - SizedBox(height: 10), - Text( - widget.offer.qrCodeData, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - ], + ); + } + return const SizedBox.shrink(); + }, ), ), ], diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart index b3c2885..b7f6e27 100644 --- a/lib/services/mqtt_service.dart +++ b/lib/services/mqtt_service.dart @@ -28,7 +28,7 @@ class MqttService { client = MqttServerClient.withPort(server, clientId, port); client.logging(on: true); client.keepAlivePeriod = 60; - client.autoReconnect = false; + client.autoReconnect = true; // ✅ فعال‌سازی اتصال مجدد خودکار client.setProtocolV311(); debugPrint('--- [MQTT] Attempting to connect...'); @@ -64,9 +64,20 @@ class MqttService { }); }; + // این callback زمانی فراخوانی می‌شود که اتصال قطع شود client.onDisconnected = () { debugPrint('❌ [MQTT] Disconnected.'); }; + + // این callback زمانی فراخوانی می‌شود که کلاینت در حال تلاش برای اتصال مجدد خودکار است + client.onAutoReconnect = () { + debugPrint('↪️ [MQTT] Auto-reconnecting...'); + }; + + // این callback زمانی فراخوانی می‌شود که اتصال مجدد خودکار موفقیت‌آمیز باشد + client.onAutoReconnected = () { + debugPrint('✅ [MQTT] Auto-reconnected successfully.'); + }; client.onSubscribed = (String topic) { debugPrint('✅ [MQTT] Subscribed to topic: $topic'); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 2c04cf6..9b55d26 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,9 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); + audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1462f6f..3cf4286 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux file_selector_linux flutter_localization flutter_secure_storage_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 053b1a1..3a6fae2 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,9 @@ import FlutterMacOS import Foundation +import audioplayers_darwin import cloud_firestore +import connectivity_plus import file_selector_macos import firebase_auth import firebase_core @@ -20,7 +22,9 @@ import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 8b4111d..0ff9e7a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.59" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" analyzer: dependency: transitive description: @@ -49,6 +57,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de + url: "https://pub.dev" + source: hosted + version: "6.5.0" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7" + url: "https://pub.dev" + source: hosted + version: "4.2.1" bloc: dependency: transitive description: @@ -225,6 +289,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -265,6 +345,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_jsonwebtoken: + dependency: "direct main" + description: + name: dart_jsonwebtoken + sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413" + url: "https://pub.dev" + source: hosted + version: "3.2.0" dart_style: dependency: transitive description: @@ -281,6 +369,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" dio: dependency: "direct main" description: @@ -297,6 +393,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" equatable: dependency: "direct main" description: @@ -917,6 +1021,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" octo_image: dependency: transitive description: @@ -1077,6 +1189,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 51a4a9a..6aab6fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,9 @@ dependencies: cloud_firestore: ^5.6.12 firebase_storage: ^12.4.10 flutter_secure_storage: ^9.2.4 + connectivity_plus: ^6.1.4 + dart_jsonwebtoken: ^3.2.0 + audioplayers: ^6.5.0 dev_dependencies: flutter_test: @@ -87,6 +90,7 @@ flutter: assets: - assets/images/ - assets/icons/ + - assets/sounds/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c5be68e..4ba7d55 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,7 +6,9 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include #include #include @@ -19,8 +21,12 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); CloudFirestorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseAuthPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 489377e..8930e6e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows cloud_firestore + connectivity_plus file_selector_windows firebase_auth firebase_core