mqtt connected

This commit is contained in:
mohamadmahdi jebeli 2025-07-23 14:30:42 +03:30
parent 690813829d
commit 050fb6b620
24 changed files with 1220 additions and 767 deletions

View File

@ -4,12 +4,15 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:label="Proxibuy" android:label="Proxibuy"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<!-- android:networkSecurityConfig="@xml/network_security_config"> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">5.75.200.241</domain>
</domain-config>
</network-security-config>

View File

@ -3,5 +3,6 @@ class ApiConfig {
static const String sendCode = "/login/sendcode"; static const String sendCode = "/login/sendcode";
static const String verifyCode = "/login/getcode"; static const String verifyCode = "/login/getcode";
static const String updateUser = "/user/updateName"; static const String updateUser = "/user/updateName";
static const String updateCategories = "/user/favoriteCategory"; static const String updateCategories = "/user/favoriteCategory";
static const String getFavoriteCategories = "/user/getfavoriteCategory"; // این خط اضافه شد
} }

View File

@ -1,3 +1,5 @@
// lib/data/models/comment_model.dart
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class CommentModel extends Equatable { class CommentModel extends Equatable {
@ -19,4 +21,15 @@ class CommentModel extends Equatable {
@override @override
List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls]; List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls];
factory CommentModel.fromJson(Map<String, dynamic> 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<String>.from(json['uploadedImageUrls'] ?? []),
);
}
} }

View File

