mqtt connected
This commit is contained in:
parent
690813829d
commit
050fb6b620
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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"; // این خط اضافه شد
|
||||||
}
|
}
|
||||||
|
|
@ -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'] ?? []),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 متر";
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
@ -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();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -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'] ?? 'کد صحیح نیست'));
|
||||||
|
|
|
||||||
|
|
@ -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'] ?? 'خطا در ارتباط با سرور'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {} // این کلاس را اضافه کنید
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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('جستوجوی تصادفی'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue