From 050fb6b6201825699ae82c3f6be22dfd57710792 Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Wed, 23 Jul 2025 14:30:42 +0330 Subject: [PATCH] mqtt connected --- android/app/src/main/AndroidManifest.xml | 3 + .../src/main/res/network_security_config.xml | 6 + lib/core/config/api_config.dart | 3 +- lib/data/models/comment_model.dart | 13 + .../models/datasources/offer_data_source.dart | 716 +++++++++--------- lib/data/models/discount_info_model.dart | 9 + lib/data/models/offer_model.dart | 139 +++- lib/data/models/working_hours.dart | 18 + lib/data/repositories/offer_repository.dart | 40 +- lib/main.dart | 77 +- lib/presentation/auth/bloc/auth_bloc.dart | 4 + .../bloc/notification_preferences_bloc.dart | 37 + .../bloc/notification_preferences_event.dart | 2 + lib/presentation/offer/bloc/offer_bloc.dart | 52 +- lib/presentation/offer/bloc/offer_event.dart | 15 +- .../bloc/widgets/category_offers_row.dart | 17 +- .../pages/notification_preferences_page.dart | 31 +- lib/presentation/pages/offers_page.dart | 290 +++++-- lib/presentation/pages/otp_page.dart | 19 +- .../pages/product_detail_page.dart | 134 ++-- .../pages/reserved_list_page.dart | 78 +- lib/presentation/pages/splash_screen.dart | 63 +- .../bloc/product_detail_bloc.dart | 58 +- lib/services/mqtt_service.dart | 163 ++-- 24 files changed, 1220 insertions(+), 767 deletions(-) create mode 100644 android/app/src/main/res/network_security_config.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 01490dd..df814f7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,12 +4,15 @@ + + + + + + 5.75.200.241 + + \ No newline at end of file diff --git a/lib/core/config/api_config.dart b/lib/core/config/api_config.dart index 3c370eb..21eaf36 100644 --- a/lib/core/config/api_config.dart +++ b/lib/core/config/api_config.dart @@ -3,5 +3,6 @@ class ApiConfig { static const String sendCode = "/login/sendcode"; static const String verifyCode = "/login/getcode"; static const String updateUser = "/user/updateName"; - static const String updateCategories = "/user/favoriteCategory"; + static const String updateCategories = "/user/favoriteCategory"; + static const String getFavoriteCategories = "/user/getfavoriteCategory"; // این خط اضافه شد } \ No newline at end of file diff --git a/lib/data/models/comment_model.dart b/lib/data/models/comment_model.dart index 9b16437..dd15047 100644 --- a/lib/data/models/comment_model.dart +++ b/lib/data/models/comment_model.dart @@ -1,3 +1,5 @@ +// lib/data/models/comment_model.dart + import 'package:equatable/equatable.dart'; class CommentModel extends Equatable { @@ -19,4 +21,15 @@ class CommentModel extends Equatable { @override List get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls]; + + factory CommentModel.fromJson(Map json) { + return CommentModel( + id: json['id'], + userName: json['userName'], + rating: (json['rating'] as num).toDouble(), + comment: json['comment'], + publishedAt: DateTime.parse(json['publishedAt']), + uploadedImageUrls: List.from(json['uploadedImageUrls'] ?? []), + ); + } } \ No newline at end of file diff --git a/lib/data/models/datasources/offer_data_source.dart b/lib/data/models/datasources/offer_data_source.dart index 17d1e83..0c6bafc 100644 --- a/lib/data/models/datasources/offer_data_source.dart +++ b/lib/data/models/datasources/offer_data_source.dart @@ -1,362 +1,362 @@ -import 'package:proxibuy/data/models/comment_model.dart'; -import 'package:proxibuy/data/models/discount_info_model.dart'; -import 'package:proxibuy/data/models/offer_model.dart'; -import 'package:proxibuy/data/models/working_hours.dart'; +// import 'package:proxibuy/data/models/comment_model.dart'; +// import 'package:proxibuy/data/models/discount_info_model.dart'; +// import 'package:proxibuy/data/models/offer_model.dart'; +// import 'package:proxibuy/data/models/working_hours.dart'; -abstract class OfferDataSource { - Future> getNearbyOffers(); - Future getOfferById(String id); -} +// abstract class OfferDataSource { +// Future> getNearbyOffers(); +// Future getOfferById(String id); +// } -class MockOfferDataSource implements OfferDataSource { - final List _mockOffers = [ - OfferModel( - id: '1', - storeName: 'روچیک (Ruchik)', - title: 'چیزبرگر', - discount: '۲۰٪', - imageUrls: [ - 'https://picsum.photos/seed/food/400/200', - 'https://picsum.photos/seed/burger1/400/400', - 'https://picsum.photos/seed/burger2/400/400', - ], - category: 'فست‌فود', - distanceInMeters: 130, - expiryTime: DateTime.now().add(const Duration(hours: 2, minutes: 30, seconds: 10)), - rating: 4.8, - workingHours: [ - WorkingHours( - day: 'شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'یکشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'دوشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'سه‌شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'چهارشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'پنج‌شنبه', - shifts: [ - Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), - Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), - ], - ), - WorkingHours(day: 'جمعه', shifts: []), - ], - discountType: 'رفیق‌بازی', - isOpen: false, - address: 'چهارباغ پایین ', - ratingCount: 340, - latitude: 32.660, - longitude: 51.670, - originalPrice: 150000, - finalPrice: 120000, - features: [ - "تهیه شده از بهترین و تازه‌ترین مواد اولیه", - "محیطی دنج و مناسب برای قرارهای دوستانه", - "دارای منوی متنوع برای تمام سلیقه‌ها", - ], - discountInfo: const DiscountInfoModel( - name: "رفیق‌بازی", - description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", - ), - comments: [ - CommentModel( - id: 'c1', - userName: 'سارا رضایی', - rating: 4.5, - comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', - publishedAt: DateTime.now().subtract(const Duration(days: 2)), - uploadedImageUrls: [ - 'https://picsum.photos/seed/user_img1/200/200', - 'https://picsum.photos/seed/user_img2/200/200', - ] - ), - CommentModel( - id: 'c2', - userName: 'pbuser_157', - rating: 4, - comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', - publishedAt: DateTime.now().subtract(const Duration(days: 5)), - ), - ], - qrCodeData: 'PROXIBUY-OFFER-ID-1', - ), - OfferModel( - id: '2', - storeName: 'کاخ سرهنگ', - title: 'عصرانه', - discount: '۲۰% ', - imageUrls: [ - 'https://picsum.photos/seed/food/400/200', - 'https://picsum.photos/seed/burger1/400/400', - 'https://picsum.photos/seed/burger2/400/400', - ], - category: 'رستوران', - distanceInMeters: 130, - expiryTime: DateTime.now().add(const Duration(hours: 5)), - rating: 4.8, - workingHours: [ - WorkingHours( - day: 'شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'یکشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'دوشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'سه‌شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'چهارشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'پنج‌شنبه', - shifts: [ - Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), - Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), - ], - ), - WorkingHours(day: 'جمعه', shifts: []), - ], - discountType: 'رفیق‌بازی', - isOpen: true, - address: 'چهارباغ پایین ', - ratingCount: 340, - latitude: 32.660, - longitude: 51.670, - originalPrice: 150000, - finalPrice: 120000, - features: [ - "تهیه شده از بهترین و تازه‌ترین مواد اولیه", - "محیطی دنج و مناسب برای قرارهای دوستانه", - "دارای منوی متنوع برای تمام سلیقه‌ها", - ], - discountInfo: const DiscountInfoModel( - name: "رفیق‌بازی", - description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", - ), - comments: [ - CommentModel( - id: 'c1', - userName: 'سارا رضایی', - rating: 4.5, - comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', - publishedAt: DateTime.now().subtract(const Duration(days: 2)), - uploadedImageUrls: [ - 'https://picsum.photos/seed/user_img1/200/200', - 'https://picsum.photos/seed/user_img2/200/200', - ] - ), - CommentModel( - id: 'c2', - userName: 'علی اکبری', - rating: 4, - comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', - publishedAt: DateTime.now().subtract(const Duration(days: 5)), - ), - ], - qrCodeData: 'PROXIBUY-OFFER-ID-1', - ), - OfferModel( - id: '3', - storeName: 'روچیک (Ruchik)', - title: 'چیزبرگر', - discount: '۲۰٪', - imageUrls: [ - 'https://picsum.photos/seed/food/ 400/200', - 'https://picsum.photos/seed/burger1/400/400', - 'https://picsum.photos/seed/burger2/400/400', - ], - category: 'فست‌فود', - distanceInMeters: 130, - expiryTime: DateTime.now().add(const Duration(hours: 5)), - rating: 4.8, - workingHours: [ - WorkingHours( - day: 'شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'یکشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'دوشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'سه‌شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'چهارشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'پنج‌شنبه', - shifts: [ - Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), - Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), - ], - ), - WorkingHours(day: 'جمعه', shifts: []), - ], - discountType: 'رفیق‌بازی', - isOpen: true, - address: 'چهارباغ پایین ', - ratingCount: 340, - latitude: 32.660, - longitude: 51.670, - originalPrice: 150000, - finalPrice: 120000, - features: [ - "تهیه شده از بهترین و تازه‌ترین مواد اولیه", - "محیطی دنج و مناسب برای قرارهای دوستانه", - "دارای منوی متنوع برای تمام سلیقه‌ها", - ], - discountInfo: const DiscountInfoModel( - name: "رفیق‌بازی", - description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", - ), - comments: [ - CommentModel( - id: 'c1', - userName: 'سارا رضایی', - rating: 4.5, - comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', - publishedAt: DateTime.now().subtract(const Duration(days: 2)), - uploadedImageUrls: [ - 'https://picsum.photos/seed/user_img1/200/200', - 'https://picsum.photos/seed/user_img2/200/200', - ] - ), - CommentModel( - id: 'c2', - userName: 'علی اکبری', - rating: 4, - comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', - publishedAt: DateTime.now().subtract(const Duration(days: 5)), - ), - ], - qrCodeData: 'PROXIBUY-OFFER-ID-1', - ), - OfferModel( - id: '4', - storeName: 'روچیک (Ruchik)', - title: 'چیزبرگر', - discount: '۲۰٪', - imageUrls: [ - 'https://picsum.photos/seed/food/400/200', - 'https://picsum.photos/seed/burger1/400/400', - 'https://picsum.photos/seed/burger2/400/400', - ], - category: 'فست‌فود', - distanceInMeters: 130, - expiryTime: DateTime.now().add(const Duration(hours: 5)), - rating: 4.8, - workingHours: [ - WorkingHours( - day: 'شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'یکشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'دوشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'سه‌شنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'چهارشنبه', - shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], - ), - WorkingHours( - day: 'پنج‌شنبه', - shifts: [ - Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), - Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), - ], - ), - WorkingHours(day: 'جمعه', shifts: []), - ], - discountType: 'رفیق‌بازی', - isOpen: false, - address: 'چهارباغ پایین ', - ratingCount: 340, - latitude: 32.660, - longitude: 51.670, - originalPrice: 150000, - finalPrice: 120000, - features: [ - "تهیه شده از بهترین و تازه‌ترین مواد اولیه", - "محیطی دنج و مناسب برای قرارهای دوستانه", - "دارای منوی متنوع برای تمام سلیقه‌ها", - ], - discountInfo: const DiscountInfoModel( - name: "رفیق‌بازی", - description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", - ), - comments: [ - CommentModel( - id: 'c1', - userName: 'سارا رضایی', - rating: 4.5, - comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', - publishedAt: DateTime.now().subtract(const Duration(days: 2)), - uploadedImageUrls: [ - 'https://picsum.photos/seed/user_img1/200/200', - 'https://picsum.photos/seed/user_img2/200/200', - ] - ), - CommentModel( - id: 'c2', - userName: 'علی اکبری', - rating: 4, - comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', - publishedAt: DateTime.now().subtract(const Duration(days: 5)), - ), - ], - qrCodeData: 'PROXIBUY-OFFER-ID-1', - ), - ]; +// class MockOfferDataSource implements OfferDataSource { +// final List _mockOffers = [ +// OfferModel( +// id: '1', +// storeName: 'روچیک (Ruchik)', +// title: 'چیزبرگر', +// discount: '۲۰٪', +// imageUrls: [ +// 'https://picsum.photos/seed/food/400/200', +// 'https://picsum.photos/seed/burger1/400/400', +// 'https://picsum.photos/seed/burger2/400/400', +// ], +// category: 'فست‌فود', +// distanceInMeters: 130, +// expiryTime: DateTime.now().add(const Duration(hours: 2, minutes: 30, seconds: 10)), +// rating: 4.8, +// workingHours: [ +// WorkingHours( +// day: 'شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'یکشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'دوشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'سه‌شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'چهارشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'پنج‌شنبه', +// shifts: [ +// Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), +// Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), +// ], +// ), +// WorkingHours(day: 'جمعه', shifts: []), +// ], +// discountType: 'رفیق‌بازی', +// isOpen: false, +// address: 'چهارباغ پایین ', +// ratingCount: 340, +// latitude: 32.660, +// longitude: 51.670, +// originalPrice: 150000, +// finalPrice: 120000, +// features: [ +// "تهیه شده از بهترین و تازه‌ترین مواد اولیه", +// "محیطی دنج و مناسب برای قرارهای دوستانه", +// "دارای منوی متنوع برای تمام سلیقه‌ها", +// ], +// discountInfo: const DiscountInfoModel( +// name: "رفیق‌بازی", +// description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", +// ), +// comments: [ +// CommentModel( +// id: 'c1', +// userName: 'سارا رضایی', +// rating: 4.5, +// comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', +// publishedAt: DateTime.now().subtract(const Duration(days: 2)), +// uploadedImageUrls: [ +// 'https://picsum.photos/seed/user_img1/200/200', +// 'https://picsum.photos/seed/user_img2/200/200', +// ] +// ), +// CommentModel( +// id: 'c2', +// userName: 'pbuser_157', +// rating: 4, +// comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', +// publishedAt: DateTime.now().subtract(const Duration(days: 5)), +// ), +// ], +// qrCodeData: 'PROXIBUY-OFFER-ID-1', +// ), +// OfferModel( +// id: '2', +// storeName: 'کاخ سرهنگ', +// title: 'عصرانه', +// discount: '۲۰% ', +// imageUrls: [ +// 'https://picsum.photos/seed/food/400/200', +// 'https://picsum.photos/seed/burger1/400/400', +// 'https://picsum.photos/seed/burger2/400/400', +// ], +// category: 'رستوران', +// distanceInMeters: 130, +// expiryTime: DateTime.now().add(const Duration(hours: 5)), +// rating: 4.8, +// workingHours: [ +// WorkingHours( +// day: 'شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'یکشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'دوشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'سه‌شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'چهارشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'پنج‌شنبه', +// shifts: [ +// Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), +// Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), +// ], +// ), +// WorkingHours(day: 'جمعه', shifts: []), +// ], +// discountType: 'رفیق‌بازی', +// isOpen: true, +// address: 'چهارباغ پایین ', +// ratingCount: 340, +// latitude: 32.660, +// longitude: 51.670, +// originalPrice: 150000, +// finalPrice: 120000, +// features: [ +// "تهیه شده از بهترین و تازه‌ترین مواد اولیه", +// "محیطی دنج و مناسب برای قرارهای دوستانه", +// "دارای منوی متنوع برای تمام سلیقه‌ها", +// ], +// discountInfo: const DiscountInfoModel( +// name: "رفیق‌بازی", +// description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", +// ), +// comments: [ +// CommentModel( +// id: 'c1', +// userName: 'سارا رضایی', +// rating: 4.5, +// comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', +// publishedAt: DateTime.now().subtract(const Duration(days: 2)), +// uploadedImageUrls: [ +// 'https://picsum.photos/seed/user_img1/200/200', +// 'https://picsum.photos/seed/user_img2/200/200', +// ] +// ), +// CommentModel( +// id: 'c2', +// userName: 'علی اکبری', +// rating: 4, +// comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', +// publishedAt: DateTime.now().subtract(const Duration(days: 5)), +// ), +// ], +// qrCodeData: 'PROXIBUY-OFFER-ID-1', +// ), +// OfferModel( +// id: '3', +// storeName: 'روچیک (Ruchik)', +// title: 'چیزبرگر', +// discount: '۲۰٪', +// imageUrls: [ +// 'https://picsum.photos/seed/food/ 400/200', +// 'https://picsum.photos/seed/burger1/400/400', +// 'https://picsum.photos/seed/burger2/400/400', +// ], +// category: 'فست‌فود', +// distanceInMeters: 130, +// expiryTime: DateTime.now().add(const Duration(hours: 5)), +// rating: 4.8, +// workingHours: [ +// WorkingHours( +// day: 'شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'یکشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'دوشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'سه‌شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'چهارشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'پنج‌شنبه', +// shifts: [ +// Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), +// Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), +// ], +// ), +// WorkingHours(day: 'جمعه', shifts: []), +// ], +// discountType: 'رفیق‌بازی', +// isOpen: true, +// address: 'چهارباغ پایین ', +// ratingCount: 340, +// latitude: 32.660, +// longitude: 51.670, +// originalPrice: 150000, +// finalPrice: 120000, +// features: [ +// "تهیه شده از بهترین و تازه‌ترین مواد اولیه", +// "محیطی دنج و مناسب برای قرارهای دوستانه", +// "دارای منوی متنوع برای تمام سلیقه‌ها", +// ], +// discountInfo: const DiscountInfoModel( +// name: "رفیق‌بازی", +// description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", +// ), +// comments: [ +// CommentModel( +// id: 'c1', +// userName: 'سارا رضایی', +// rating: 4.5, +// comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', +// publishedAt: DateTime.now().subtract(const Duration(days: 2)), +// uploadedImageUrls: [ +// 'https://picsum.photos/seed/user_img1/200/200', +// 'https://picsum.photos/seed/user_img2/200/200', +// ] +// ), +// CommentModel( +// id: 'c2', +// userName: 'علی اکبری', +// rating: 4, +// comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', +// publishedAt: DateTime.now().subtract(const Duration(days: 5)), +// ), +// ], +// qrCodeData: 'PROXIBUY-OFFER-ID-1', +// ), +// OfferModel( +// id: '4', +// storeName: 'روچیک (Ruchik)', +// title: 'چیزبرگر', +// discount: '۲۰٪', +// imageUrls: [ +// 'https://picsum.photos/seed/food/400/200', +// 'https://picsum.photos/seed/burger1/400/400', +// 'https://picsum.photos/seed/burger2/400/400', +// ], +// category: 'فست‌فود', +// distanceInMeters: 130, +// expiryTime: DateTime.now().add(const Duration(hours: 5)), +// rating: 4.8, +// workingHours: [ +// WorkingHours( +// day: 'شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'یکشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'دوشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'سه‌شنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'چهارشنبه', +// shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], +// ), +// WorkingHours( +// day: 'پنج‌شنبه', +// shifts: [ +// Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), +// Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), +// ], +// ), +// WorkingHours(day: 'جمعه', shifts: []), +// ], +// discountType: 'رفیق‌بازی', +// isOpen: false, +// address: 'چهارباغ پایین ', +// ratingCount: 340, +// latitude: 32.660, +// longitude: 51.670, +// originalPrice: 150000, +// finalPrice: 120000, +// features: [ +// "تهیه شده از بهترین و تازه‌ترین مواد اولیه", +// "محیطی دنج و مناسب برای قرارهای دوستانه", +// "دارای منوی متنوع برای تمام سلیقه‌ها", +// ], +// discountInfo: const DiscountInfoModel( +// name: "رفیق‌بازی", +// description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", +// ), +// comments: [ +// CommentModel( +// id: 'c1', +// userName: 'سارا رضایی', +// rating: 4.5, +// comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', +// publishedAt: DateTime.now().subtract(const Duration(days: 2)), +// uploadedImageUrls: [ +// 'https://picsum.photos/seed/user_img1/200/200', +// 'https://picsum.photos/seed/user_img2/200/200', +// ] +// ), +// CommentModel( +// id: 'c2', +// userName: 'علی اکبری', +// rating: 4, +// comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', +// publishedAt: DateTime.now().subtract(const Duration(days: 5)), +// ), +// ], +// qrCodeData: 'PROXIBUY-OFFER-ID-1', +// ), +// ]; - @override - Future> getNearbyOffers() async { - await Future.delayed(const Duration(seconds: 1)); - return _mockOffers; - } +// @override +// Future> getNearbyOffers() async { +// await Future.delayed(const Duration(seconds: 1)); +// return _mockOffers; +// } - @override - Future getOfferById(String id) async { - await Future.delayed(const Duration(milliseconds: 300)); - try { - return _mockOffers.firstWhere((offer) => offer.id == id); - } catch (e) { - return null; - } - } -} +// @override +// Future getOfferById(String id) async { +// await Future.delayed(const Duration(milliseconds: 300)); +// try { +// return _mockOffers.firstWhere((offer) => offer.id == id); +// } catch (e) { +// return null; +// } +// } +// } diff --git a/lib/data/models/discount_info_model.dart b/lib/data/models/discount_info_model.dart index f11a10f..f6ca74a 100644 --- a/lib/data/models/discount_info_model.dart +++ b/lib/data/models/discount_info_model.dart @@ -1,3 +1,5 @@ +// lib/data/models/discount_info_model.dart + import 'package:equatable/equatable.dart'; class DiscountInfoModel extends Equatable { @@ -11,4 +13,11 @@ class DiscountInfoModel extends Equatable { @override List get props => [name, description]; + + factory DiscountInfoModel.fromJson(Map json) { + return DiscountInfoModel( + name: json['name'], + description: json['description'], + ); + } } \ No newline at end of file diff --git a/lib/data/models/offer_model.dart b/lib/data/models/offer_model.dart index 45aa9ad..eb188a4 100644 --- a/lib/data/models/offer_model.dart +++ b/lib/data/models/offer_model.dart @@ -1,8 +1,43 @@ +// lib/data/models/offer_model.dart + import 'package:equatable/equatable.dart'; -import 'package:proxibuy/data/models/comment_model.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; + final String category; + final String address; + final double latitude; + final double longitude; + final List properties; + + const ShopData({ + required this.id, + required this.name, + required this.category, + required this.address, + required this.latitude, + required this.longitude, + required this.properties, + }); + + factory ShopData.fromJson(Map json) { + return ShopData( + id: json['_id'] ?? '', + 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, + properties: List.from(json['Property'] ?? []), + ); + } +} + class OfferModel extends Equatable { final String id; final String storeName; @@ -25,7 +60,7 @@ class OfferModel extends Equatable { final List features; final DiscountInfoModel? discountInfo; final List comments; - final String qrCodeData; + final String qrCodeData; const OfferModel({ required this.id, @@ -52,24 +87,96 @@ class OfferModel extends Equatable { required this.qrCodeData, }); + factory OfferModel.fromJson(Map json) { // <-- پارامتر calculatedDistance حذف شد + final shopData = ShopData.fromJson(json['shopData']); + + 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 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); + } 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; + + + 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() ?? 'بدون دسته‌بندی', + expiryTime: DateTime.tryParse(json['EndDate'] ?? '') ?? DateTime.now().add(const Duration(days: 1)), + discountType: json['Type']?.toString() ?? '', + originalPrice: originalPriceValue, + finalPrice: finalPriceValue, + qrCodeData: json['QRcode'] ?? '', + storeName: shopData.name, + address: shopData.address, + latitude: shopData.latitude, + longitude: shopData.longitude, + features: shopData.properties, + 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, + ); + } + String get coverImageUrl => imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image'; @override - List get props => [ - id, - title, - storeName, - rating, - ratingCount, - latitude, - longitude, - features, - discountInfo, - comments, - qrCodeData - ]; - + List get props => [id]; + String get distanceAsString { if (distanceInMeters < 1000) { return "$distanceInMeters متر"; diff --git a/lib/data/models/working_hours.dart b/lib/data/models/working_hours.dart index b08a70b..8251d9c 100644 --- a/lib/data/models/working_hours.dart +++ b/lib/data/models/working_hours.dart @@ -1,3 +1,4 @@ +// lib/data/models/working_hours.dart import 'package:equatable/equatable.dart'; @@ -9,6 +10,13 @@ class Shift extends Equatable { @override List get props => [openAt, closeAt]; + + factory Shift.fromJson(Map json) { + return Shift( + openAt: json['openAt'], + closeAt: json['closeAt'], + ); + } } class WorkingHours extends Equatable { @@ -21,4 +29,14 @@ class WorkingHours extends Equatable { @override List get props => [day, shifts]; + + factory WorkingHours.fromJson(Map json) { + var shiftsFromJson = json['shifts'] as List; + List shiftList = shiftsFromJson.map((s) => Shift.fromJson(s)).toList(); + + return WorkingHours( + day: json['day'], + shifts: shiftList, + ); + } } \ No newline at end of file diff --git a/lib/data/repositories/offer_repository.dart b/lib/data/repositories/offer_repository.dart index 7b2557d..1d203f5 100644 --- a/lib/data/repositories/offer_repository.dart +++ b/lib/data/repositories/offer_repository.dart @@ -1,26 +1,26 @@ -import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; -import 'package:proxibuy/data/models/offer_model.dart'; +// import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; +// import 'package:proxibuy/data/models/offer_model.dart'; -class OfferRepository { - final OfferDataSource _offerDataSource; +// class OfferRepository { +// final OfferDataSource _offerDataSource; - OfferRepository({required OfferDataSource offerDataSource}) - : _offerDataSource = offerDataSource; +// OfferRepository({required OfferDataSource offerDataSource}) +// : _offerDataSource = offerDataSource; - Future> fetchOffers({required List selectedCategories}) async { - final allOffers = await _offerDataSource.getNearbyOffers(); +// Future> fetchOffers({required List selectedCategories}) async { +// final allOffers = await _offerDataSource.getNearbyOffers(); - if (selectedCategories.isEmpty) { - return allOffers; - } +// if (selectedCategories.isEmpty) { +// return allOffers; +// } - final filteredOffers = allOffers - .where((offer) => selectedCategories.contains(offer.category)) - .toList(); +// final filteredOffers = allOffers +// .where((offer) => selectedCategories.contains(offer.category)) +// .toList(); - return filteredOffers; - } - Future fetchOfferById(String id) async { - return _offerDataSource.getOfferById(id); - } -} \ No newline at end of file +// return filteredOffers; +// } +// Future fetchOfferById(String id) async { +// return _offerDataSource.getOfferById(id); +// } +// } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 3ca6bde..0093235 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,14 +10,11 @@ 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'; import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; -import 'package:proxibuy/presentation/pages/offers_page.dart'; -import 'package:proxibuy/presentation/pages/otp_page.dart'; -import 'package:proxibuy/presentation/pages/user_info_page.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; -// import 'package:proxibuy/services/mqtt_service.dart'; +import 'package:proxibuy/services/mqtt_service.dart'; import 'core/config/app_colors.dart'; -import 'presentation/pages/onboarding_page.dart'; -import 'package:proxibuy/presentation/pages/splash_screen.dart'; // <--- ایمپورت جدید +import 'package:proxibuy/presentation/pages/splash_screen.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -33,20 +30,18 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ + RepositoryProvider( + create: (context) => MqttService(), + ), BlocProvider( create: (context) => AuthBloc()..add(CheckAuthStatusEvent()), ), - RepositoryProvider( - create: (context) => - OfferRepository(offerDataSource: MockOfferDataSource()), - ), + // RepositoryProvider برای OfferRepository حذف شد BlocProvider( create: (context) => ReservationCubit(), ), BlocProvider( - create: (context) => OffersBloc( - offerRepository: context.read(), - ), + create: (context) => OffersBloc(), ), BlocProvider( create: (context) => NotificationPreferencesBloc(), @@ -55,7 +50,7 @@ class MyApp extends StatelessWidget { child: MaterialApp( title: 'Proxibuy', debugShowCheckedModeBanner: false, - home: const SplashScreen(), // <--- استفاده از صفحه اسپلش + home: const SplashScreen(), localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -131,36 +126,36 @@ class MyApp extends StatelessWidget { } } -class AppRouter extends StatelessWidget { - const AppRouter({super.key}); +// class AppRouter extends StatelessWidget { +// const AppRouter({super.key}); - @override - Widget build(BuildContext context) { - final authState = context.select((AuthBloc bloc) => bloc.state); +// @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 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 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 AuthSuccess) { +// return const OffersPage(); +// } - if (authState is AuthNeedsInfo) { - return const UserInfoPage(); - } +// if (authState is AuthNeedsInfo) { +// return const UserInfoPage(); +// } - return const OnboardingPage(); - } -} +// return const OnboardingPage(); +// } +// } diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart index c74894e..0cdb4d9 100644 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ b/lib/presentation/auth/bloc/auth_bloc.dart @@ -1,3 +1,5 @@ +// lib/presentation/auth/bloc/auth_bloc.dart + import 'package:bloc/bloc.dart'; import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; @@ -74,8 +76,10 @@ class AuthBloc extends Bloc { if (response.statusCode == 200) { final accessToken = response.data['data']['accessToken']; final refreshToken = response.data['data']['refreshToken']; + final userID = response.data['data']['ID']; // <-- خط جدید: استخراج ID await _storage.write(key: 'accessToken', value: accessToken); await _storage.write(key: 'refreshToken', value: refreshToken); + await _storage.write(key: 'userID', value: userID); // <-- خط جدید: ذخیره ID emit(AuthNeedsInfo()); } else { emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست')); diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart index dd2e96f..760ea47 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart @@ -16,6 +16,7 @@ class NotificationPreferencesBloc on(_onLoadCategories); on(_onToggleCategorySelection); on(_onSubmitPreferences); + on(_onLoadFavoriteCategories); // این خط اضافه شد add(LoadCategories()); } @@ -77,4 +78,40 @@ class NotificationPreferencesBloc 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) { + emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید.")); + return; + } + + final response = await _dio.get( + ApiConfig.baseUrl + ApiConfig.getFavoriteCategories, + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); + + if (response.statusCode == 200) { + final List fCategory = response.data['data']['FCategory']; + final Set favoriteCategoryIds = + fCategory.map((category) => category['ID'] as String).toSet(); + emit(state.copyWith( + selectedCategoryIds: favoriteCategoryIds, + isLoading: false, + )); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: response.data['message'] ?? 'خطا در دریافت اطلاعات')); + } + } on DioException catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); + } + } } \ No newline at end of file diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart index 1fc014e..6dfde45 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart @@ -9,6 +9,8 @@ abstract class NotificationPreferencesEvent extends Equatable { class LoadCategories extends NotificationPreferencesEvent {} +class LoadFavoriteCategories extends NotificationPreferencesEvent {} // این کلاس اضافه شد + class ToggleCategorySelection extends NotificationPreferencesEvent { final String categoryId; diff --git a/lib/presentation/offer/bloc/offer_bloc.dart b/lib/presentation/offer/bloc/offer_bloc.dart index 0c1b20f..ff13e5a 100644 --- a/lib/presentation/offer/bloc/offer_bloc.dart +++ b/lib/presentation/offer/bloc/offer_bloc.dart @@ -1,31 +1,41 @@ -// ignore: depend_on_referenced_packages +// lib/presentation/offer/bloc/offer_bloc.dart + import 'package:bloc/bloc.dart'; -import 'package:proxibuy/data/repositories/offer_repository.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; - class OffersBloc extends Bloc { - final OfferRepository _offerRepository; - - OffersBloc({required OfferRepository offerRepository}) - : _offerRepository = offerRepository, - super(OffersInitial()) { - on(_onFetchRequested); + OffersBloc() : super(OffersInitial()) { + on(_onOffersReceivedFromMqtt); + on(_onClearOffers); // رویداد جدید برای پاک کردن دیتا } - Future _onFetchRequested( - OffersFetchRequested event, + void _onOffersReceivedFromMqtt( + OffersReceivedFromMqtt event, Emitter emit, - ) async { - emit(OffersLoadInProgress()); - try { - final offers = await _offerRepository.fetchOffers( - selectedCategories: event.selectedCategories, - ); - emit(OffersLoadSuccess(offers)); - } catch (e) { - emit(OffersLoadFailure(e.toString())); + ) { + // فقط در صورتی که لیست جدید خالی نباشد، آن را جایگزین کن + if (event.offers.isNotEmpty) { + emit(OffersLoadSuccess(event.offers)); + } + // اگر لیست جدید خالی بود، و قبلا دیتایی داشتیم، حالت را تغییر نده + // این کار از نمایش صفحه خالی جلوگیری می‌کند + else if (state is! OffersLoadSuccess) { + // اگر اولین بار است و لیست خالی است، حالت موفقیت با لیست خالی را نشان بده + emit(const OffersLoadSuccess([])); } } -} \ No newline at end of file + + // برای زمانی که مثلا کاربر GPS را خاموش می‌کند + void _onClearOffers(ClearOffers event, Emitter emit) { + emit(OffersInitial()); + } +} + // مدیریت رویداد جدید + void _onOffersReceivedFromMqtt( + OffersReceivedFromMqtt event, + Emitter emit, + ) { + // جایگزین کردن لیست پیشنهادها با داده‌های جدید + emit(OffersLoadSuccess(event.offers)); + } diff --git a/lib/presentation/offer/bloc/offer_event.dart b/lib/presentation/offer/bloc/offer_event.dart index f325f20..f6816e1 100644 --- a/lib/presentation/offer/bloc/offer_event.dart +++ b/lib/presentation/offer/bloc/offer_event.dart @@ -1,5 +1,7 @@ +// lib/presentation/offer/bloc/offer_event.dart import 'package:equatable/equatable.dart'; +import 'package:proxibuy/data/models/offer_model.dart'; abstract class OffersEvent extends Equatable { const OffersEvent(); @@ -15,4 +17,15 @@ class OffersFetchRequested extends OffersEvent { @override List get props => [selectedCategories]; -} \ No newline at end of file +} + +class OffersReceivedFromMqtt extends OffersEvent { + final List offers; + + const OffersReceivedFromMqtt(this.offers); + + @override + List get props => [offers]; +} + +class ClearOffers extends OffersEvent {} // این کلاس را اضافه کنید diff --git a/lib/presentation/offer/bloc/widgets/category_offers_row.dart b/lib/presentation/offer/bloc/widgets/category_offers_row.dart index b200ce1..10a24c3 100644 --- a/lib/presentation/offer/bloc/widgets/category_offers_row.dart +++ b/lib/presentation/offer/bloc/widgets/category_offers_row.dart @@ -22,13 +22,13 @@ class CategoryOffersRow extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text( categoryTitle, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), ), ), SizedBox( - height: 300, + height: 300, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -39,12 +39,13 @@ class CategoryOffersRow extends StatelessWidget { padding: const EdgeInsets.only(left: 12.0), child: OfferCard( offer: offer, - width: 320, + width: 320, onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) { - return ProductDetailPage(offerId: offer.id,); + // کل آبجکت offer پاس داده می‌شود + return ProductDetailPage(offer: offer); }, ), ); @@ -54,8 +55,8 @@ class CategoryOffersRow extends StatelessWidget { }, ), ), - // const SizedBox(height: 16), + // const SizedBox(height: 16), ], ); } -} \ 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 a9e1842..8f53888 100644 --- a/lib/presentation/pages/notification_preferences_page.dart +++ b/lib/presentation/pages/notification_preferences_page.dart @@ -11,18 +11,38 @@ import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/widgets/category_selection_card.dart'; import 'package:shared_preferences/shared_preferences.dart'; -class NotificationPreferencesPage extends StatelessWidget { - const NotificationPreferencesPage({super.key}); +class NotificationPreferencesPage extends StatefulWidget { + // This parameter is used to decide whether to fetch favorite categories on start + final bool loadFavoritesOnStart; - static Route route() { - return MaterialPageRoute( + // 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(), - child: const NotificationPreferencesPage(), + // The widget is created here, passing the parameter correctly + child: NotificationPreferencesPage(loadFavoritesOnStart: loadFavorites), ), ); } + @override + State createState() => + _NotificationPreferencesPageState(); +} + +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()); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -100,6 +120,7 @@ class NotificationPreferencesPage extends StatelessWidget { body: BlocListener( listener: (context, state) { if (state.submissionSuccess) { + // Pop the page and return 'true' to signal a successful update if (Navigator.canPop(context)) { Navigator.of(context).pop(true); } else { diff --git a/lib/presentation/pages/offers_page.dart b/lib/presentation/pages/offers_page.dart index 51f7815..fbd8068 100644 --- a/lib/presentation/pages/offers_page.dart +++ b/lib/presentation/pages/offers_page.dart @@ -1,7 +1,11 @@ +// lib/presentation/pages/offers_page.dart + +import 'dart:async'; import 'package:collection/collection.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:geolocator/geolocator.dart'; import 'package:proxibuy/core/config/app_colors.dart'; @@ -14,11 +18,11 @@ import 'package:proxibuy/presentation/offer/bloc/widgets/category_offers_row.dar import 'package:proxibuy/presentation/pages/notification_preferences_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/gps_dialog.dart'; -import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart'; +import 'package:proxibuy/services/mqtt_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class OffersPage extends StatefulWidget { + // این پارامتر دیگر استفاده نمی‌شود اما برای سازگاری باقی می‌ماند final bool showDialogsOnLoad; const OffersPage({super.key, this.showDialogsOnLoad = false}); @@ -29,25 +33,155 @@ class OffersPage extends StatefulWidget { class _OffersPageState extends State { List _selectedCategories = []; + StreamSubscription? _locationServiceSubscription; + StreamSubscription? _mqttMessageSubscription; + Timer? _locationTimer; + bool _isSubscribedToOffers = false; + bool _isGpsEnabled = false; @override void initState() { super.initState(); - _loadOffersAndPreferences(); + _initializePage(); + } - if (widget.showDialogsOnLoad) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (mounted) { - await showGPSDialog(context); - } - if (mounted) { - await showNotificationPermissionDialog(context); - } - }); + Future _initializePage() async { + await _loadPreferences(); + _subscribeToUserOffersOnLoad(); + _initLocationListener(); + } + + @override + void dispose() { + _locationServiceSubscription?.cancel(); + _mqttMessageSubscription?.cancel(); + _locationTimer?.cancel(); + super.dispose(); + } + + Future _subscribeToUserOffersOnLoad() async { + final storage = const FlutterSecureStorage(); + final userID = await storage.read(key: 'userID'); + if (userID != null && mounted) { + _subscribeToUserOffers(userID); } } - Future _loadOffersAndPreferences() async { + void _initLocationListener() { + _checkInitialGpsStatus(); + _locationServiceSubscription = + Geolocator.getServiceStatusStream().listen((status) { + final isEnabled = status == ServiceStatus.enabled; + if (mounted && _isGpsEnabled != isEnabled) { + setState(() { + _isGpsEnabled = isEnabled; + }); + } + + if (isEnabled) { + _startSendingLocationUpdates(); + } else { + print("❌ Location Service Disabled. Stopping updates."); + _locationTimer?.cancel(); + context.read().add(ClearOffers()); + } + }); + } + + Future _checkInitialGpsStatus() async { + final status = await Geolocator.isLocationServiceEnabled(); + if (mounted) { + setState(() { + _isGpsEnabled = status; + }); + if (_isGpsEnabled) { + _startSendingLocationUpdates(); + } + } + } + + void _startSendingLocationUpdates() { + print("🚀 Starting periodic location updates."); + _locationTimer?.cancel(); + _locationTimer = Timer.periodic(const Duration(seconds: 15), (timer) { + _sendLocationUpdate(); + }); + _sendLocationUpdate(); + } + + Future _sendLocationUpdate() async { + final mqttService = context.read(); + if (!mqttService.isConnected) { + print("⚠️ MQTT not connected in OffersPage. Cannot send location."); + return; + } + + try { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) { + print("🚫 Location permission denied by user."); + return; + } + + final position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + ); + + const storage = FlutterSecureStorage(); + final userID = await storage.read(key: 'userID'); + + if (userID == null) { + print("⚠️ UserID not found. Cannot send location."); + return; + } + + 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"); + } + } + + void _subscribeToUserOffers(String userID) { + if (_isSubscribedToOffers) return; + + final mqttService = context.read(); + final topic = 'user-proxybuy/$userID'; + mqttService.subscribe(topic); + _isSubscribedToOffers = true; + + _mqttMessageSubscription = mqttService.messages.listen((message) { + final data = message['data']; + if (data != null && data is List) { + try { + List offers = data + .whereType>() + .map((json) => OfferModel.fromJson(json)) + .toList(); + + if (mounted) { + context.read().add(OffersReceivedFromMqtt(offers)); + } + } catch (e, stackTrace) { + print("❌ Error parsing offers from MQTT: $e"); + print(stackTrace); + } + } + }); + } + + Future _loadPreferences() async { final prefs = await SharedPreferences.getInstance(); final savedCategories = prefs.getStringList('user_selected_categories') ?? []; @@ -56,9 +190,6 @@ class _OffersPageState extends State { setState(() { _selectedCategories = savedCategories; }); - context.read().add( - OffersFetchRequested(selectedCategories: savedCategories), - ); } } @@ -78,13 +209,11 @@ class _OffersPageState extends State { TextButton( onPressed: () async { final result = await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const NotificationPreferencesPage(), - ), + NotificationPreferencesPage.route(loadFavorites: true), ); - + if (result == true && mounted) { - _loadOffersAndPreferences(); + _loadPreferences(); } }, child: Row( @@ -219,7 +348,7 @@ class _OffersPageState extends State { body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [_buildFavoriteCategoriesSection(), const OffersView()], + children: [_buildFavoriteCategoriesSection(), OffersView(isGpsEnabled: _isGpsEnabled)], ), ), ), @@ -228,58 +357,45 @@ class _OffersPageState extends State { } class OffersView extends StatelessWidget { - const OffersView({super.key}); + final bool isGpsEnabled; + + const OffersView({super.key, required this.isGpsEnabled}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - if (state is OffersLoadInProgress || state is OffersInitial) { + if (!isGpsEnabled) { + return _buildGpsActivationUI(context); + } + + if (state is OffersInitial) { return const SizedBox( height: 300, - child: Center(child: CircularProgressIndicator()), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text("در حال یافتن بهترین پیشنهادها برای شما..."), + ], + ), + ), ); } + if (state is OffersLoadSuccess) { if (state.offers.isEmpty) { - return Center( - child: SizedBox( - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 85), - SvgPicture.asset(Assets.images.emptyHome.path), - const SizedBox(height: 60), - ElevatedButton( - onPressed: () async { - await Geolocator.openLocationSettings(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.confirm, - foregroundColor: Colors.white, - disabledBackgroundColor: Colors.grey, - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 125, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - ), - child: const Text( - 'فعال‌سازی GPS', - style: TextStyle( - fontFamily: 'Dana', - fontSize: 16, - fontWeight: FontWeight.normal, - ), - ), - ), - const SizedBox(height: 15), - const Text('جست‌وجوی تصادفی'), - ], - ), + return const SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("فعلاً تخفیفی در این اطراف نیست!"), + Text("کمی قدم بزنید..."), + ], ), ), ); @@ -310,14 +426,60 @@ class OffersView extends StatelessWidget { }, ); } + if (state is OffersLoadFailure) { return SizedBox( height: 200, child: Center(child: Text("خطا در بارگذاری: ${state.error}")), ); } + return const SizedBox.shrink(); }, ); } -} + + Widget _buildGpsActivationUI(BuildContext context) { + return Center( + child: SizedBox( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 85), + SvgPicture.asset(Assets.images.emptyHome.path), + const SizedBox(height: 60), + ElevatedButton( + onPressed: () async { + await Geolocator.openLocationSettings(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.confirm, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey, + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 125, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: const Text( + 'فعال‌سازی GPS', + style: TextStyle( + fontFamily: 'Dana', + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + ), + const SizedBox(height: 15), + const Text('جست‌وجوی تصادفی'), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/pages/otp_page.dart b/lib/presentation/pages/otp_page.dart index 3d3153f..37013cb 100644 --- a/lib/presentation/pages/otp_page.dart +++ b/lib/presentation/pages/otp_page.dart @@ -1,8 +1,10 @@ +// 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/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'; @@ -152,9 +154,8 @@ class _OtpPageState extends State { }); } if (state is AuthNeedsInfo) { - final offerRepository = OfferRepository( - offerDataSource: MockOfferDataSource(), - ); + // **تغییر اصلی در این قسمت است** + // دیگر نیازی به ساخت OfferRepository نیست Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: @@ -163,11 +164,9 @@ class _OtpPageState extends State { BlocProvider.value( value: context.read(), ), + // OffersBloc دیگر به ریپازیتوری نیاز ندارد BlocProvider( - create: - (_) => OffersBloc( - offerRepository: offerRepository, - ), + create: (_) => OffersBloc(), ), BlocProvider( create: (_) => ReservationCubit(), @@ -343,4 +342,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 2c5143d..312e127 100644 --- a/lib/presentation/pages/product_detail_page.dart +++ b/lib/presentation/pages/product_detail_page.dart @@ -17,91 +17,65 @@ import 'package:proxibuy/presentation/widgets/comments_section.dart'; import 'package:slide_countdown/slide_countdown.dart'; class ProductDetailPage extends StatelessWidget { - final String offerId; + final OfferModel offer; - const ProductDetailPage({super.key, required this.offerId}); + const ProductDetailPage({super.key, required this.offer}); @override Widget build(BuildContext context) { - return BlocProvider( - create: - (context) => ProductDetailBloc( - offerRepository: context.read(), - )..add(ProductDetailFetchRequested(offerId: offerId)), - child: Scaffold( - body: Stack( - children: [ - BlocBuilder( - builder: (context, state) { - if (state is ProductDetailLoadInProgress || - state is ProductDetailInitial) { - return const Center(child: CircularProgressIndicator()); - } - if (state is ProductDetailLoadFailure) { - return Center(child: Text('خطا: ${state.error}')); - } - if (state is ProductDetailLoadSuccess) { - return ProductDetailView(offer: state.offer) - .animate() - .fadeIn(duration: 400.ms, curve: Curves.easeOut) - .slideY( - begin: 0.2, - duration: 400.ms, - curve: Curves.easeOut, - ); - } - return const SizedBox.shrink(); - }, - ), - Positioned( - bottom: 30, - left: 24, - right: 24, - child: BlocBuilder( - builder: (context, state) { - if (state is ProductDetailLoadSuccess) { - return ElevatedButton( - onPressed: () { - context.read().reserveProduct(state.offer.id); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ReservationConfirmationPage(offer: state.offer), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.confirm, - elevation: 5, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset(Assets.icons.receiptDisscount.path,), - const SizedBox(width: 12), - const Text( - 'رزرو تخفیف', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.normal, - color: Colors.black, - ), - ), - ], - ), - ).animate() - .fadeIn(delay: 200.ms, duration: 400.ms, curve: Curves.easeOut) - .slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut); - } - return const SizedBox.shrink(); - }, + return Scaffold( + body: Stack( + children: [ + // ویجت نمایش جزئیات مستقیما ساخته می‌شود + ProductDetailView(offer: offer) + .animate() + .fadeIn(duration: 400.ms, curve: Curves.easeOut) + .slideY( + begin: 0.2, + duration: 400.ms, + curve: Curves.easeOut, ), - ), - ], - ), + 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), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.confirm, + elevation: 5, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(Assets.icons.receiptDisscount.path), + const SizedBox(width: 12), + const Text( + 'رزرو تخفیف', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ], + ), + ).animate().fadeIn(delay: 200.ms).slideY(begin: 2), + ), + ], ), ); } diff --git a/lib/presentation/pages/reserved_list_page.dart b/lib/presentation/pages/reserved_list_page.dart index d58d3a5..2af40db 100644 --- a/lib/presentation/pages/reserved_list_page.dart +++ b/lib/presentation/pages/reserved_list_page.dart @@ -5,6 +5,8 @@ import 'package:flutter_svg/svg.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/offer/bloc/offer_bloc.dart'; +import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/widgets/reserved_list_item_card.dart'; @@ -17,20 +19,30 @@ class ReservedListPage extends StatefulWidget { class _ReservedListPageState extends State { late final List _reservedIds; - Future>? _reservedOffersFuture; + // دیگر نیازی به Future نیست + List _reservedOffers = []; @override void initState() { super.initState(); _reservedIds = context.read().state.reservedProductIds; - _reservedOffersFuture = _fetchReservedOffers(); + // اطلاعات مستقیما از BLoC خوانده می‌شود + _fetchReservedOffersFromBloc(); } - Future> _fetchReservedOffers() { - final offerRepo = context.read(); - final offerFutures = - _reservedIds.map((id) => offerRepo.fetchOfferById(id)).toList(); - return Future.wait(offerFutures); + 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(); + }); + } + } } @override @@ -39,40 +51,17 @@ class _ReservedListPageState extends State { textDirection: TextDirection.rtl, child: Scaffold( appBar: _buildCustomAppBar(context), - body: FutureBuilder>( - future: _reservedOffersFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return Center( - child: Text('خطا در بارگذاری اطلاعات: ${snapshot.error}'), - ); - } - - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center( - child: Text('هیچ آیتم رزرو شده‌ای وجود ندارد.'), - ); - } - - final reservedOffers = - snapshot.data!.whereType().toList(); - - if (reservedOffers.isEmpty) { - return const Center( + // FutureBuilder با یک ویجت ساده جایگزین شد + body: _reservedOffers.isEmpty + ? const Center( child: Text('هیچ آیتم رزرو شده‌ای یافت نشد.'), - ); - } - - return ListView.builder( - padding: const EdgeInsets.all(16.0), - itemCount: reservedOffers.length, - itemBuilder: (context, index) { - final offer = reservedOffers[index]; - return Padding( + ) + : 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, @@ -101,11 +90,9 @@ class _ReservedListPageState extends State { curve: Curves.easeOutCubic, ); }, - ); - }, - ), - ), - ); + ) + ) ); } + } PreferredSizeWidget _buildCustomAppBar(BuildContext context) { @@ -160,4 +147,3 @@ class _ReservedListPageState extends State { ), ); } -} \ No newline at end of file diff --git a/lib/presentation/pages/splash_screen.dart b/lib/presentation/pages/splash_screen.dart index bdfc24f..679ecee 100644 --- a/lib/presentation/pages/splash_screen.dart +++ b/lib/presentation/pages/splash_screen.dart @@ -1,10 +1,13 @@ +// lib/presentation/pages/splash_screen.dart + import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:proxibuy/presentation/pages/onboarding_page.dart'; import 'package:proxibuy/presentation/pages/offers_page.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; +import 'package:proxibuy/services/mqtt_service.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -14,31 +17,57 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { - late final StreamSubscription _authSubscription; @override void initState() { super.initState(); - final authBloc = context.read(); + // با کمی تاخیر برای نمایش لوگو، فرآیند را شروع می‌کنیم + Timer(const Duration(seconds: 2), _checkAuthAndNavigate); + } - _authSubscription = authBloc.stream.listen((state) { - _authSubscription.cancel(); - if (state is AuthSuccess) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const OffersPage()), - ); - } else { + Future _checkAuthAndNavigate() async { + final storage = const FlutterSecureStorage(); + final token = await storage.read(key: 'accessToken'); + + if (token != null && token.isNotEmpty) { + // کاربر احراز هویت شده است + debugPrint("--- SplashScreen: User is authenticated. Connecting to MQTT..."); + try { + final mqttService = context.read(); + + // ۱. منتظر می‌مانیم تا اتصال کامل برقرار شود + await mqttService.connect(token); + + // ۲. پس از اطمینان از اتصال، به صفحه بعد می‌رویم + if (mounted && mqttService.isConnected) { + debugPrint("--- SplashScreen: MQTT Connected. Navigating to OffersPage."); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const OffersPage()), + ); + } else if (mounted) { + // اگر به هر دلیلی پس از اتمام متد، اتصال برقرار نبود + debugPrint("--- SplashScreen: MQTT connection failed after attempt. Navigating to Onboarding."); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const OnboardingPage()), + ); + } + } catch (e) { + debugPrint("❌ SplashScreen: Critical error during MQTT connection: $e"); + if (mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const OnboardingPage()), + ); + } + } + } else { + // کاربر احراز هویت نشده است + debugPrint("--- SplashScreen: User not authenticated. Navigating to Onboarding."); + if (mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const OnboardingPage()), ); } - }); - } - - @override - void dispose() { - _authSubscription.cancel(); - super.dispose(); + } } @override diff --git a/lib/presentation/product_detail/bloc/product_detail_bloc.dart b/lib/presentation/product_detail/bloc/product_detail_bloc.dart index f4cba02..e1ba6f9 100644 --- a/lib/presentation/product_detail/bloc/product_detail_bloc.dart +++ b/lib/presentation/product_detail/bloc/product_detail_bloc.dart @@ -1,33 +1,33 @@ -// ignore: depend_on_referenced_packages -import 'package:bloc/bloc.dart'; -import 'package:proxibuy/data/repositories/offer_repository.dart'; -import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart'; -import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart'; +// // ignore: depend_on_referenced_packages +// import 'package:bloc/bloc.dart'; +// import 'package:proxibuy/data/repositories/offer_repository.dart'; +// import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart'; +// import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart'; -class ProductDetailBloc extends Bloc { - final OfferRepository _offerRepository; +// class ProductDetailBloc extends Bloc { +// final OfferRepository _offerRepository; - ProductDetailBloc({required OfferRepository offerRepository}) - : _offerRepository = offerRepository, - super(ProductDetailInitial()) { - on(_onFetchRequested); - } +// ProductDetailBloc({required OfferRepository offerRepository}) +// : _offerRepository = offerRepository, +// super(ProductDetailInitial()) { +// on(_onFetchRequested); +// } - Future _onFetchRequested( - ProductDetailFetchRequested event, - Emitter emit, - ) async { - emit(ProductDetailLoadInProgress()); - try { - final offer = await _offerRepository.fetchOfferById(event.offerId); - if (offer != null) { - emit(ProductDetailLoadSuccess(offer)); - } else { - emit(const ProductDetailLoadFailure('محصول مورد نظر یافت نشد.')); - } - } catch (e) { - emit(ProductDetailLoadFailure(e.toString())); - } - } -} \ No newline at end of file +// Future _onFetchRequested( +// ProductDetailFetchRequested event, +// Emitter emit, +// ) async { +// emit(ProductDetailLoadInProgress()); +// try { +// final offer = await _offerRepository.fetchOfferById(event.offerId); +// if (offer != null) { +// emit(ProductDetailLoadSuccess(offer)); +// } else { +// emit(const ProductDetailLoadFailure('محصول مورد نظر یافت نشد.')); +// } +// } catch (e) { +// emit(ProductDetailLoadFailure(e.toString())); +// } +// } +// } \ No newline at end of file diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart index 4a092c1..b3c2885 100644 --- a/lib/services/mqtt_service.dart +++ b/lib/services/mqtt_service.dart @@ -1,60 +1,123 @@ -// import 'dart:async'; -// import 'dart:math'; -// import 'package:mqtt_client/mqtt_client.dart'; -// import 'package:mqtt_client/mqtt_server_client.dart'; +// lib/services/mqtt_service.dart -// class MqttService { -// late MqttServerClient client; -// final String server = '5.75.200.241'; -// final int port = 1883; +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; -// Future connect(String token) async { -// // 1. معادل‌سازی پارامترها -// final String clientId = 'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0'); -// final String username = 'ignored'; -// final String password = token; // ✨ توکن شما مستقیماً به عنوان پسورد در نظر گرفته می‌شود +class MqttService { + late MqttServerClient client; + final String server = '5.75.200.241'; + final int port = 1883; + final StreamController> _messageStreamController = + StreamController.broadcast(); -// // 2. ساخت کلاینت -// client = MqttServerClient.withPort(server, clientId, port); -// client.logging(on: true); -// client.keepAlivePeriod = 60; -// client.autoReconnect = false; // معادل reconnectPeriod: 0 -// client.setProtocolV311(); + Stream> get messages => _messageStreamController.stream; -// // 3. ساخت پیام اتصال با پارامترهای تعریف شده -// final connMessage = MqttConnectMessage() -// .withClientIdentifier(clientId) -// .startClean() -// .authenticateAs(username, password); // ارسال نام کاربری و رمز عبور (توکن) + bool get isConnected => client.connectionStatus?.state == MqttConnectionState.connected; -// client.connectionMessage = connMessage; + Future connect(String token) async { + final String clientId = + 'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0'); + final String username = 'ignored'; + final String password = token; -// // 4. تعریف Callbackها و اتصال -// client.onConnected = () { -// print('✅ MQTT Connected'); -// client.updates!.listen((List> c) { -// final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage; -// final String payload = -// MqttPublishPayload.bytesToStringAsString(recMess.payload.message); -// print('Received message: "$payload" from topic: ${c[0].topic}'); -// }); + client = MqttServerClient.withPort(server, clientId, port); + client.logging(on: true); + client.keepAlivePeriod = 60; + client.autoReconnect = false; + client.setProtocolV311(); -// client.subscribe('test/topic', MqttQos.atLeastOnce); -// }; + debugPrint('--- [MQTT] Attempting to connect...'); + debugPrint('--- [MQTT] Server: $server:$port'); + debugPrint('--- [MQTT] ClientID: $clientId'); -// client.onDisconnected = () { -// print('❌ MQTT Disconnected'); -// }; + final connMessage = MqttConnectMessage() + .withClientIdentifier(clientId) + .startClean() + .authenticateAs(username, password); -// client.onSubscribed = (String topic) { -// print('✅ Subscribed to $topic'); -// }; + client.connectionMessage = connMessage; -// try { -// await client.connect(); -// } catch (e) { -// print('Exception: $e'); -// client.disconnect(); -// } -// } -// } \ No newline at end of file + client.onConnected = () { + debugPrint('✅ [MQTT] Connected successfully.'); + client.updates!.listen((List> c) { + final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage; + + final String payload = + MqttPublishPayload.bytesToStringAsString(recMess.payload.message); + + debugPrint('<<<<< [MQTT] Received Data <<<<<'); + debugPrint('<<<<< [MQTT] Topic: ${c[0].topic}'); + debugPrint('<<<<< [MQTT] Payload as String: $payload'); + debugPrint('<<<<< ======================== <<<<<'); + + try { + final Map jsonPayload = json.decode(payload); + _messageStreamController.add(jsonPayload); + } catch (e) { + debugPrint("❌ [MQTT] Error decoding received JSON: $e"); + } + }); + }; + + client.onDisconnected = () { + debugPrint('❌ [MQTT] Disconnected.'); + }; + + client.onSubscribed = (String topic) { + debugPrint('✅ [MQTT] Subscribed to topic: $topic'); + }; + + client.pongCallback = () { + debugPrint('🏓 [MQTT] Ping response received'); + }; + + try { + await client.connect(); + } on NoConnectionException catch (e) { + debugPrint('❌ [MQTT] Connection failed - No Connection Exception: $e'); + client.disconnect(); + } on SocketException catch (e) { + debugPrint('❌ [MQTT] Connection failed - Socket Exception: $e'); + client.disconnect(); + } catch (e) { + debugPrint('❌ [MQTT] Connection failed - General Exception: $e'); + client.disconnect(); + } + } + + void subscribe(String topic) { + if (isConnected) { + client.subscribe(topic, MqttQos.atLeastOnce); + } else { + debugPrint("⚠️ [MQTT] Cannot subscribe. Client is not connected."); + } + } + + void publish(String topic, Map message) { + if (isConnected) { + final builder = MqttClientPayloadBuilder(); + final payloadString = json.encode(message); + builder.addString(payloadString); + + debugPrint('>>>>> [MQTT] Publishing Data >>>>>'); + debugPrint('>>>>> [MQTT] Topic: $topic'); + debugPrint('>>>>> [MQTT] Payload: $payloadString'); + debugPrint('>>>>> ======================= >>>>>'); + + client.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!); + } else { + debugPrint("⚠️ [MQTT] Cannot publish. Client is not connected."); + } + } + + void dispose() { + debugPrint("--- [MQTT] Disposing MQTT Service."); + _messageStreamController.close(); + client.disconnect(); + } +} \ No newline at end of file