@ -1,362 +1,362 @@
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/discount_info_model.dart';
import 'package:proxibuy/data/models/offer_model.dart'; // import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/data/models/working_hours.dart'; // import 'package:proxibuy/data/models/working_hours.dart';
abstract class OfferDataSource { // abstract class OfferDataSource {
Future<List<OfferModel>> getNearbyOffers(); // Future<List<OfferModel>> getNearbyOffers();
Future<OfferModel?> getOfferById(String id); // Future<OfferModel?> getOfferById(String id);
} // }
class MockOfferDataSource implements OfferDataSource { // class MockOfferDataSource implements OfferDataSource {
final List<OfferModel> _mockOffers = [ // final List<OfferModel> _mockOffers = [
OfferModel( // OfferModel(
id: '1', // id: '1',
storeName: 'روچیک (Ruchik)', // storeName: 'روچیک (Ruchik)',
title: 'چیزبرگر', // title: 'چیزبرگر',
discount: '۲۰٪', // discount: '۲۰٪',
imageUrls: [ // imageUrls: [
'https://picsum.photos/seed/food/400/200', // 'https://picsum.photos/seed/food/400/200',
'https://picsum.photos/seed/burger1/400/400', // 'https://picsum.photos/seed/burger1/400/400',
'https://picsum.photos/seed/burger2/400/400', // 'https://picsum.photos/seed/burger2/400/400',
], // ],
category: 'فست‌فود', // category: 'فست‌فود',
distanceInMeters: 130, // distanceInMeters: 130,
expiryTime: DateTime.now().add(const Duration(hours: 2, minutes: 30, seconds: 10)), // expiryTime: DateTime.now().add(const Duration(hours: 2, minutes: 30, seconds: 10)),
rating: 4.8, // rating: 4.8,
workingHours: [ // workingHours: [
WorkingHours( // WorkingHours(
day: 'شنبه', // day: 'شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'یکشنبه', // day: 'یکشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'دوشنبه', // day: 'دوشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'سه‌شنبه', // day: 'سه‌شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'چهارشنبه', // day: 'چهارشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'پنج‌شنبه', // day: 'پنج‌شنبه',
shifts: [ // shifts: [
Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), // Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'),
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), // Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
], // ],
), // ),
WorkingHours(day: 'جمعه', shifts: []), // WorkingHours(day: 'جمعه', shifts: []),
], // ],
discountType: 'رفیق‌بازی', // discountType: 'رفیق‌بازی',
isOpen: false, // isOpen: false,
address: 'چهارباغ پایین ', // address: 'چهارباغ پایین ',
ratingCount: 340, // ratingCount: 340,
latitude: 32.660, // latitude: 32.660,
longitude: 51.670, // longitude: 51.670,
originalPrice: 150000, // originalPrice: 150000,
finalPrice: 120000, // finalPrice: 120000,
features: [ // features: [
"تهیه شده از بهترین و تازه‌ترین مواد اولیه", // "تهیه شده از بهترین و تازه‌ترین مواد اولیه",
"محیطی دنج و مناسب برای قرارهای دوستانه", // "محیطی دنج و مناسب برای قرارهای دوستانه",
"دارای منوی متنوع برای تمام سلیقه‌ها", // "دارای منوی متنوع برای تمام سلیقه‌ها",
], // ],
discountInfo: const DiscountInfoModel( // discountInfo: const DiscountInfoModel(
name: "رفیق‌بازی", // name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", // description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
), // ),
comments: [ // comments: [
CommentModel( // CommentModel(
id: 'c1', // id: 'c1',
userName: 'سارا رضایی', // userName: 'سارا رضایی',
rating: 4.5, // rating: 4.5,
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', // comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!',
publishedAt: DateTime.now().subtract(const Duration(days: 2)), // publishedAt: DateTime.now().subtract(const Duration(days: 2)),
uploadedImageUrls: [ // uploadedImageUrls: [
'https://picsum.photos/seed/user_img1/200/200', // 'https://picsum.photos/seed/user_img1/200/200',
'https://picsum.photos/seed/user_img2/200/200', // 'https://picsum.photos/seed/user_img2/200/200',
] // ]
), // ),
CommentModel( // CommentModel(
id: 'c2', // id: 'c2',
userName: 'pbuser_157', // userName: 'pbuser_157',
rating: 4, // rating: 4,
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', // comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
publishedAt: DateTime.now().subtract(const Duration(days: 5)), // publishedAt: DateTime.now().subtract(const Duration(days: 5)),
), // ),
], // ],
qrCodeData: 'PROXIBUY-OFFER-ID-1', // qrCodeData: 'PROXIBUY-OFFER-ID-1',
), // ),
OfferModel( // OfferModel(
id: '2', // id: '2',
storeName: 'کاخ سرهنگ', // storeName: 'کاخ سرهنگ',
title: 'عصرانه', // title: 'عصرانه',
discount: '۲۰% ', // discount: '۲۰% ',
imageUrls: [ // imageUrls: [
'https://picsum.photos/seed/food/400/200', // 'https://picsum.photos/seed/food/400/200',
'https://picsum.photos/seed/burger1/400/400', // 'https://picsum.photos/seed/burger1/400/400',
'https://picsum.photos/seed/burger2/400/400', // 'https://picsum.photos/seed/burger2/400/400',
], // ],
category: 'رستوران', // category: 'رستوران',
distanceInMeters: 130, // distanceInMeters: 130,
expiryTime: DateTime.now().add(const Duration(hours: 5)), // expiryTime: DateTime.now().add(const Duration(hours: 5)),
rating: 4.8, // rating: 4.8,
workingHours: [ // workingHours: [
WorkingHours( // WorkingHours(
day: 'شنبه', // day: 'شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'یکشنبه', // day: 'یکشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'دوشنبه', // day: 'دوشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'سه‌شنبه', // day: 'سه‌شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'چهارشنبه', // day: 'چهارشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'پنج‌شنبه', // day: 'پنج‌شنبه',
shifts: [ // shifts: [
Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), // Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'),
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), // Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
], // ],
), // ),
WorkingHours(day: 'جمعه', shifts: []), // WorkingHours(day: 'جمعه', shifts: []),
], // ],
discountType: 'رفیق‌بازی', // discountType: 'رفیق‌بازی',
isOpen: true, // isOpen: true,
address: 'چهارباغ پایین ', // address: 'چهارباغ پایین ',
ratingCount: 340, // ratingCount: 340,
latitude: 32.660, // latitude: 32.660,
longitude: 51.670, // longitude: 51.670,
originalPrice: 150000, // originalPrice: 150000,
finalPrice: 120000, // finalPrice: 120000,
features: [ // features: [
"تهیه شده از بهترین و تازه‌ترین مواد اولیه", // "تهیه شده از بهترین و تازه‌ترین مواد اولیه",
"محیطی دنج و مناسب برای قرارهای دوستانه", // "محیطی دنج و مناسب برای قرارهای دوستانه",
"دارای منوی متنوع برای تمام سلیقه‌ها", // "دارای منوی متنوع برای تمام سلیقه‌ها",
], // ],
discountInfo: const DiscountInfoModel( // discountInfo: const DiscountInfoModel(
name: "رفیق‌بازی", // name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", // description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
), // ),
comments: [ // comments: [
CommentModel( // CommentModel(
id: 'c1', // id: 'c1',
userName: 'سارا رضایی', // userName: 'سارا رضایی',
rating: 4.5, // rating: 4.5,
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', // comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!',
publishedAt: DateTime.now().subtract(const Duration(days: 2)), // publishedAt: DateTime.now().subtract(const Duration(days: 2)),
uploadedImageUrls: [ // uploadedImageUrls: [
'https://picsum.photos/seed/user_img1/200/200', // 'https://picsum.photos/seed/user_img1/200/200',
'https://picsum.photos/seed/user_img2/200/200', // 'https://picsum.photos/seed/user_img2/200/200',
] // ]
), // ),
CommentModel( // CommentModel(
id: 'c2', // id: 'c2',
userName: 'علی اکبری', // userName: 'علی اکبری',
rating: 4, // rating: 4,
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', // comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
publishedAt: DateTime.now().subtract(const Duration(days: 5)), // publishedAt: DateTime.now().subtract(const Duration(days: 5)),
), // ),
], // ],
qrCodeData: 'PROXIBUY-OFFER-ID-1', // qrCodeData: 'PROXIBUY-OFFER-ID-1',
), // ),
OfferModel( // OfferModel(
id: '3', // id: '3',
storeName: 'روچیک (Ruchik)', // storeName: 'روچیک (Ruchik)',
title: 'چیزبرگر', // title: 'چیزبرگر',
discount: '۲۰٪', // discount: '۲۰٪',
imageUrls: [ // imageUrls: [
'https://picsum.photos/seed/food/ 400/200', // 'https://picsum.photos/seed/food/ 400/200',
'https://picsum.photos/seed/burger1/400/400', // 'https://picsum.photos/seed/burger1/400/400',
'https://picsum.photos/seed/burger2/400/400', // 'https://picsum.photos/seed/burger2/400/400',
], // ],
category: 'فست‌فود', // category: 'فست‌فود',
distanceInMeters: 130, // distanceInMeters: 130,
expiryTime: DateTime.now().add(const Duration(hours: 5)), // expiryTime: DateTime.now().add(const Duration(hours: 5)),
rating: 4.8, // rating: 4.8,
workingHours: [ // workingHours: [
WorkingHours( // WorkingHours(
day: 'شنبه', // day: 'شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'یکشنبه', // day: 'یکشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'دوشنبه', // day: 'دوشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'سه‌شنبه', // day: 'سه‌شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'چهارشنبه', // day: 'چهارشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'پنج‌شنبه', // day: 'پنج‌شنبه',
shifts: [ // shifts: [
Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), // Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'),
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), // Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
], // ],
), // ),
WorkingHours(day: 'جمعه', shifts: []), // WorkingHours(day: 'جمعه', shifts: []),
], // ],
discountType: 'رفیق‌بازی', // discountType: 'رفیق‌بازی',
isOpen: true, // isOpen: true,
address: 'چهارباغ پایین ', // address: 'چهارباغ پایین ',
ratingCount: 340, // ratingCount: 340,
latitude: 32.660, // latitude: 32.660,
longitude: 51.670, // longitude: 51.670,
originalPrice: 150000, // originalPrice: 150000,
finalPrice: 120000, // finalPrice: 120000,
features: [ // features: [
"تهیه شده از بهترین و تازه‌ترین مواد اولیه", // "تهیه شده از بهترین و تازه‌ترین مواد اولیه",
"محیطی دنج و مناسب برای قرارهای دوستانه", // "محیطی دنج و مناسب برای قرارهای دوستانه",
"دارای منوی متنوع برای تمام سلیقه‌ها", // "دارای منوی متنوع برای تمام سلیقه‌ها",
], // ],
discountInfo: const DiscountInfoModel( // discountInfo: const DiscountInfoModel(
name: "رفیق‌بازی", // name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", // description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
), // ),
comments: [ // comments: [
CommentModel( // CommentModel(
id: 'c1', // id: 'c1',
userName: 'سارا رضایی', // userName: 'سارا رضایی',
rating: 4.5, // rating: 4.5,
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', // comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!',
publishedAt: DateTime.now().subtract(const Duration(days: 2)), // publishedAt: DateTime.now().subtract(const Duration(days: 2)),
uploadedImageUrls: [ // uploadedImageUrls: [
'https://picsum.photos/seed/user_img1/200/200', // 'https://picsum.photos/seed/user_img1/200/200',
'https://picsum.photos/seed/user_img2/200/200', // 'https://picsum.photos/seed/user_img2/200/200',
] // ]
), // ),
CommentModel( // CommentModel(
id: 'c2', // id: 'c2',
userName: 'علی اکبری', // userName: 'علی اکبری',
rating: 4, // rating: 4,
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', // comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
publishedAt: DateTime.now().subtract(const Duration(days: 5)), // publishedAt: DateTime.now().subtract(const Duration(days: 5)),
), // ),
], // ],
qrCodeData: 'PROXIBUY-OFFER-ID-1', // qrCodeData: 'PROXIBUY-OFFER-ID-1',
), // ),
OfferModel( // OfferModel(
id: '4', // id: '4',
storeName: 'روچیک (Ruchik)', // storeName: 'روچیک (Ruchik)',
title: 'چیزبرگر', // title: 'چیزبرگر',
discount: '۲۰٪', // discount: '۲۰٪',
imageUrls: [ // imageUrls: [
'https://picsum.photos/seed/food/400/200', // 'https://picsum.photos/seed/food/400/200',
'https://picsum.photos/seed/burger1/400/400', // 'https://picsum.photos/seed/burger1/400/400',
'https://picsum.photos/seed/burger2/400/400', // 'https://picsum.photos/seed/burger2/400/400',
], // ],
category: 'فست‌فود', // category: 'فست‌فود',
distanceInMeters: 130, // distanceInMeters: 130,
expiryTime: DateTime.now().add(const Duration(hours: 5)), // expiryTime: DateTime.now().add(const Duration(hours: 5)),
rating: 4.8, // rating: 4.8,
workingHours: [ // workingHours: [
WorkingHours( // WorkingHours(
day: 'شنبه', // day: 'شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'یکشنبه', // day: 'یکشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'دوشنبه', // day: 'دوشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'سه‌شنبه', // day: 'سه‌شنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'چهارشنبه', // day: 'چهارشنبه',
shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], // shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')],
), // ),
WorkingHours( // WorkingHours(
day: 'پنج‌شنبه', // day: 'پنج‌شنبه',
shifts: [ // shifts: [
Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), // Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'),
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), // Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
], // ],
), // ),
WorkingHours(day: 'جمعه', shifts: []), // WorkingHours(day: 'جمعه', shifts: []),
], // ],
discountType: 'رفیق‌بازی', // discountType: 'رفیق‌بازی',
isOpen: false, // isOpen: false,
address: 'چهارباغ پایین ', // address: 'چهارباغ پایین ',
ratingCount: 340, // ratingCount: 340,
latitude: 32.660, // latitude: 32.660,
longitude: 51.670, // longitude: 51.670,
originalPrice: 150000, // originalPrice: 150000,
finalPrice: 120000, // finalPrice: 120000,
features: [ // features: [
"تهیه شده از بهترین و تازه‌ترین مواد اولیه", // "تهیه شده از بهترین و تازه‌ترین مواد اولیه",
"محیطی دنج و مناسب برای قرارهای دوستانه", // "محیطی دنج و مناسب برای قرارهای دوستانه",
"دارای منوی متنوع برای تمام سلیقه‌ها", // "دارای منوی متنوع برای تمام سلیقه‌ها",
], // ],
discountInfo: const DiscountInfoModel( // discountInfo: const DiscountInfoModel(
name: "رفیق‌بازی", // name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", // description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
), // ),
comments: [ // comments: [
CommentModel( // CommentModel(
id: 'c1', // id: 'c1',
userName: 'سارا رضایی', // userName: 'سارا رضایی',
rating: 4.5, // rating: 4.5,
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!', // comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر می‌زنم!',
publishedAt: DateTime.now().subtract(const Duration(days: 2)), // publishedAt: DateTime.now().subtract(const Duration(days: 2)),
uploadedImageUrls: [ // uploadedImageUrls: [
'https://picsum.photos/seed/user_img1/200/200', // 'https://picsum.photos/seed/user_img1/200/200',
'https://picsum.photos/seed/user_img2/200/200', // 'https://picsum.photos/seed/user_img2/200/200',
] // ]
), // ),
CommentModel( // CommentModel(
id: 'c2', // id: 'c2',
userName: 'علی اکبری', // userName: 'علی اکبری',
rating: 4, // rating: 4,
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.', // comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
publishedAt: DateTime.now().subtract(const Duration(days: 5)), // publishedAt: DateTime.now().subtract(const Duration(days: 5)),
), // ),
], // ],
qrCodeData: 'PROXIBUY-OFFER-ID-1', // qrCodeData: 'PROXIBUY-OFFER-ID-1',
), // ),
]; // ];
@override // @override
Future<List<OfferModel>> getNearbyOffers() async { // Future<List<OfferModel>> getNearbyOffers() async {
await Future.delayed(const Duration(seconds: 1)); // await Future.delayed(const Duration(seconds: 1));
return _mockOffers; // return _mockOffers;
} // }
@override // @override
Future<OfferModel?> getOfferById(String id) async { // Future<OfferModel?> getOfferById(String id) async {
await Future.delayed(const Duration(milliseconds: 300)); // await Future.delayed(const Duration(milliseconds: 300));
try { // try {
return _mockOffers.firstWhere((offer) => offer.id == id); // return _mockOffers.firstWhere((offer) => offer.id == id);
} catch (e) { // } catch (e) {
return null; // return null;
} // }
} // }
} // }

