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.READ_EXTERNAL_STORAGE"/>
<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
android:label="Proxibuy"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- android:networkSecurityConfig="@xml/network_security_config"> -->
<activity
android:name=".MainActivity"
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

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

View File

@ -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<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/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<List<OfferModel>> getNearbyOffers();
Future<OfferModel?> getOfferById(String id);
}
// abstract class OfferDataSource {
// Future<List<OfferModel>> getNearbyOffers();
// Future<OfferModel?> getOfferById(String id);
// }
class MockOfferDataSource implements OfferDataSource {
final List<OfferModel> _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<OfferModel> _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<List<OfferModel>> getNearbyOffers() async {
await Future.delayed(const Duration(seconds: 1));
return _mockOffers;
}
// @override
// Future<List<OfferModel>> getNearbyOffers() async {
// await Future.delayed(const Duration(seconds: 1));
// return _mockOffers;
// }
@override
Future<OfferModel?> 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<OfferModel?> getOfferById(String id) async {
// await Future.delayed(const Duration(milliseconds: 300));
// try {
// return _mockOffers.firstWhere((offer) => offer.id == id);
// } catch (e) {
// return null;
// }
// }
// }

View File

@ -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<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: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<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 {
final String id;
final String storeName;
@ -52,23 +87,95 @@ class OfferModel extends Equatable {
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 =>
imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
@override
List<Object?> get props => [
id,
title,
storeName,
rating,
ratingCount,
latitude,
longitude,
features,
discountInfo,
comments,
qrCodeData
];
List<Object?> get props => [id];
String get distanceAsString {
if (distanceInMeters < 1000) {

View File

@ -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<Object?> get props => [openAt, closeAt];
factory Shift.fromJson(Map<String, dynamic> json) {
return Shift(
openAt: json['openAt'],
closeAt: json['closeAt'],
);
}
}
class WorkingHours extends Equatable {
@ -21,4 +29,14 @@ class WorkingHours extends Equatable {
@override
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/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<List<OfferModel>> fetchOffers({required List<String> selectedCategories}) async {
final allOffers = await _offerDataSource.getNearbyOffers();
// Future<List<OfferModel>> fetchOffers({required List<String> 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<OfferModel?> fetchOfferById(String id) async {
return _offerDataSource.getOfferById(id);
}
}
// return filteredOffers;
// }
// Future<OfferModel?> fetchOfferById(String id) async {
// 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/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<MqttService>(
create: (context) => MqttService(),
),
BlocProvider<AuthBloc>(
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
),
RepositoryProvider<OfferRepository>(
create: (context) =>
OfferRepository(offerDataSource: MockOfferDataSource()),
),
// RepositoryProvider برای OfferRepository حذف شد
BlocProvider<ReservationCubit>(
create: (context) => ReservationCubit(),
),
BlocProvider<OffersBloc>(
create: (context) => OffersBloc(
offerRepository: context.read<OfferRepository>(),
),
create: (context) => OffersBloc(),
),
BlocProvider<NotificationPreferencesBloc>(
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<AuthBloc>().state;
if (currentState is! AuthCodeSentSuccess) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}
// if (authState is AuthLoading) {
// final currentState = context.read<AuthBloc>().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();
// }
// }

View File

@ -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<AuthEvent, AuthState> {
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'] ?? 'کد صحیح نیست'));

View File

@ -16,6 +16,7 @@ class NotificationPreferencesBloc
on<LoadCategories>(_onLoadCategories);
on<ToggleCategorySelection>(_onToggleCategorySelection);
on<SubmitPreferences>(_onSubmitPreferences);
on<LoadFavoriteCategories>(_onLoadFavoriteCategories); // این خط اضافه شد
add(LoadCategories());
}
@ -77,4 +78,40 @@ class NotificationPreferencesBloc
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 LoadFavoriteCategories extends NotificationPreferencesEvent {} // این کلاس اضافه شد
class ToggleCategorySelection extends NotificationPreferencesEvent {
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: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<OffersEvent, OffersState> {
final OfferRepository _offerRepository;
OffersBloc({required OfferRepository offerRepository})
: _offerRepository = offerRepository,
super(OffersInitial()) {
on<OffersFetchRequested>(_onFetchRequested);
OffersBloc() : super(OffersInitial()) {
on<OffersReceivedFromMqtt>(_onOffersReceivedFromMqtt);
on<ClearOffers>(_onClearOffers); // رویداد جدید برای پاک کردن دیتا
}
Future<void> _onFetchRequested(
OffersFetchRequested event,
void _onOffersReceivedFromMqtt(
OffersReceivedFromMqtt event,
Emitter<OffersState> 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([]));
}
}
// برای زمانی که مثلا کاربر 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:proxibuy/data/models/offer_model.dart';
abstract class OffersEvent extends Equatable {
const OffersEvent();
@ -16,3 +18,14 @@ class OffersFetchRequested extends OffersEvent {
@override
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,9 +22,9 @@ 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(
@ -44,7 +44,8 @@ class CategoryOffersRow extends StatelessWidget {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) {
return ProductDetailPage(offerId: offer.id,);
// کل آبجکت offer پاس داده میشود
return ProductDetailPage(offer: offer);
},
),
);

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: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<void> route() {
return MaterialPageRoute<void>(
// The constructor now accepts the 'loadFavoritesOnStart' parameter
const NotificationPreferencesPage({super.key, this.loadFavoritesOnStart = false});
static Route<bool> route({bool loadFavorites = false}) {
return MaterialPageRoute<bool>(
builder: (_) => BlocProvider(
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
Widget build(BuildContext context) {
return Scaffold(
@ -100,6 +120,7 @@ class NotificationPreferencesPage extends StatelessWidget {
body: BlocListener<NotificationPreferencesBloc, NotificationPreferencesState>(
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 {

View File

@ -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<OffersPage> {
List<String> _selectedCategories = [];
StreamSubscription? _locationServiceSubscription;
StreamSubscription? _mqttMessageSubscription;
Timer? _locationTimer;
bool _isSubscribedToOffers = false;
bool _isGpsEnabled = false;
@override
void initState() {
super.initState();
_loadOffersAndPreferences();
if (widget.showDialogsOnLoad) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) {
await showGPSDialog(context);
_initializePage();
}
if (mounted) {
await showNotificationPermissionDialog(context);
Future<void> _initializePage() async {
await _loadPreferences();
_subscribeToUserOffersOnLoad();
_initLocationListener();
}
@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);
}
}
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();
}
}
}
Future<void> _loadOffersAndPreferences() async {
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 savedCategories =
prefs.getStringList('user_selected_categories') ?? [];
@ -56,9 +190,6 @@ class _OffersPageState extends State<OffersPage> {
setState(() {
_selectedCategories = savedCategories;
});
context.read<OffersBloc>().add(
OffersFetchRequested(selectedCategories: savedCategories),
);
}
}
@ -78,13 +209,11 @@ class _OffersPageState extends State<OffersPage> {
TextButton(
onPressed: () async {
final result = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => const NotificationPreferencesPage(),
),
NotificationPreferencesPage.route(loadFavorites: true),
);
if (result == true && mounted) {
_loadOffersAndPreferences();
_loadPreferences();
}
},
child: Row(
@ -219,7 +348,7 @@ class _OffersPageState extends State<OffersPage> {
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildFavoriteCategoriesSection(), const OffersView()],
children: [_buildFavoriteCategoriesSection(), OffersView(isGpsEnabled: _isGpsEnabled)],
),
),
),
@ -228,20 +357,89 @@ class _OffersPageState extends State<OffersPage> {
}
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<OffersBloc, OffersState>(
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 const SizedBox(
height: 300,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("فعلاً تخفیفی در این اطراف نیست!"),
Text("کمی قدم بزنید..."),
],
),
),
);
}
final groupedOffers = groupBy(
state.offers,
(OfferModel offer) => offer.category,
);
final categories = groupedOffers.keys.toList();
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 16),
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final offersForCategory = groupedOffers[category]!;
return CategoryOffersRow(
categoryTitle: category,
offers: offersForCategory,
)
.animate()
.fade(duration: 500.ms)
.slideY(begin: 0.3, duration: 400.ms, curve: Curves.easeOut);
},
);
}
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(
@ -284,40 +482,4 @@ class OffersView extends StatelessWidget {
),
);
}
final groupedOffers = groupBy(
state.offers,
(OfferModel offer) => offer.category,
);
final categories = groupedOffers.keys.toList();
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 16),
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final offersForCategory = groupedOffers[category]!;
return CategoryOffersRow(
categoryTitle: category,
offers: offersForCategory,
)
.animate()
.fade(duration: 500.ms)
.slideY(begin: 0.3, duration: 400.ms, curve: Curves.easeOut);
},
);
}
if (state is OffersLoadFailure) {
return SizedBox(
height: 200,
child: Center(child: Text("خطا در بارگذاری: ${state.error}")),
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@ -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<OtpPage> {
});
}
if (state is AuthNeedsInfo) {
final offerRepository = OfferRepository(
offerDataSource: MockOfferDataSource(),
);
// **تغییر اصلی در این قسمت است**
// دیگر نیازی به ساخت OfferRepository نیست
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder:
@ -163,11 +164,9 @@ class _OtpPageState extends State<OtpPage> {
BlocProvider.value(
value: context.read<AuthBloc>(),
),
// OffersBloc دیگر به ریپازیتوری نیاز ندارد
BlocProvider<OffersBloc>(
create:
(_) => OffersBloc(
offerRepository: offerRepository,
),
create: (_) => OffersBloc(),
),
BlocProvider<ReservationCubit>(
create: (_) => ReservationCubit(),

View File

@ -17,55 +17,36 @@ 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<OfferRepository>(),
)..add(ProductDetailFetchRequested(offerId: offerId)),
child: Scaffold(
return Scaffold(
body: Stack(
children: [
BlocBuilder<ProductDetailBloc, ProductDetailState>(
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)
// ویجت نمایش جزئیات مستقیما ساخته میشود
ProductDetailView(offer: 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(
// BlocBuilder حذف شد
child: ElevatedButton(
onPressed: () {
context.read<ReservationCubit>().reserveProduct(state.offer.id);
context.read<ReservationCubit>().reserveProduct(offer.id);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ReservationConfirmationPage(offer: state.offer),
builder: (_) =>
ReservationConfirmationPage(offer: offer),
),
);
},
@ -80,7 +61,7 @@ class ProductDetailPage extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(Assets.icons.receiptDisscount.path,),
SvgPicture.asset(Assets.icons.receiptDisscount.path),
const SizedBox(width: 12),
const Text(
'رزرو تخفیف',
@ -92,17 +73,10 @@ class ProductDetailPage extends StatelessWidget {
),
],
),
).animate()
.fadeIn(delay: 200.ms, duration: 400.ms, curve: Curves.easeOut)
.slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut);
}
return const SizedBox.shrink();
},
),
).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/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<ReservedListPage> {
late final List<String> _reservedIds;
Future<List<OfferModel?>>? _reservedOffersFuture;
// دیگر نیازی به Future نیست
List<OfferModel> _reservedOffers = [];
@override
void initState() {
super.initState();
_reservedIds = context.read<ReservationCubit>().state.reservedProductIds;
_reservedOffersFuture = _fetchReservedOffers();
// اطلاعات مستقیما از BLoC خوانده میشود
_fetchReservedOffersFromBloc();
}
Future<List<OfferModel?>> _fetchReservedOffers() {
final offerRepo = context.read<OfferRepository>();
final offerFutures =
_reservedIds.map((id) => offerRepo.fetchOfferById(id)).toList();
return Future.wait(offerFutures);
void _fetchReservedOffersFromBloc() {
final offersState = context.read<OffersBloc>().state;
// بررسی میکند که آیا پیشنهادها قبلا بارگذاری شدهاند یا خیر
if (offersState is OffersLoadSuccess) {
final allOffers = offersState.offers;
if (mounted) {
setState(() {
_reservedOffers = allOffers
.where((offer) => _reservedIds.contains(offer.id))
.toList();
});
}
}
}
@override
@ -39,39 +51,16 @@ class _ReservedListPageState extends State<ReservedListPage> {
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: _buildCustomAppBar(context),
body: FutureBuilder<List<OfferModel?>>(
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<OfferModel>().toList();
if (reservedOffers.isEmpty) {
return const Center(
// FutureBuilder با یک ویجت ساده جایگزین شد
body: _reservedOffers.isEmpty
? const Center(
child: Text('هیچ آیتم رزرو شده‌ای یافت نشد.'),
);
}
return ListView.builder(
)
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: reservedOffers.length,
itemCount: _reservedOffers.length,
itemBuilder: (context, index) {
final offer = reservedOffers[index];
final offer = _reservedOffers[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
@ -101,11 +90,9 @@ class _ReservedListPageState extends State<ReservedListPage> {
curve: Curves.easeOutCubic,
);
},
);
},
),
),
);
)
) ); }
}
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 '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<SplashScreen> {
late final StreamSubscription _authSubscription;
@override
void initState() {
super.initState();
final authBloc = context.read<AuthBloc>();
// با کمی تاخیر برای نمایش لوگو، فرآیند را شروع میکنیم
Timer(const Duration(seconds: 2), _checkAuthAndNavigate);
}
_authSubscription = authBloc.stream.listen((state) {
_authSubscription.cancel();
if (state is AuthSuccess) {
Future<void> _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<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 {
} 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

View File

@ -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<ProductDetailEvent, ProductDetailState> {
final OfferRepository _offerRepository;
// class ProductDetailBloc extends Bloc<ProductDetailEvent, ProductDetailState> {
// final OfferRepository _offerRepository;
ProductDetailBloc({required OfferRepository offerRepository})
: _offerRepository = offerRepository,
super(ProductDetailInitial()) {
on<ProductDetailFetchRequested>(_onFetchRequested);
}
// ProductDetailBloc({required OfferRepository offerRepository})
// : _offerRepository = offerRepository,
// super(ProductDetailInitial()) {
// on<ProductDetailFetchRequested>(_onFetchRequested);
// }
Future<void> _onFetchRequested(
ProductDetailFetchRequested event,
Emitter<ProductDetailState> 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()));
}
}
}
// Future<void> _onFetchRequested(
// ProductDetailFetchRequested event,
// Emitter<ProductDetailState> 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()));
// }
// }
// }

View File

@ -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<void> 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<Map<String, dynamic>> _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<Map<String, dynamic>> 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<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.onConnected = () {
// print('✅ MQTT Connected');
// client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> 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();
// }
// }
// }
client.onConnected = () {
debugPrint('✅ [MQTT] Connected successfully.');
client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> 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<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();
}
}