diff --git a/assets/sounds/positive-notification-alert-351299.mp3 b/assets/sounds/positive-notification-alert-351299.mp3 new file mode 100644 index 0000000..19d7434 Binary files /dev/null and b/assets/sounds/positive-notification-alert-351299.mp3 differ 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