View File

@ -1,3 +1,5 @@
// lib/data/models/discount_info_model.dart
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class DiscountInfoModel extends Equatable { class DiscountInfoModel extends Equatable {
@ -11,4 +13,11 @@ class DiscountInfoModel extends Equatable {
@override @override
List<Object?> get props => [name, description]; List<Object?> get props => [name, description];
factory DiscountInfoModel.fromJson(Map<String, dynamic> json) {
return DiscountInfoModel(
name: json['name'],
description: json['description'],
);
}
} }

View File

@ -1,8 +1,43 @@
// lib/data/models/offer_model.dart
import 'package:equatable/equatable.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/discount_info_model.dart';
import 'package:proxibuy/data/models/working_hours.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<String> 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<String, dynamic> 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<String>.from(json['Property'] ?? []),
);
}
}
class OfferModel extends Equatable { class OfferModel extends Equatable {
final String id; final String id;
final String storeName; final String storeName;
@ -25,7 +60,7 @@ class OfferModel extends Equatable {
final List<String> features; final List<String> features;
final DiscountInfoModel? discountInfo; final DiscountInfoModel? discountInfo;
final List<CommentModel> comments; final List<CommentModel> comments;
final String qrCodeData; final String qrCodeData;
const OfferModel({ const OfferModel({
required this.id, required this.id,
@ -52,24 +87,96 @@ class OfferModel extends Equatable {
required this.qrCodeData, required this.qrCodeData,
}); });
factory OfferModel.fromJson(Map<String, dynamic> 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<dynamic>?)
?.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 => String get coverImageUrl =>
imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image'; imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
@override @override
List<Object?> get props => [ List<Object?> get props => [id];
id,
title,
storeName,
rating,
ratingCount,
latitude,
longitude,
features,
discountInfo,
comments,
qrCodeData
];
String get distanceAsString { String get distanceAsString {
if (distanceInMeters < 1000) { if (distanceInMeters < 1000) {
return "$distanceInMeters متر"; return "$distanceInMeters متر";

View File

@ -1,3 +1,4 @@
// lib/data/models/working_hours.dart
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -9,6 +10,13 @@ class Shift extends Equatable {
@override @override
List<Object?> get props => [openAt, closeAt]; List<Object?> get props => [openAt, closeAt];
factory Shift.fromJson(Map<String, dynamic> json) {
return Shift(
openAt: json['openAt'],
closeAt: json['closeAt'],
);
}
} }
class WorkingHours extends Equatable { class WorkingHours extends Equatable {
@ -21,4 +29,14 @@ class WorkingHours extends Equatable {
@override @override
List<Object?> get props => [day, shifts]; List<Object?> get props => [day, shifts];
factory WorkingHours.fromJson(Map<String, dynamic> json) {
var shiftsFromJson = json['shifts'] as List;
List<Shift> shiftList = shiftsFromJson.map((s) => Shift.fromJson(s)).toList();
return WorkingHours(
day: json['day'],
shifts: shiftList,
);
}
} }

View File

@ -1,26 +1,26 @@
import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; // import 'package:proxibuy/data/models/datasources/offer_data_source.dart';
import 'package:proxibuy/data/models/offer_model.dart'; // import 'package:proxibuy/data/models/offer_model.dart';
class OfferRepository { // class OfferRepository {
final OfferDataSource _offerDataSource; // final OfferDataSource _offerDataSource;
OfferRepository({required OfferDataSource offerDataSource}) // OfferRepository({required OfferDataSource offerDataSource})
: _offerDataSource = offerDataSource; // : _offerDataSource = offerDataSource;
Future<List<OfferModel>> fetchOffers({required List<String> selectedCategories}) async { // Future<List<OfferModel>> fetchOffers({required List<String> selectedCategories}) async {
final allOffers = await _offerDataSource.getNearbyOffers(); // final allOffers = await _offerDataSource.getNearbyOffers();
if (selectedCategories.isEmpty) { // if (selectedCategories.isEmpty) {
return allOffers; // return allOffers;
} // }
final filteredOffers = allOffers // final filteredOffers = allOffers
.where((offer) => selectedCategories.contains(offer.category)) // .where((offer) => selectedCategories.contains(offer.category))
.toList(); // .toList();
return filteredOffers; // return filteredOffers;
} // }
Future<OfferModel?> fetchOfferById(String id) async { // Future<OfferModel?> fetchOfferById(String id) async {
return _offerDataSource.getOfferById(id); // return _offerDataSource.getOfferById(id);
} // }
} // }

View File

@ -10,14 +10,11 @@ import 'package:proxibuy/firebase_options.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.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/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_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/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 '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 { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -33,20 +30,18 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
RepositoryProvider<MqttService>(
create: (context) => MqttService(),
),
BlocProvider<AuthBloc>( BlocProvider<AuthBloc>(
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()), create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
), ),
RepositoryProvider<OfferRepository>( // RepositoryProvider برای OfferRepository حذف شد
create: (context) =>
OfferRepository(offerDataSource: MockOfferDataSource()),
),
BlocProvider<ReservationCubit>( BlocProvider<ReservationCubit>(
create: (context) => ReservationCubit(), create: (context) => ReservationCubit(),
), ),
BlocProvider<OffersBloc>( BlocProvider<OffersBloc>(
create: (context) => OffersBloc( create: (context) => OffersBloc(),
offerRepository: context.read<OfferRepository>(),
),
), ),
BlocProvider<NotificationPreferencesBloc>( BlocProvider<NotificationPreferencesBloc>(
create: (context) => NotificationPreferencesBloc(), create: (context) => NotificationPreferencesBloc(),
@ -55,7 +50,7 @@ class MyApp extends StatelessWidget {
child: MaterialApp( child: MaterialApp(
title: 'Proxibuy', title: 'Proxibuy',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: const SplashScreen(), // <--- استفاده از صفحه اسپلش home: const SplashScreen(),
localizationsDelegates: const [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
@ -131,36 +126,36 @@ class MyApp extends StatelessWidget {
} }
} }
class AppRouter extends StatelessWidget { // class AppRouter extends StatelessWidget {
const AppRouter({super.key}); // const AppRouter({super.key});
@override // @override
Widget build(BuildContext context) { // Widget build(BuildContext context) {
final authState = context.select((AuthBloc bloc) => bloc.state); // final authState = context.select((AuthBloc bloc) => bloc.state);
if (authState is AuthCodeSentSuccess) { // if (authState is AuthCodeSentSuccess) {
return OtpPage( // return OtpPage(
phoneNumber: "+${authState.countryCode}${authState.phone}", // phoneNumber: "+${authState.countryCode}${authState.phone}",
phone: authState.phone, // phone: authState.phone,
countryCode: authState.countryCode, // countryCode: authState.countryCode,
); // );
} // }
if (authState is AuthLoading) { // if (authState is AuthLoading) {
final currentState = context.read<AuthBloc>().state; // final currentState = context.read<AuthBloc>().state;
if (currentState is! AuthCodeSentSuccess) { // if (currentState is! AuthCodeSentSuccess) {
return const Scaffold(body: Center(child: CircularProgressIndicator())); // return const Scaffold(body: Center(child: CircularProgressIndicator()));
} // }
} // }
if (authState is AuthSuccess) { // if (authState is AuthSuccess) {
return const OffersPage(); // return const OffersPage();
} // }
if (authState is AuthNeedsInfo) { // if (authState is AuthNeedsInfo) {
return const UserInfoPage(); // return const UserInfoPage();
} // }
return const OnboardingPage(); // return const OnboardingPage();
} // }
} // }

View File

@ -1,3 +1,5 @@
// lib/presentation/auth/bloc/auth_bloc.dart
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -74,8 +76,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final accessToken = response.data['data']['accessToken']; final accessToken = response.data['data']['accessToken'];
final refreshToken = response.data['data']['refreshToken']; final refreshToken = response.data['data']['refreshToken'];
final userID = response.data['data']['ID']; // <-- خط جدید: استخراج ID
await _storage.write(key: 'accessToken', value: accessToken); await _storage.write(key: 'accessToken', value: accessToken);
await _storage.write(key: 'refreshToken', value: refreshToken); await _storage.write(key: 'refreshToken', value: refreshToken);
await _storage.write(key: 'userID', value: userID); // <-- خط جدید: ذخیره ID
emit(AuthNeedsInfo()); emit(AuthNeedsInfo());
} else { } else {
emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست')); emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست'));

View File

@ -16,6 +16,7 @@ class NotificationPreferencesBloc
on<LoadCategories>(_onLoadCategories); on<LoadCategories>(_onLoadCategories);
on<ToggleCategorySelection>(_onToggleCategorySelection); on<ToggleCategorySelection>(_onToggleCategorySelection);
on<SubmitPreferences>(_onSubmitPreferences); on<SubmitPreferences>(_onSubmitPreferences);
on<LoadFavoriteCategories>(_onLoadFavoriteCategories); // این خط اضافه شد
add(LoadCategories()); add(LoadCategories());
} }
@ -77,4 +78,40 @@ class NotificationPreferencesBloc
errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
} }
} }
// این متد اضافه شد
Future<void> _onLoadFavoriteCategories(
LoadFavoriteCategories event, Emitter<NotificationPreferencesState> 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<dynamic> fCategory = response.data['data']['FCategory'];
final Set<String> 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'] ?? 'خطا در ارتباط با سرور'));
}
}
} }

View File

@ -9,6 +9,8 @@ abstract class NotificationPreferencesEvent extends Equatable {
class LoadCategories extends NotificationPreferencesEvent {} class LoadCategories extends NotificationPreferencesEvent {}
class LoadFavoriteCategories extends NotificationPreferencesEvent {} // این کلاس اضافه شد
class ToggleCategorySelection extends NotificationPreferencesEvent { class ToggleCategorySelection extends NotificationPreferencesEvent {
final String categoryId; final String categoryId;

View File

@ -1,31 +1,41 @@
// ignore: depend_on_referenced_packages // lib/presentation/offer/bloc/offer_bloc.dart
import 'package:bloc/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_event.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_state.dart';
class OffersBloc extends Bloc<OffersEvent, OffersState> { class OffersBloc extends Bloc<OffersEvent, OffersState> {
final OfferRepository _offerRepository; OffersBloc() : super(OffersInitial()) {
on<OffersReceivedFromMqtt>(_onOffersReceivedFromMqtt);
OffersBloc({required OfferRepository offerRepository}) on<ClearOffers>(_onClearOffers); // رویداد جدید برای پاک کردن دیتا
: _offerRepository = offerRepository,
super(OffersInitial()) {
on<OffersFetchRequested>(_onFetchRequested);
} }
Future<void> _onFetchRequested( void _onOffersReceivedFromMqtt(
OffersFetchRequested event, OffersReceivedFromMqtt event,
Emitter<OffersState> emit, Emitter<OffersState> emit,
) async { ) {
emit(OffersLoadInProgress()); // فقط در صورتی که لیست جدید خالی نباشد، آن را جایگزین کن
try { if (event.offers.isNotEmpty) {
final offers = await _offerRepository.fetchOffers( emit(OffersLoadSuccess(event.offers));
selectedCategories: event.selectedCategories, }
); // اگر لیست جدید خالی بود، و قبلا دیتایی داشتیم، حالت را تغییر نده
emit(OffersLoadSuccess(offers)); // این کار از نمایش صفحه خالی جلوگیری میکند
} catch (e) { else if (state is! OffersLoadSuccess) {
emit(OffersLoadFailure(e.toString())); // اگر اولین بار است و لیست خالی است، حالت موفقیت با لیست خالی را نشان بده
emit(const OffersLoadSuccess([]));
} }
} }
}
// برای زمانی که مثلا کاربر GPS را خاموش میکند
void _onClearOffers(ClearOffers event, Emitter<OffersState> emit) {
emit(OffersInitial());
}
}
// مدیریت رویداد جدید
void _onOffersReceivedFromMqtt(
OffersReceivedFromMqtt event,
Emitter<OffersState> emit,
) {
// جایگزین کردن لیست پیشنهادها با دادههای جدید
emit(OffersLoadSuccess(event.offers));
}

View File

@ -1,5 +1,7 @@
// lib/presentation/offer/bloc/offer_event.dart
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:proxibuy/data/models/offer_model.dart';
abstract class OffersEvent extends Equatable { abstract class OffersEvent extends Equatable {
const OffersEvent(); const OffersEvent();
@ -15,4 +17,15 @@ class OffersFetchRequested extends OffersEvent {
@override @override
List<Object> get props => [selectedCategories]; List<Object> get props => [selectedCategories];
} }
class OffersReceivedFromMqtt extends OffersEvent {
final List<OfferModel> offers;
const OffersReceivedFromMqtt(this.offers);
@override
List<Object> get props => [offers];
}
class ClearOffers extends OffersEvent {} // این کلاس را اضافه کنید

View File

@ -22,13 +22,13 @@ class CategoryOffersRow extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text( child: Text(
categoryTitle, categoryTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith( style: Theme.of(
fontWeight: FontWeight.bold, context,
), ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
), ),
), ),
SizedBox( SizedBox(
height: 300, height: 300,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
@ -39,12 +39,13 @@ class CategoryOffersRow extends StatelessWidget {
padding: const EdgeInsets.only(left: 12.0), padding: const EdgeInsets.only(left: 12.0),
child: OfferCard( child: OfferCard(
offer: offer, offer: offer,
width: 320, width: 320,
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) { 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),
], ],
); );
} }
} }

View File

@ -11,18 +11,38 @@ import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/category_selection_card.dart'; import 'package:proxibuy/presentation/widgets/category_selection_card.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class NotificationPreferencesPage extends StatelessWidget { class NotificationPreferencesPage extends StatefulWidget {
const NotificationPreferencesPage({super.key}); // This parameter is used to decide whether to fetch favorite categories on start
final bool loadFavoritesOnStart;
static Route<void> route() { // The constructor now accepts the 'loadFavoritesOnStart' parameter
return MaterialPageRoute<void>( const NotificationPreferencesPage({super.key, this.loadFavoritesOnStart = false});
static Route<bool> route({bool loadFavorites = false}) {
return MaterialPageRoute<bool>(
builder: (_) => BlocProvider( builder: (_) => BlocProvider(
create: (context) => NotificationPreferencesBloc(), create: (context) => NotificationPreferencesBloc(),
child: const NotificationPreferencesPage(), // The widget is created here, passing the parameter correctly
child: NotificationPreferencesPage(loadFavoritesOnStart: loadFavorites),
), ),
); );
} }
@override
State<NotificationPreferencesPage> createState() =>
_NotificationPreferencesPageState();
}
class _NotificationPreferencesPageState extends State<NotificationPreferencesPage> {
@override
void initState() {
super.initState();
// If the flag is true, dispatch the event to load favorites from the API
if (widget.loadFavoritesOnStart) {
context.read<NotificationPreferencesBloc>().add(LoadFavoriteCategories());
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -100,6 +120,7 @@ class NotificationPreferencesPage extends StatelessWidget {
body: BlocListener<NotificationPreferencesBloc, NotificationPreferencesState>( body: BlocListener<NotificationPreferencesBloc, NotificationPreferencesState>(
listener: (context, state) { listener: (context, state) {
if (state.submissionSuccess) { if (state.submissionSuccess) {
// Pop the page and return 'true' to signal a successful update
if (Navigator.canPop(context)) { if (Navigator.canPop(context)) {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
} else { } else {

View File

@ -1,7 +1,11 @@
// lib/presentation/pages/offers_page.dart
import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.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:flutter_svg/svg.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:proxibuy/core/config/app_colors.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/notification_preferences_page.dart';
import 'package:proxibuy/presentation/pages/reserved_list_page.dart'; import 'package:proxibuy/presentation/pages/reserved_list_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/gps_dialog.dart'; import 'package:proxibuy/services/mqtt_service.dart';
import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class OffersPage extends StatefulWidget { class OffersPage extends StatefulWidget {
// این پارامتر دیگر استفاده نمیشود اما برای سازگاری باقی میماند
final bool showDialogsOnLoad; final bool showDialogsOnLoad;
const OffersPage({super.key, this.showDialogsOnLoad = false}); const OffersPage({super.key, this.showDialogsOnLoad = false});
@ -29,25 +33,155 @@ class OffersPage extends StatefulWidget {
class _OffersPageState extends State<OffersPage> { class _OffersPageState extends State<OffersPage> {
List<String> _selectedCategories = []; List<String> _selectedCategories = [];
StreamSubscription? _locationServiceSubscription;
StreamSubscription? _mqttMessageSubscription;
Timer? _locationTimer;
bool _isSubscribedToOffers = false;
bool _isGpsEnabled = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadOffersAndPreferences(); _initializePage();
}
if (widget.showDialogsOnLoad) { Future<void> _initializePage() async {
WidgetsBinding.instance.addPostFrameCallback((_) async { await _loadPreferences();
if (mounted) { _subscribeToUserOffersOnLoad();
await showGPSDialog(context); _initLocationListener();
} }
if (mounted) {
await showNotificationPermissionDialog(context); @override
} void dispose() {
}); _locationServiceSubscription?.cancel();
_mqttMessageSubscription?.cancel();
_locationTimer?.cancel();
super.dispose();
}
Future<void> _subscribeToUserOffersOnLoad() async {
final storage = const FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID != null && mounted) {
_subscribeToUserOffers(userID);
} }
} }
Future<void> _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<OffersBloc>().add(ClearOffers());
}
});
}
Future<void> _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<void> _sendLocationUpdate() async {
final mqttService = context.read<MqttService>();
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<MqttService>();
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<OfferModel> offers = data
.whereType<Map<String, dynamic>>()
.map((json) => OfferModel.fromJson(json))
.toList();
if (mounted) {
context.read<OffersBloc>().add(OffersReceivedFromMqtt(offers));
}
} catch (e, stackTrace) {
print("❌ Error parsing offers from MQTT: $e");
print(stackTrace);
}
}
});
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final savedCategories = final savedCategories =
prefs.getStringList('user_selected_categories') ?? []; prefs.getStringList('user_selected_categories') ?? [];
@ -56,9 +190,6 @@ class _OffersPageState extends State<OffersPage> {
setState(() { setState(() {
_selectedCategories = savedCategories; _selectedCategories = savedCategories;
}); });
context.read<OffersBloc>().add(
OffersFetchRequested(selectedCategories: savedCategories),
);
} }
} }
@ -78,13 +209,11 @@ class _OffersPageState extends State<OffersPage> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final result = await Navigator.of(context).push<bool>( final result = await Navigator.of(context).push<bool>(
MaterialPageRoute( NotificationPreferencesPage.route(loadFavorites: true),
builder: (_) => const NotificationPreferencesPage(),
),
); );
if (result == true && mounted) { if (result == true && mounted) {
_loadOffersAndPreferences(); _loadPreferences();
} }
}, },
child: Row( child: Row(
@ -219,7 +348,7 @@ class _OffersPageState extends State<OffersPage> {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildFavoriteCategoriesSection(), const OffersView()], children: [_buildFavoriteCategoriesSection(), OffersView(isGpsEnabled: _isGpsEnabled)],
), ),
), ),
), ),
@ -228,58 +357,45 @@ class _OffersPageState extends State<OffersPage> {
} }
class OffersView extends StatelessWidget { class OffersView extends StatelessWidget {
const OffersView({super.key}); final bool isGpsEnabled;
const OffersView({super.key, required this.isGpsEnabled});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<OffersBloc, OffersState>( return BlocBuilder<OffersBloc, OffersState>(
builder: (context, state) { builder: (context, state) {
if (state is OffersLoadInProgress || state is OffersInitial) { if (!isGpsEnabled) {
return _buildGpsActivationUI(context);
}
if (state is OffersInitial) {
return const SizedBox( return const SizedBox(
height: 300, 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 is OffersLoadSuccess) {
if (state.offers.isEmpty) { if (state.offers.isEmpty) {
return Center( return const SizedBox(
child: SizedBox( height: 300,
child: Center( child: Center(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: 85), Text("فعلاً تخفیفی در این اطراف نیست!"),
SvgPicture.asset(Assets.images.emptyHome.path), Text("کمی قدم بزنید..."),
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('جست‌وجوی تصادفی'),
],
),
), ),
), ),
); );
@ -310,14 +426,60 @@ class OffersView extends StatelessWidget {
}, },
); );
} }
if (state is OffersLoadFailure) { if (state is OffersLoadFailure) {
return SizedBox( return SizedBox(
height: 200, height: 200,
child: Center(child: Text("خطا در بارگذاری: ${state.error}")), child: Center(child: Text("خطا در بارگذاری: ${state.error}")),
); );
} }
return const SizedBox.shrink(); 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('جست‌وجوی تصادفی'),
],
),
),
),
);
}
}

View File

@ -1,8 +1,10 @@
// lib/presentation/pages/otp_page.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; // import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; // حذف شد
import 'package:proxibuy/data/repositories/offer_repository.dart'; // import 'package:proxibuy/data/repositories/offer_repository.dart'; // حذف شد
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.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/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
@ -152,9 +154,8 @@ class _OtpPageState extends State<OtpPage> {
}); });
} }
if (state is AuthNeedsInfo) { if (state is AuthNeedsInfo) {
final offerRepository = OfferRepository( // **تغییر اصلی در این قسمت است**
offerDataSource: MockOfferDataSource(), // دیگر نیازی به ساخت OfferRepository نیست
);
Navigator.of(context).pushAndRemoveUntil( Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute( MaterialPageRoute(
builder: builder:
@ -163,11 +164,9 @@ class _OtpPageState extends State<OtpPage> {
BlocProvider.value( BlocProvider.value(
value: context.read<AuthBloc>(), value: context.read<AuthBloc>(),
), ),
// OffersBloc دیگر به ریپازیتوری نیاز ندارد
BlocProvider<OffersBloc>( BlocProvider<OffersBloc>(
create: create: (_) => OffersBloc(),
(_) => OffersBloc(
offerRepository: offerRepository,
),
), ),
BlocProvider<ReservationCubit>( BlocProvider<ReservationCubit>(
create: (_) => ReservationCubit(), create: (_) => ReservationCubit(),
@ -343,4 +342,4 @@ class _OtpPageState extends State<OtpPage> {
); );
_otpTimer.resetTimer(); _otpTimer.resetTimer();
} }
} }

View File

@ -17,91 +17,65 @@ import 'package:proxibuy/presentation/widgets/comments_section.dart';
import 'package:slide_countdown/slide_countdown.dart'; import 'package:slide_countdown/slide_countdown.dart';
class ProductDetailPage extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return Scaffold(
create: body: Stack(
(context) => ProductDetailBloc( children: [
offerRepository: context.read<OfferRepository>(), // ویجت نمایش جزئیات مستقیما ساخته میشود
)..add(ProductDetailFetchRequested(offerId: offerId)), ProductDetailView(offer: offer)
child: Scaffold( .animate()
body: Stack( .fadeIn(duration: 400.ms, curve: Curves.easeOut)
children: [ .slideY(
BlocBuilder<ProductDetailBloc, ProductDetailState>( begin: 0.2,
builder: (context, state) { duration: 400.ms,
if (state is ProductDetailLoadInProgress || curve: Curves.easeOut,
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<ProductDetailBloc, ProductDetailState>(
builder: (context, state) {
if (state is ProductDetailLoadSuccess) {
return ElevatedButton(
onPressed: () {
context.read<ReservationCubit>().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();
},
), ),
), Positioned(
], bottom: 30,
), left: 24,
right: 24,
// BlocBuilder حذف شد
child: ElevatedButton(
onPressed: () {
context.read<ReservationCubit>().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),
),
],
), ),
); );
} }

View File

@ -5,6 +5,8 @@ import 'package:flutter_svg/svg.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/data/repositories/offer_repository.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/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/reserved_list_item_card.dart'; import 'package:proxibuy/presentation/widgets/reserved_list_item_card.dart';
@ -17,20 +19,30 @@ class ReservedListPage extends StatefulWidget {
class _ReservedListPageState extends State<ReservedListPage> { class _ReservedListPageState extends State<ReservedListPage> {
late final List<String> _reservedIds; late final List<String> _reservedIds;
Future<List<OfferModel?>>? _reservedOffersFuture; // دیگر نیازی به Future نیست
List<OfferModel> _reservedOffers = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_reservedIds = context.read<ReservationCubit>().state.reservedProductIds; _reservedIds = context.read<ReservationCubit>().state.reservedProductIds;
_reservedOffersFuture = _fetchReservedOffers(); // اطلاعات مستقیما از BLoC خوانده میشود
_fetchReservedOffersFromBloc();
} }
Future<List<OfferModel?>> _fetchReservedOffers() { void _fetchReservedOffersFromBloc() {
final offerRepo = context.read<OfferRepository>(); final offersState = context.read<OffersBloc>().state;
final offerFutures = // بررسی میکند که آیا پیشنهادها قبلا بارگذاری شدهاند یا خیر
_reservedIds.map((id) => offerRepo.fetchOfferById(id)).toList(); if (offersState is OffersLoadSuccess) {
return Future.wait(offerFutures); final allOffers = offersState.offers;
if (mounted) {
setState(() {
_reservedOffers = allOffers
.where((offer) => _reservedIds.contains(offer.id))
.toList();
});
}
}
} }
@override @override
@ -39,40 +51,17 @@ class _ReservedListPageState extends State<ReservedListPage> {
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: Scaffold( child: Scaffold(
appBar: _buildCustomAppBar(context), appBar: _buildCustomAppBar(context),
body: FutureBuilder<List<OfferModel?>>( // FutureBuilder با یک ویجت ساده جایگزین شد
future: _reservedOffersFuture, body: _reservedOffers.isEmpty
builder: (context, snapshot) { ? const Center(
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<OfferModel>().toList();
if (reservedOffers.isEmpty) {
return const Center(
child: Text('هیچ آیتم رزرو شده‌ای یافت نشد.'), child: Text('هیچ آیتم رزرو شده‌ای یافت نشد.'),
); )
} : ListView.builder(
padding: const EdgeInsets.all(16.0),
return ListView.builder( itemCount: _reservedOffers.length,
padding: const EdgeInsets.all(16.0), itemBuilder: (context, index) {
itemCount: reservedOffers.length, final offer = _reservedOffers[index];
itemBuilder: (context, index) { return Padding(
final offer = reservedOffers[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.only(bottom: 16.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -101,11 +90,9 @@ class _ReservedListPageState extends State<ReservedListPage> {
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
); );
}, },
); )
}, ) ); }
),
),
);
} }
PreferredSizeWidget _buildCustomAppBar(BuildContext context) { PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
@ -160,4 +147,3 @@ class _ReservedListPageState extends State<ReservedListPage> {
), ),
); );
} }
}

View File

@ -1,10 +1,13 @@
// lib/presentation/pages/splash_screen.dart
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/onboarding_page.dart';
import 'package:proxibuy/presentation/pages/offers_page.dart'; import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/services/mqtt_service.dart';
class SplashScreen extends StatefulWidget { class SplashScreen extends StatefulWidget {
const SplashScreen({super.key}); const SplashScreen({super.key});
@ -14,31 +17,57 @@ class SplashScreen extends StatefulWidget {
} }
class _SplashScreenState extends State<SplashScreen> { class _SplashScreenState extends State<SplashScreen> {
late final StreamSubscription _authSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final authBloc = context.read<AuthBloc>(); // با کمی تاخیر برای نمایش لوگو، فرآیند را شروع میکنیم
Timer(const Duration(seconds: 2), _checkAuthAndNavigate);
}
_authSubscription = authBloc.stream.listen((state) { Future<void> _checkAuthAndNavigate() async {
_authSubscription.cancel(); final storage = const FlutterSecureStorage();
if (state is AuthSuccess) { final token = await storage.read(key: 'accessToken');
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const OffersPage()), if (token != null && token.isNotEmpty) {
); // کاربر احراز هویت شده است
} else { debugPrint("--- SplashScreen: User is authenticated. Connecting to MQTT...");
try {
final mqttService = context.read<MqttService>();
// ۱. منتظر میمانیم تا اتصال کامل برقرار شود
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( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const OnboardingPage()), MaterialPageRoute(builder: (_) => const OnboardingPage()),
); );
} }
}); }
}
@override
void dispose() {
_authSubscription.cancel();
super.dispose();
} }
@override @override

View File

@ -1,33 +1,33 @@
// ignore: depend_on_referenced_packages // // ignore: depend_on_referenced_packages
import 'package:bloc/bloc.dart'; // import 'package:bloc/bloc.dart';
import 'package:proxibuy/data/repositories/offer_repository.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_event.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart'; // import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
class ProductDetailBloc extends Bloc<ProductDetailEvent, ProductDetailState> { // class ProductDetailBloc extends Bloc<ProductDetailEvent, ProductDetailState> {
final OfferRepository _offerRepository; // final OfferRepository _offerRepository;
ProductDetailBloc({required OfferRepository offerRepository}) // ProductDetailBloc({required OfferRepository offerRepository})
: _offerRepository = offerRepository, // : _offerRepository = offerRepository,
super(ProductDetailInitial()) { // super(ProductDetailInitial()) {
on<ProductDetailFetchRequested>(_onFetchRequested); // on<ProductDetailFetchRequested>(_onFetchRequested);
} // }
Future<void> _onFetchRequested( // Future<void> _onFetchRequested(
ProductDetailFetchRequested event, // ProductDetailFetchRequested event,
Emitter<ProductDetailState> emit, // Emitter<ProductDetailState> emit,
) async { // ) async {
emit(ProductDetailLoadInProgress()); // emit(ProductDetailLoadInProgress());
try { // try {
final offer = await _offerRepository.fetchOfferById(event.offerId); // final offer = await _offerRepository.fetchOfferById(event.offerId);
if (offer != null) { // if (offer != null) {
emit(ProductDetailLoadSuccess(offer)); // emit(ProductDetailLoadSuccess(offer));
} else { // } else {
emit(const ProductDetailLoadFailure('محصول مورد نظر یافت نشد.')); // emit(const ProductDetailLoadFailure('محصول مورد نظر یافت نشد.'));
} // }
} catch (e) { // } catch (e) {
emit(ProductDetailLoadFailure(e.toString())); // emit(ProductDetailLoadFailure(e.toString()));
} // }
} // }
} // }

View File

@ -1,60 +1,123 @@
// import 'dart:async'; // lib/services/mqtt_service.dart
// import 'dart:math';
// import 'package:mqtt_client/mqtt_client.dart';
// import 'package:mqtt_client/mqtt_server_client.dart';
// class MqttService { import 'dart:async';
// late MqttServerClient client; import 'dart:convert';
// final String server = '5.75.200.241'; import 'dart:io';
// final int port = 1883; import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
// Future<void> connect(String token) async { class MqttService {
// // 1. معادلسازی پارامترها late MqttServerClient client;
// final String clientId = 'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0'); final String server = '5.75.200.241';
// final String username = 'ignored'; final int port = 1883;
// final String password = token; // توکن شما مستقیماً به عنوان پسورد در نظر گرفته میشود final StreamController<Map<String, dynamic>> _messageStreamController =
StreamController.broadcast();
// // 2. ساخت کلاینت Stream<Map<String, dynamic>> get messages => _messageStreamController.stream;
// client = MqttServerClient.withPort(server, clientId, port);
// client.logging(on: true);
// client.keepAlivePeriod = 60;
// client.autoReconnect = false; // معادل reconnectPeriod: 0
// client.setProtocolV311();
// // 3. ساخت پیام اتصال با پارامترهای تعریف شده bool get isConnected => client.connectionStatus?.state == MqttConnectionState.connected;
// final connMessage = MqttConnectMessage()
// .withClientIdentifier(clientId)
// .startClean()
// .authenticateAs(username, password); // ارسال نام کاربری و رمز عبور (توکن)
// client.connectionMessage = connMessage; Future<void> 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 = MqttServerClient.withPort(server, clientId, port);
// client.onConnected = () { client.logging(on: true);
// print('✅ MQTT Connected'); client.keepAlivePeriod = 60;
// client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) { client.autoReconnect = false;
// final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage; client.setProtocolV311();
// final String payload =
// MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
// print('Received message: "$payload" from topic: ${c[0].topic}');
// });
// client.subscribe('test/topic', MqttQos.atLeastOnce); debugPrint('--- [MQTT] Attempting to connect...');
// }; debugPrint('--- [MQTT] Server: $server:$port');
debugPrint('--- [MQTT] ClientID: $clientId');
// client.onDisconnected = () { final connMessage = MqttConnectMessage()
// print('❌ MQTT Disconnected'); .withClientIdentifier(clientId)
// }; .startClean()
.authenticateAs(username, password);
// client.onSubscribed = (String topic) { client.connectionMessage = connMessage;
// print('✅ Subscribed to $topic');
// };
// try { client.onConnected = () {
// await client.connect(); debugPrint('✅ [MQTT] Connected successfully.');
// } catch (e) { client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) {
// print('Exception: $e'); final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage;
// client.disconnect();
// } 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<String, dynamic> 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<String, dynamic> 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();
}
}