Compare commits
6 Commits
main
...
background
| Author | SHA1 | Date |
|---|---|---|
|
|
e4fc94a3e7 | |
|
|
570ff6bb06 | |
|
|
60c91e8fbb | |
|
|
ce62c567c5 | |
|
|
608222e8a3 | |
|
|
f4cd446cde |
|
|
@ -11,32 +11,29 @@ plugins {
|
|||
android {
|
||||
namespace = "com.example.proxibuy"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.proxibuy"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 23
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
multiDexEnabled = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
|
|
@ -45,3 +42,8 @@ android {
|
|||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// این نسخه به آخرین نسخه مورد نیاز آپدیت شد
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:label="Proxibuy"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<!-- android:networkSecurityConfig="@xml/network_security_config"> -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
@ -22,10 +24,7 @@
|
|||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
|
|
@ -35,21 +34,20 @@
|
|||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
||||
<service
|
||||
android:name="id.flutter.flutter_background_service.BackgroundService"
|
||||
android:foregroundServiceType="location" />
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
@ -5,6 +5,9 @@ class ApiConfig {
|
|||
static const String updateUser = "/user/updateName";
|
||||
static const String updateCategories = "/user/favoriteCategory";
|
||||
static const String getFavoriteCategories = "/user/getfavoriteCategory";
|
||||
static const String addReservation = "/reservation/add";
|
||||
static const String getReservations = "/reservation/get";
|
||||
static const String addReservation = "/reservation/add";
|
||||
static const String getReservations = "/reservation/get";
|
||||
static const String updateFcmToken = "/user/firebaseUpdate";
|
||||
static const String addComment = "/comment/add";
|
||||
static const String getComments = "/comment/get/";
|
||||
}
|
||||
|
|
@ -17,4 +17,6 @@ class AppColors {
|
|||
static const Color countdownBorderRserve = Color.fromARGB(255, 186, 222, 251);
|
||||
static const Color expiryReserve = Color.fromARGB(255, 183, 28, 28);
|
||||
static const Color uploadElevated = Color.fromARGB(255, 233, 245, 254);
|
||||
static const Color backgroundConfirm = Color.fromARGB(255, 237, 247, 238);
|
||||
static const Color notifIcon = Color.fromARGB(255, 179, 38, 30);
|
||||
}
|
||||
|
|
@ -23,13 +23,18 @@ class CommentModel extends Equatable {
|
|||
List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls];
|
||||
|
||||
factory CommentModel.fromJson(Map<String, dynamic> json) {
|
||||
final List<String> images = (json['UserImages'] as List<dynamic>?)
|
||||
?.map((image) => image['Url'] as String)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
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'] ?? []),
|
||||
id: json['ID'] ?? '',
|
||||
userName: json['User']?['Name'] ?? 'کاربر ناشناس',
|
||||
rating: (json['Score'] as num?)?.toDouble() ?? 0.0,
|
||||
comment: json['Text'] ?? '',
|
||||
publishedAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
uploadedImageUrls: images,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:proxibuy/data/models/offer_model.dart';
|
||||
|
||||
class NotificationModel {
|
||||
final String id;
|
||||
final String description;
|
||||
final DateTime createdAt;
|
||||
final String discountId;
|
||||
final String discountName;
|
||||
final String shopName;
|
||||
final bool status;
|
||||
OfferModel? offer;
|
||||
|
||||
NotificationModel({
|
||||
required this.id,
|
||||
required this.description,
|
||||
required this.createdAt,
|
||||
required this.discountId,
|
||||
required this.discountName,
|
||||
required this.shopName,
|
||||
required this.status,
|
||||
this.offer,
|
||||
});
|
||||
|
||||
factory NotificationModel.fromJson(Map<String, dynamic> json) {
|
||||
final bool statusValue = json['Status'] is bool ? json['Status'] : false;
|
||||
|
||||
return NotificationModel(
|
||||
id: json['ID'] ?? '',
|
||||
description: json['Description'] ?? 'No description available.',
|
||||
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
discountId: json['Discount']?['ID'] ?? '',
|
||||
discountName: json['Discount']?['Name'] ?? 'Unknown Discount',
|
||||
shopName: json['Discount']?['Shop']?['Name'] ?? 'Unknown Shop',
|
||||
status: statusValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +149,7 @@ class OfferModel extends Equatable {
|
|||
rating: 0.0,
|
||||
ratingCount: 0,
|
||||
comments: [],
|
||||
discountInfo: json['Description'],
|
||||
discountInfo: json['Description'] ?? 'توضیحات موجود نیست',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +157,29 @@ class OfferModel extends Equatable {
|
|||
imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
storeName,
|
||||
title,
|
||||
discount,
|
||||
imageUrls,
|
||||
category,
|
||||
distanceInMeters,
|
||||
expiryTime,
|
||||
address,
|
||||
workingHours,
|
||||
discountType,
|
||||
isOpen,
|
||||
rating,
|
||||
ratingCount,
|
||||
latitude,
|
||||
longitude,
|
||||
originalPrice,
|
||||
finalPrice,
|
||||
features,
|
||||
discountInfo,
|
||||
comments,
|
||||
];
|
||||
|
||||
String get distanceAsString {
|
||||
if (distanceInMeters < 1000) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// lib/main.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
|
@ -10,18 +9,29 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||
import 'package:proxibuy/core/config/http_overrides.dart';
|
||||
import 'package:proxibuy/firebase_options.dart';
|
||||
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_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/reservation/cubit/reservation_cubit.dart';
|
||||
import 'package:proxibuy/services/background_service.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
import 'core/config/app_colors.dart';
|
||||
import 'package:proxibuy/presentation/pages/splash_screen.dart';
|
||||
|
||||
|
||||
void main() async {
|
||||
|
||||
Future<String?> getFcmToken() async {
|
||||
FirebaseMessaging messaging = FirebaseMessaging.instance;
|
||||
String? token = await messaging.getToken();
|
||||
print("🔥 Firebase Messaging Token: $token");
|
||||
return token;
|
||||
}
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await initializeService();
|
||||
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
Animate.restartOnHotReload = true;
|
||||
runApp(const MyApp());
|
||||
|
|
@ -34,22 +44,18 @@ class MyApp extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
RepositoryProvider<MqttService>(
|
||||
create: (context) => MqttService(),
|
||||
),
|
||||
RepositoryProvider<MqttService>(create: (context) => MqttService()),
|
||||
BlocProvider<AuthBloc>(
|
||||
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
|
||||
),
|
||||
BlocProvider<ReservationCubit>(
|
||||
create: (context) => ReservationCubit(),
|
||||
),
|
||||
BlocProvider<OffersBloc>(
|
||||
create: (context) => OffersBloc(),
|
||||
),
|
||||
BlocProvider<ReservationCubit>(create: (context) => ReservationCubit()),
|
||||
BlocProvider<OffersBloc>(create: (context) => OffersBloc()),
|
||||
BlocProvider<NotificationPreferencesBloc>(
|
||||
create: (context) => NotificationPreferencesBloc(),
|
||||
),
|
||||
|
||||
BlocProvider<CommentBloc>(
|
||||
create: (context) => CommentBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'Proxibuy',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
// lib/presentation/auth/bloc/auth_bloc.dart
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
|
@ -15,7 +13,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||
late final Dio _dio;
|
||||
final _storage = const FlutterSecureStorage();
|
||||
|
||||
AuthBloc() : super(AuthInitial()) {
|
||||
AuthBloc() : super(AuthUnknown()) {
|
||||
_dio = Dio();
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(
|
||||
|
|
@ -31,10 +29,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||
on<VerifyOTPEvent>(_onVerifyOTP);
|
||||
on<UpdateUserInfoEvent>(_onUpdateUserInfo);
|
||||
on<LogoutEvent>(_onLogout);
|
||||
on<SendFcmTokenEvent>(_onSendFcmToken);
|
||||
}
|
||||
|
||||
Future<void> _onCheckAuthStatus(
|
||||
CheckAuthStatusEvent event, Emitter<AuthState> emit) async {
|
||||
CheckAuthStatusEvent event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
final token = await _storage.read(key: 'accessToken');
|
||||
if (token != null && token.isNotEmpty) {
|
||||
emit(AuthSuccess());
|
||||
|
|
@ -43,6 +44,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
|
|
@ -52,10 +54,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||
);
|
||||
if (isClosed) return;
|
||||
if (response.statusCode == 200) {
|
||||
emit(AuthCodeSentSuccess(
|
||||
phone: event.phoneNumber,
|
||||
countryCode: event.countryCode,
|
||||
));
|
||||
emit(
|
||||
AuthCodeSentSuccess(
|
||||
phone: event.phoneNumber,
|
||||
countryCode: event.countryCode,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد'));
|
||||
}
|
||||
|
|
@ -65,7 +69,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _onVerifyOTP(VerifyOTPEvent event, Emitter<AuthState> emit) async {
|
||||
Future<void> _onVerifyOTP(
|
||||
VerifyOTPEvent event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
|
|
@ -97,8 +104,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||
}
|
||||
|
||||
Future<void> _onUpdateUserInfo(
|
||||
UpdateUserInfoEvent event, Emitter<AuthState> emit) async {
|
||||
debugPrint("AuthBloc: 🔵 ایونت UpdateUserInfoEvent دریافت شد با نام: ${event.name}");
|
||||
UpdateUserInfoEvent event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
debugPrint(
|
||||
"AuthBloc: 🔵 ایونت UpdateUserInfoEvent دریافت شد با نام: ${event.name}",
|
||||
);
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
final token = await _storage.read(key: 'accessToken');
|
||||
|
|
@ -115,30 +126,59 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
|
||||
debugPrint("AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}");
|
||||
debugPrint(
|
||||
"AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}",
|
||||
);
|
||||
|
||||
if (isClosed) {
|
||||
debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است.");
|
||||
return;
|
||||
debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint("AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess...");
|
||||
debugPrint(
|
||||
"AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess...",
|
||||
);
|
||||
emit(AuthSuccess());
|
||||
} else {
|
||||
debugPrint("AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}");
|
||||
debugPrint(
|
||||
"AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}",
|
||||
);
|
||||
emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات'));
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
debugPrint("AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}");
|
||||
debugPrint(
|
||||
"AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}",
|
||||
);
|
||||
if (isClosed) return;
|
||||
emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _onLogout(LogoutEvent event, Emitter<AuthState> emit) async {
|
||||
await _storage.deleteAll();
|
||||
emit(AuthInitial());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSendFcmToken(
|
||||
SendFcmTokenEvent event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
try {
|
||||
final token = await _storage.read(key: 'accessToken');
|
||||
if (token == null) {
|
||||
emit(const AuthFailure("شما وارد نشدهاید."));
|
||||
return;
|
||||
}
|
||||
|
||||
await _dio.post(
|
||||
ApiConfig.baseUrl + ApiConfig.updateFcmToken,
|
||||
data: {'Token': event.fcmToken},
|
||||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
print("Firebase token: ${event.fcmToken}");
|
||||
} on DioException catch (e) {
|
||||
debugPrint("Error sending FCM token: ${e.response?.data['message']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,4 +25,9 @@ class UpdateUserInfoEvent extends AuthEvent {
|
|||
final String gender;
|
||||
|
||||
UpdateUserInfoEvent({required this.name, required this.gender});
|
||||
}
|
||||
|
||||
class SendFcmTokenEvent extends AuthEvent {
|
||||
final String fcmToken;
|
||||
SendFcmTokenEvent({required this.fcmToken});
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ abstract class AuthState extends Equatable {
|
|||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class AuthUnknown extends AuthState {}
|
||||
|
||||
class AuthInitial extends AuthState {}
|
||||
|
||||
class AuthLoading extends AuthState {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:proxibuy/core/config/api_config.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class CommentBloc extends Bloc<CommentEvent, CommentState> {
|
||||
late final Dio _dio;
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
|
||||
CommentBloc() : super(CommentInitial()) {
|
||||
_dio = Dio();
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
logPrint: (obj) => debugPrint(obj.toString()),
|
||||
),
|
||||
);
|
||||
on<SubmitComment>(_onSubmitComment);
|
||||
}
|
||||
|
||||
Future<void> _onSubmitComment(SubmitComment event, Emitter<CommentState> emit) async {
|
||||
if (event.text.isEmpty && event.score == 0) {
|
||||
emit(const CommentSubmissionFailure("لطفا امتیاز یا نظری برای این تخفیف ثبت کنید."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(CommentSubmitting());
|
||||
try {
|
||||
final token = await _storage.read(key: 'accessToken');
|
||||
if (token == null) {
|
||||
emit(const CommentSubmissionFailure("شما وارد نشدهاید."));
|
||||
return;
|
||||
}
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'Discount': event.discountId,
|
||||
'Text': event.text,
|
||||
'Score': event.score,
|
||||
});
|
||||
|
||||
for (File imageFile in event.images) {
|
||||
formData.files.add(MapEntry(
|
||||
'Images',
|
||||
await MultipartFile.fromFile(
|
||||
imageFile.path,
|
||||
filename: imageFile.path.split('/').last,
|
||||
contentType: MediaType('image', 'jpeg'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
final response = await _dio.post(
|
||||
ApiConfig.baseUrl + ApiConfig.addComment,
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
emit(CommentSubmissionSuccess());
|
||||
} else {
|
||||
emit(CommentSubmissionFailure(response.data['message'] ?? 'خطا در ارسال نظر'));
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
emit(CommentSubmissionFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
|
||||
} catch (e) {
|
||||
emit(CommentSubmissionFailure('خطایی ناشناخته رخ داد: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import 'dart:io';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class CommentEvent extends Equatable {
|
||||
const CommentEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class SubmitComment extends CommentEvent {
|
||||
final String discountId;
|
||||
final String text;
|
||||
final double score;
|
||||
final List<File> images;
|
||||
|
||||
const SubmitComment({
|
||||
required this.discountId,
|
||||
required this.text,
|
||||
required this.score,
|
||||
required this.images,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [discountId, text, score, images];
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class CommentState extends Equatable {
|
||||
const CommentState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class CommentInitial extends CommentState {}
|
||||
|
||||
class CommentSubmitting extends CommentState {}
|
||||
|
||||
class CommentSubmissionSuccess extends CommentState {}
|
||||
|
||||
class CommentSubmissionFailure extends CommentState {
|
||||
final String error;
|
||||
|
||||
const CommentSubmissionFailure(this.error);
|
||||
|
||||
@override
|
||||
List<Object> get props => [error];
|
||||
}
|
||||
|
|
@ -26,12 +26,12 @@ class NotificationPreferencesBloc
|
|||
LoadCategories event, Emitter<NotificationPreferencesState> emit) {
|
||||
final categories = [
|
||||
CategoryEntity(id: "e33dd7f9-5b20-4273-8eea-59da6ca5f206", name: 'لوازم دیجیتال', icon: Assets.icons.digital),
|
||||
CategoryEntity(id: "b73a868a-a2d2-4d96-8fd4-615327ed9629", name: 'کافیشاپ', icon: Assets.icons.coffeeshop),
|
||||
CategoryEntity(id: "b73a868a-a2d2-4d96-8fd4-615327ed9629", name: 'کافی شاپ', icon: Assets.icons.coffeeshop), // Change Here
|
||||
CategoryEntity(id: "b5881239-bfd5-4c27-967a-187316a7e0b7", name: 'رستوران', icon: Assets.icons.resturan),
|
||||
CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: 'فستفود', icon: Assets.icons.fastfood),
|
||||
CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: 'فست فود', icon: Assets.icons.fastfood), // Change Here
|
||||
CategoryEntity(id: "71e371f8-a47a-4a58-aee6-4ed0f26bf29b", name: 'پوشاک', icon: Assets.icons.pooshak),
|
||||
CategoryEntity(id: "42acff41-1165-4e62-89b9-58db7329ec3a", name: 'تریا', icon: Assets.icons.teria),
|
||||
CategoryEntity(id: "2f38918c-5566-4aec-a0a9-2c7c48b1e878", name: 'کیفوکفش', icon: Assets.icons.kafsh),
|
||||
CategoryEntity(id: "2f38918c-5566-4aec-a0a9-2c7c48b1e878", name: 'کیف و کفش', icon: Assets.icons.kafsh), // Change Here
|
||||
CategoryEntity(id: "52c51010-3a63-4264-a350-e011c889f3dd", name: 'سینما', icon: Assets.icons.cinama),
|
||||
CategoryEntity(id: "34185954-f79f-4b9e-8eb2-1702679c40a0", name: 'لوازم آرایشی', icon: Assets.icons.arayesh),
|
||||
CategoryEntity(id: "e4517b0c-aacf-4758-94bd-85f45062980f", name: 'طلا و زیورآلات', icon: Assets.icons.tala),
|
||||
|
|
@ -126,6 +126,4 @@ class NotificationPreferencesBloc
|
|||
ResetSubmissionStatus event, Emitter<NotificationPreferencesState> emit) {
|
||||
emit(state.copyWith(submissionSuccess: false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ class AddPhotoScreen extends StatelessWidget {
|
|||
required this.offer,
|
||||
});
|
||||
|
||||
// متد ساخت توکن
|
||||
Future<String> _generateQrToken(BuildContext context) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final userID = await storage.read(key: 'userID');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,414 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_bloc.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
|
||||
import 'package:proxibuy/presentation/pages/offers_page.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
class CommentPage extends StatefulWidget {
|
||||
final String discountId;
|
||||
|
||||
const CommentPage({super.key, required this.discountId});
|
||||
|
||||
@override
|
||||
State<CommentPage> createState() => _CommentPageState();
|
||||
}
|
||||
|
||||
class _CommentPageState extends State<CommentPage> {
|
||||
final _commentController = TextEditingController();
|
||||
double _rating = 0.0;
|
||||
final List<File> _images = [];
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_commentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pickImage(ImageSource source) async {
|
||||
if (_images.length >= 2) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('شما فقط میتوانید ۲ عکس اضافه کنید.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final pickedFile = await _picker.pickImage(
|
||||
source: source,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
setState(() {
|
||||
_images.add(File(pickedFile.path));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _removeImage(int index) {
|
||||
setState(() {
|
||||
_images.removeAt(index);
|
||||
});
|
||||
}
|
||||
|
||||
void _showImageSourceActionSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Wrap(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.photo_library,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
title: const Text('انتخاب از گالری'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickImage(ImageSource.gallery);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt, color: AppColors.primary),
|
||||
title: const Text('گرفتن عکس با دوربین'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickImage(ImageSource.camera);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CommentBloc(),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: _buildCustomAppBar(),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 24),
|
||||
Center(child: _buildRatingCard()),
|
||||
const SizedBox(height: 16),
|
||||
_buildCommentCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildImagePickerSection(),
|
||||
const SizedBox(height: 32),
|
||||
_buildActionButtons(),
|
||||
]
|
||||
.animate(interval: 100.ms)
|
||||
.fadeIn(duration: 400.ms)
|
||||
.slideY(begin: 0.2, curve: Curves.easeOut),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildCustomAppBar() {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size.fromHeight(70.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
bottom: Radius.circular(15),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Text(
|
||||
'ثبت نظر',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: SvgPicture.asset(Assets.icons.arrowLeft.path),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.confirm.withOpacity(0.1),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: AppColors.confirm,
|
||||
size: 50,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'خریدت با موفقیت انجام شد. منتظر دیدار دوبارهات هستیم. لطفا نظرت رو در مورد این تخفیف بهمون بگو.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black, height: 1.5),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRatingCard() {
|
||||
return Card(
|
||||
elevation: 1,
|
||||
shadowColor: Colors.black12,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
"امتیاز شما چقدره؟",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
RatingBar.builder(
|
||||
initialRating: 0,
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
itemPadding: const EdgeInsets.symmetric(horizontal: 7.0),
|
||||
itemBuilder:
|
||||
(context, _) =>
|
||||
Icon(Icons.star, color: Colors.amber.shade700),
|
||||
onRatingUpdate: (rating) => setState(() => _rating = rating),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommentCard() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
controller: _commentController,
|
||||
maxLines: 4,
|
||||
textAlign: TextAlign.right,
|
||||
decoration: InputDecoration(
|
||||
labelText: "گوشمون به شماست",
|
||||
hintText: "نظراتت رو بگو...",
|
||||
alignLabelWithHint: true,
|
||||
suffixIcon: Padding(padding: const EdgeInsets.all(12.0)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImagePickerSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"افزودن عکس (اختیاری)",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"میتونی عکس تجربه خریدت رو باهامون به اشتراک بذاری!",
|
||||
style: TextStyle(fontSize: 14, color: AppColors.hint),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(_images.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Image.file(
|
||||
_images[index],
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -8,
|
||||
right: -8,
|
||||
child: GestureDetector(
|
||||
onTap: () => _removeImage(index),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (_images.length < 2)
|
||||
GestureDetector(
|
||||
onTap: _showImageSourceActionSheet,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade400,
|
||||
width: 1.5,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add_a_photo_outlined,
|
||||
color: Colors.grey,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return BlocConsumer<CommentBloc, CommentState>(
|
||||
listener: (context, state) {
|
||||
if (state is CommentSubmissionSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('نظر شما با موفقیت ثبت شد. ممنونیم!'),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const OffersPage()),
|
||||
(route) => false,
|
||||
);
|
||||
} else if (state is CommentSubmissionFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.error),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isLoading = state is CommentSubmitting;
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
isLoading
|
||||
? null
|
||||
: () {
|
||||
context.read<CommentBloc>().add(
|
||||
SubmitComment(
|
||||
discountId: widget.discountId,
|
||||
text: _commentController.text,
|
||||
score: _rating,
|
||||
images: _images,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.confirm,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
)
|
||||
: const Text('ارسال'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed:
|
||||
isLoading
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const OffersPage()),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'رد شدن',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ class _NotificationPreferencesPageState
|
|||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
body: BlocListener<NotificationPreferencesBloc,
|
||||
body: BlocConsumer<NotificationPreferencesBloc,
|
||||
NotificationPreferencesState>(
|
||||
listener: (context, state) async {
|
||||
if (state.submissionSuccess) {
|
||||
|
|
@ -157,7 +157,7 @@ class _NotificationPreferencesPageState
|
|||
);
|
||||
}
|
||||
} else {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
|
|
@ -176,151 +176,151 @@ class _NotificationPreferencesPageState
|
|||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
builder: (context, state) {
|
||||
return Stack(
|
||||
children: [
|
||||
const Text(
|
||||
'دریافت اعلان',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Divider(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 14,
|
||||
color: AppColors.hint,
|
||||
height: 1.5,
|
||||
),
|
||||
children: const <TextSpan>[
|
||||
TextSpan(
|
||||
text:
|
||||
'ترجیح میدی از کدام دستهبندیها اعلان تخفیف دریافت کنی؟ ',
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'دریافت اعلان',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'),
|
||||
const SizedBox(height: 4),
|
||||
const Divider(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 14,
|
||||
color: AppColors.hint,
|
||||
height: 1.5,
|
||||
),
|
||||
children: const <TextSpan>[
|
||||
TextSpan(
|
||||
text:
|
||||
'ترجیح میدی از کدام دستهبندیها اعلان تخفیف دریافت کنی؟ ',
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (state.categories.isEmpty && state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final double horizontalPadding = 24.0;
|
||||
final double crossAxisSpacing = 16.0;
|
||||
final int crossAxisCount = 3;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final itemWidth = (screenWidth -
|
||||
(horizontalPadding * 2) -
|
||||
(crossAxisSpacing * (crossAxisCount - 1))) /
|
||||
crossAxisCount;
|
||||
final itemHeight = itemWidth / 0.9;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: crossAxisSpacing,
|
||||
runSpacing: 24.0,
|
||||
alignment: WrapAlignment.center,
|
||||
children: state.categories.map((category) {
|
||||
final isSelected =
|
||||
state.selectedCategoryIds.contains(category.id);
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
child: CategorySelectionCard(
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
isSelected: isSelected,
|
||||
showSelectableIndicator:
|
||||
state.selectedCategoryIds.isNotEmpty,
|
||||
onTap: () {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
ToggleCategorySelection(category.id));
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.selectedCategoryIds.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: state.isLoading
|
||||
? null
|
||||
: () async {
|
||||
final bloc =
|
||||
context.read<NotificationPreferencesBloc>();
|
||||
|
||||
final selectedCategoryNames = bloc
|
||||
.state.categories
|
||||
.where((cat) => bloc.state
|
||||
.selectedCategoryIds
|
||||
.contains(cat.id))
|
||||
.map((cat) => cat.name)
|
||||
.toList();
|
||||
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(
|
||||
'user_selected_categories',
|
||||
selectedCategoryNames);
|
||||
|
||||
bloc.add(SubmitPreferences());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.confirm,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.grey.withOpacity(0.5),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'اعمال',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: BlocBuilder<NotificationPreferencesBloc,
|
||||
NotificationPreferencesState>(
|
||||
builder: (context, state) {
|
||||
if (state.categories.isEmpty && state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final double horizontalPadding = 24.0;
|
||||
final double crossAxisSpacing = 16.0;
|
||||
final int crossAxisCount = 3;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final itemWidth = (screenWidth -
|
||||
(horizontalPadding * 2) -
|
||||
(crossAxisSpacing * (crossAxisCount - 1))) /
|
||||
crossAxisCount;
|
||||
final itemHeight = itemWidth / 0.9;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: crossAxisSpacing,
|
||||
runSpacing: 24.0,
|
||||
alignment: WrapAlignment.center,
|
||||
children: state.categories.map((category) {
|
||||
final isSelected =
|
||||
state.selectedCategoryIds.contains(category.id);
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
child: CategorySelectionCard(
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
isSelected: isSelected,
|
||||
showSelectableIndicator:
|
||||
state.selectedCategoryIds.isNotEmpty,
|
||||
onTap: () {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
ToggleCategorySelection(category.id));
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
if (state.isLoading)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocBuilder<NotificationPreferencesBloc,
|
||||
NotificationPreferencesState>(
|
||||
builder: (context, state) {
|
||||
final areCategoriesSelected =
|
||||
state.selectedCategoryIds.isNotEmpty;
|
||||
|
||||
if (areCategoriesSelected) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: !state.isLoading
|
||||
? () async {
|
||||
final bloc =
|
||||
context.read<NotificationPreferencesBloc>();
|
||||
|
||||
final selectedCategoryNames = bloc
|
||||
.state.categories
|
||||
.where((cat) => bloc.state
|
||||
.selectedCategoryIds
|
||||
.contains(cat.id))
|
||||
.map((cat) => cat.name)
|
||||
.toList();
|
||||
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(
|
||||
'user_selected_categories',
|
||||
selectedCategoryNames);
|
||||
|
||||
bloc.add(SubmitPreferences());
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.confirm,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.grey,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
),
|
||||
child: state.isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white)
|
||||
: const Text(
|
||||
'اعمال',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import 'package:proxibuy/presentation/pages/reserved_list_page.dart';
|
|||
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
|
||||
import 'package:proxibuy/presentation/widgets/gps_dialog.dart';
|
||||
import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart';
|
||||
import 'package:proxibuy/presentation/widgets/notification_panel.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
|
@ -44,6 +45,11 @@ class _OffersPageState extends State<OffersPage> {
|
|||
bool _isSubscribedToOffers = false;
|
||||
bool _isGpsEnabled = false;
|
||||
bool _isConnectedToInternet = true;
|
||||
// Notifications panel state
|
||||
final GlobalKey _bellKey = GlobalKey();
|
||||
OverlayEntry? _notifOverlay;
|
||||
bool _notifVisible = false;
|
||||
int _notificationCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -52,6 +58,7 @@ class _OffersPageState extends State<OffersPage> {
|
|||
_initializePage();
|
||||
_initConnectivityListener();
|
||||
_fetchInitialReservations();
|
||||
_fetchNotificationCount();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -60,6 +67,7 @@ class _OffersPageState extends State<OffersPage> {
|
|||
_mqttMessageSubscription?.cancel();
|
||||
_locationTimer?.cancel();
|
||||
_connectivitySubscription?.cancel();
|
||||
_removeNotificationOverlay();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -227,8 +235,8 @@ class _OffersPageState extends State<OffersPage> {
|
|||
|
||||
final payload = {
|
||||
"userID": userID,
|
||||
"lat":32.6685,
|
||||
"lng": 51.6826
|
||||
"lat": position.latitude,
|
||||
"lng": position.longitude
|
||||
};
|
||||
|
||||
mqttService.publish("proxybuy/sendGps", payload);
|
||||
|
|
@ -288,6 +296,139 @@ class _OffersPageState extends State<OffersPage> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchNotificationCount() async {
|
||||
try {
|
||||
const storage = FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'accessToken');
|
||||
if (token == null) {
|
||||
if (mounted) setState(() => _notificationCount = 0);
|
||||
return;
|
||||
}
|
||||
final dio = Dio();
|
||||
final response = await dio.get(
|
||||
'https://proxybuy.liara.run/notify/get',
|
||||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data['data'] ?? [];
|
||||
// Filter only active notifications (Status: true)
|
||||
final activeNotifications = data.where((item) => item['Status'] == true).toList();
|
||||
setState(() => _notificationCount = activeNotifications.length);
|
||||
} else {
|
||||
setState(() => _notificationCount = 0);
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _notificationCount = 0);
|
||||
}
|
||||
}
|
||||
|
||||
void _showNotificationOverlay() {
|
||||
if (_notifOverlay != null) return;
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
final renderBox = _bellKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) return;
|
||||
|
||||
final bellSize = renderBox.size;
|
||||
final bellPosition = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final panelWidth = screenSize.width.clamp(0, 360);
|
||||
final width = (panelWidth > 320 ? 320.0 : panelWidth - 24).toDouble();
|
||||
final top = bellPosition.dy + bellSize.height; // stick to icon
|
||||
final tentativeLeft = bellPosition.dx + bellSize.width - width;
|
||||
final double left = tentativeLeft.clamp(8.0, screenSize.width - width - 8.0).toDouble();
|
||||
|
||||
_notifOverlay = OverlayEntry(
|
||||
builder: (ctx) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Tap outside to close
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: _hideNotificationOverlay,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: top,
|
||||
left: left,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.9, end: 1.0),
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutBack,
|
||||
builder: (context, scale, child) {
|
||||
// Ensure scale is valid
|
||||
final validScale = scale.clamp(0.0, 1.0);
|
||||
return Opacity(
|
||||
opacity: validScale,
|
||||
child: Transform.scale(
|
||||
scale: validScale,
|
||||
alignment: Alignment.topLeft,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
width: width,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 250,
|
||||
minWidth: 370,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 24,
|
||||
spreadRadius: 4,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: NotificationPanel(
|
||||
onClose: _hideNotificationOverlay,
|
||||
onListChanged: () {
|
||||
_fetchNotificationCount();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
overlay.insert(_notifOverlay!);
|
||||
setState(() => _notifVisible = true);
|
||||
}
|
||||
|
||||
void _hideNotificationOverlay() {
|
||||
_notifOverlay?.remove();
|
||||
_notifOverlay = null;
|
||||
if (mounted) setState(() => _notifVisible = false);
|
||||
}
|
||||
|
||||
void _removeNotificationOverlay() {
|
||||
_notifOverlay?.remove();
|
||||
_notifOverlay = null;
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() async {
|
||||
await _sendLocationUpdate();
|
||||
await _fetchNotificationCount();
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Widget _buildFavoriteCategoriesSection() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0),
|
||||
|
|
@ -381,8 +522,51 @@ class _OffersPageState extends State<OffersPage> {
|
|||
child: Assets.icons.logoWithName.svg(height: 40, width: 200),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {}, icon: Assets.icons.notification.svg()),
|
||||
// Notification bell with badge and overlay trigger
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
key: _bellKey,
|
||||
onPressed: () {
|
||||
if (_notifVisible) {
|
||||
_hideNotificationOverlay();
|
||||
} else {
|
||||
_fetchNotificationCount();
|
||||
_showNotificationOverlay();
|
||||
}
|
||||
},
|
||||
icon: Assets.icons.notification.svg(),
|
||||
),
|
||||
if (_notificationCount > 0)
|
||||
Positioned(
|
||||
top: 3,
|
||||
// in RTL, actions are on the left; badge at top-left of icon
|
||||
right: 7,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$_notificationCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
BlocBuilder<ReservationCubit, ReservationState>(
|
||||
builder: (context, state) {
|
||||
final reservedCount = state.reservedProductIds.length;
|
||||
|
|
@ -445,16 +629,21 @@ class _OffersPageState extends State<OffersPage> {
|
|||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFavoriteCategoriesSection(),
|
||||
OffersView(
|
||||
isGpsEnabled: _isGpsEnabled,
|
||||
isConnectedToInternet: _isConnectedToInternet,
|
||||
),
|
||||
],
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
color: AppColors.active,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFavoriteCategoriesSection(),
|
||||
OffersView(
|
||||
isGpsEnabled: _isGpsEnabled,
|
||||
isConnectedToInternet: _isConnectedToInternet,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
|
@ -11,6 +12,7 @@ import 'package:maps_launcher/maps_launcher.dart';
|
|||
import 'package:proxibuy/core/config/api_config.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/data/models/comment_model.dart';
|
||||
import 'package:proxibuy/data/models/offer_model.dart';
|
||||
import 'package:proxibuy/presentation/pages/add_photo_screen.dart';
|
||||
import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
|
||||
|
|
@ -87,20 +89,9 @@ class ProductDetailPage extends StatelessWidget {
|
|||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
debugPrint("----------- REQUEST-----------");
|
||||
debugPrint("URL: POST $url");
|
||||
debugPrint("Headers: ${options.headers}");
|
||||
debugPrint("Body: $data");
|
||||
debugPrint("-----------------------------");
|
||||
|
||||
final response =
|
||||
await dio.post(url, data: data, options: options);
|
||||
|
||||
debugPrint("---------- RESPONSE-----------");
|
||||
debugPrint("StatusCode: ${response.statusCode}");
|
||||
debugPrint("Data: ${response.data}");
|
||||
debugPrint("-----------------------------");
|
||||
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -131,11 +122,6 @@ class ProductDetailPage extends StatelessWidget {
|
|||
} on DioException catch (e) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
|
||||
debugPrint("---------- ERROR-----------");
|
||||
debugPrint("StatusCode: ${e.response?.statusCode}");
|
||||
debugPrint("Data: ${e.response?.data}");
|
||||
debugPrint("--------------------------");
|
||||
|
||||
final errorMessage = e.response?.data?['message'] ??
|
||||
'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.';
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -145,10 +131,6 @@ class ProductDetailPage extends StatelessWidget {
|
|||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
debugPrint("---------- GENERAL ERROR -----------");
|
||||
debugPrint(e.toString());
|
||||
debugPrint("------------------------------------");
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toString()),
|
||||
|
|
@ -205,12 +187,47 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
late List<String> imageList;
|
||||
late String selectedImage;
|
||||
final String _uploadKey = 'upload_image';
|
||||
late Future<List<CommentModel>> _commentsFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageList = List.from(widget.offer.imageUrls)..add(_uploadKey);
|
||||
selectedImage = imageList.first;
|
||||
selectedImage = imageList.isNotEmpty ? imageList.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
|
||||
_commentsFuture = _fetchComments();
|
||||
}
|
||||
|
||||
Future<List<CommentModel>> _fetchComments() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'accessToken');
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Authentication token not found!');
|
||||
}
|
||||
|
||||
try {
|
||||
final dio = Dio();
|
||||
final response = await dio.get(
|
||||
ApiConfig.baseUrl + ApiConfig.getComments + widget.offer.id,
|
||||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> commentsJson = response.data['data']['comments'];
|
||||
return commentsJson.map((json) => CommentModel.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to load comments with status code: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
debugPrint("DioException fetching comments: $e");
|
||||
if (e.response != null) {
|
||||
debugPrint("Response data: ${e.response?.data}");
|
||||
}
|
||||
throw Exception('Failed to load comments: ${e.message}');
|
||||
} catch (e) {
|
||||
debugPrint("Error fetching comments: $e");
|
||||
throw Exception('An unknown error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _launchMaps(double lat, double lon, String title) {
|
||||
|
|
@ -384,19 +401,6 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 90,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade400, width: 1.5),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(9.0),
|
||||
child: SvgPicture.asset(Assets.icons.addImg.path),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -405,8 +409,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
? widget.offer.expiryTime.difference(DateTime.now())
|
||||
: Duration.zero;
|
||||
|
||||
final formatCurrency =
|
||||
NumberFormat.decimalPattern('fa_IR'); // Or 'en_US'
|
||||
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0)
|
||||
|
|
@ -663,7 +666,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
slideDirection: SlideDirection.up,
|
||||
separator: ':',
|
||||
style: const TextStyle(
|
||||
fontSize: 50,
|
||||
fontSize: 45,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.countdown,
|
||||
),
|
||||
|
|
@ -687,7 +690,21 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
const SizedBox(height: 24),
|
||||
_buildDiscountTypeSection(),
|
||||
const SizedBox(height: 24),
|
||||
CommentsSection(comments: widget.offer.comments),
|
||||
FutureBuilder<List<CommentModel>>(
|
||||
future: _commentsFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('خطا در بارگذاری نظرات. لطفاً صفحه را رفرش کنید.'));
|
||||
}
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
return CommentsSection(comments: snapshot.data!);
|
||||
},
|
||||
),
|
||||
].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn(
|
||||
duration: 400.ms,
|
||||
curve: Curves.easeOut,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/data/models/offer_model.dart';
|
||||
import 'package:proxibuy/presentation/pages/comment_page.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
|
|
@ -30,17 +33,17 @@ class _ReservationConfirmationPageState
|
|||
Timer? _timer;
|
||||
Duration _remaining = Duration.zero;
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
StreamSubscription? _mqttSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_playSound();
|
||||
|
||||
_calculateRemainingTime();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_calculateRemainingTime();
|
||||
});
|
||||
_listenToMqtt();
|
||||
}
|
||||
|
||||
void _playSound() async {
|
||||
|
|
@ -69,14 +72,46 @@ class _ReservationConfirmationPageState
|
|||
}
|
||||
}
|
||||
|
||||
void _listenToMqtt() async {
|
||||
final mqttService = context.read<MqttService>();
|
||||
const storage = FlutterSecureStorage();
|
||||
final userID = await storage.read(key: 'userID');
|
||||
final discountId = widget.offer.id;
|
||||
|
||||
if (userID == null) {
|
||||
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
|
||||
return;
|
||||
}
|
||||
|
||||
final topic = 'user-order/$userID/$discountId';
|
||||
mqttService.subscribe(topic);
|
||||
debugPrint("✅ Subscribed to MQTT topic: $topic");
|
||||
|
||||
_mqttSubscription = mqttService.messages.listen((message) {
|
||||
debugPrint("✅ MQTT Message received on details page: $message");
|
||||
final receivedDiscountId = message['Discount'];
|
||||
if (receivedDiscountId == discountId) {
|
||||
if (mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CommentPage(discountId: discountId),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_audioPlayer.dispose();
|
||||
_mqttSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Directionality(
|
||||
|
|
@ -94,13 +129,13 @@ class _ReservationConfirmationPageState
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'تخفیف ${widget.offer.discountType} رزرو شد!',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
)
|
||||
'تخفیف ${widget.offer.discountType} رزرو شد!',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 300.ms, duration: 500.ms)
|
||||
.slideY(begin: -0.2, end: 0),
|
||||
|
|
@ -227,7 +262,7 @@ class _ReservationConfirmationPageState
|
|||
SvgPicture.asset(Assets.icons.ticketDiscount.path),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'(${(100-widget.offer.finalPrice/widget.offer.originalPrice*100).toInt()}%)',
|
||||
'(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.singleOfferType,
|
||||
|
|
@ -401,4 +436,4 @@ class _ReservationConfirmationPageState
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,14 @@ import 'package:connectivity_plus/connectivity_plus.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
|
||||
import 'package:proxibuy/presentation/pages/onboarding_page.dart';
|
||||
import 'package:proxibuy/presentation/pages/offers_page.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
|
@ -27,10 +29,25 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||
super.initState();
|
||||
Future.delayed(const Duration(seconds: 2), _startProcess);
|
||||
}
|
||||
|
||||
Future<String?> getFcmToken() async {
|
||||
FirebaseMessaging messaging = FirebaseMessaging.instance;
|
||||
try {
|
||||
String? token = await messaging.getToken();
|
||||
debugPrint("🔥 Firebase Messaging Token: $token");
|
||||
return token;
|
||||
} catch(e) {
|
||||
debugPrint("Error getting FCM token: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _startProcess() async {
|
||||
if (!mounted) return;
|
||||
|
||||
await _requestPermissions();
|
||||
|
||||
final hasInternet = await _checkInternet();
|
||||
if (!hasInternet) {
|
||||
setState(() {
|
||||
|
|
@ -59,12 +76,19 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final String? fcmToken = await getFcmToken();
|
||||
|
||||
final mqttService = context.read<MqttService>();
|
||||
final storage = const FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'accessToken');
|
||||
|
||||
if (token != null && token.isNotEmpty) {
|
||||
|
||||
if (fcmToken != null) {
|
||||
context.read<AuthBloc>().add(SendFcmTokenEvent(fcmToken: fcmToken));
|
||||
}
|
||||
|
||||
if (mqttService.isConnected) {
|
||||
_navigateToOffers();
|
||||
return;
|
||||
|
|
@ -186,4 +210,13 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
await Permission.notification.request();
|
||||
|
||||
var status = await Permission.location.request();
|
||||
if (status.isGranted) {
|
||||
await Permission.locationAlways.request();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/data/models/notification_model.dart';
|
||||
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
|
||||
import 'package:proxibuy/presentation/offer/bloc/offer_state.dart';
|
||||
import 'package:proxibuy/presentation/pages/product_detail_page.dart';
|
||||
|
||||
class NotificationPanel extends StatefulWidget {
|
||||
final VoidCallback onClose;
|
||||
final VoidCallback? onListChanged;
|
||||
|
||||
const NotificationPanel({super.key, required this.onClose, this.onListChanged});
|
||||
|
||||
@override
|
||||
State<NotificationPanel> createState() => _NotificationPanelState();
|
||||
}
|
||||
|
||||
class _NotificationPanelState extends State<NotificationPanel> {
|
||||
List<NotificationModel> _notifications = [];
|
||||
bool _isLoading = true;
|
||||
String _errorMessage = '';
|
||||
final Dio _dio = Dio();
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchNotifications();
|
||||
}
|
||||
|
||||
Future<void> _fetchNotifications() async {
|
||||
final token = await _storage.read(key: 'accessToken');
|
||||
if (token == null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = 'برای مشاهده اعلانها، لطفا ابتدا وارد شوید.';
|
||||
});
|
||||
widget.onListChanged?.call();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'https://proxybuy.liara.run/notify/get',
|
||||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && mounted) {
|
||||
final List<dynamic> data = response.data['data'] ?? [];
|
||||
setState(() {
|
||||
_notifications = data.map((json) => NotificationModel.fromJson(json)).toList();
|
||||
_isLoading = false;
|
||||
});
|
||||
widget.onListChanged?.call();
|
||||
} else if (response.statusCode == 201) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = 'اعلانی وجود ندارد';
|
||||
});
|
||||
widget.onListChanged?.call();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = 'اتصال به سرور برقرار نشد.';
|
||||
});
|
||||
widget.onListChanged?.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ignoreNotification(String notificationId) async {
|
||||
final token = await _storage.read(key: 'accessToken');
|
||||
if (token == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('خطا: شما وارد حساب کاربری نشدهاید.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'https://proxybuy.liara.run/notify/ignore/$notificationId',
|
||||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && mounted) {
|
||||
setState(() {
|
||||
_notifications.removeWhere((n) => n.id == notificationId);
|
||||
});
|
||||
widget.onListChanged?.call();
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(response.data?['message'] ?? 'خطا در حذف اعلان.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('خطا در ارتباط با سرور.')),
|
||||
);
|
||||
debugPrint('Error ignoring notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTimeAgo(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inSeconds < 60) {
|
||||
return 'همین الان';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes} دقیقه قبل';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours} ساعت قبل';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays} روز قبل';
|
||||
} else {
|
||||
return DateFormat('yyyy/MM/dd', 'fa').format(dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToOffer(NotificationModel notification) {
|
||||
if (!notification.status) return;
|
||||
|
||||
final offersState = context.read<OffersBloc>().state;
|
||||
if (offersState is OffersLoadSuccess) {
|
||||
try {
|
||||
final offer = offersState.offers.firstWhere((o) => o.id == notification.discountId);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ProductDetailPage(offer: offer),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('این تخفیف در حال حاضر در دسترس نیست.')),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('اطلاعات تخفیفها هنوز بارگذاری نشده است.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildBody(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_errorMessage.isNotEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(_errorMessage, textAlign: TextAlign.center),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_notifications.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text('اعلانی وجود ندارد.', textAlign: TextAlign.center),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
itemCount: _notifications.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildNotificationCard(_notifications[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotificationCard(NotificationModel notification) {
|
||||
final bool isExpired = !notification.status;
|
||||
final Color textColor = isExpired ? Colors.grey.shade600 : Colors.black;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
notification.description,
|
||||
style: TextStyle(fontSize: 15, height: 1.6, color: textColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_formatTimeAgo(notification.createdAt.toLocal()),
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||
),
|
||||
if (isExpired)
|
||||
ElevatedButton(
|
||||
onPressed: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
child: const Text('تخفیف تمام شد'),
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _ignoreNotification(notification.id),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red.shade700),
|
||||
child: const Text('بیخیال'),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ElevatedButton(
|
||||
onPressed: () => _navigateToOffer(notification),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.backgroundConfirm,
|
||||
foregroundColor: AppColors.selectedImg,
|
||||
side: BorderSide(color: Colors.green.shade200),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
child: const Text('بزن بریم'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/presentation/pages/comment_page.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/data/models/offer_model.dart';
|
||||
|
|
@ -25,6 +29,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
Timer? _timer;
|
||||
Duration _remaining = Duration.zero;
|
||||
Future<String>? _qrTokenFuture;
|
||||
StreamSubscription? _mqttSubscription; // برای مدیریت لیسنر MQTT
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -57,10 +62,52 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
}
|
||||
|
||||
void _toggleExpansion() {
|
||||
final isExpired = _remaining <= Duration.zero;
|
||||
if (isExpired) return;
|
||||
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
if (_isExpanded && _qrTokenFuture == null) {
|
||||
_qrTokenFuture = _generateQrToken();
|
||||
if (_isExpanded) {
|
||||
if (_qrTokenFuture == null) {
|
||||
_qrTokenFuture = _generateQrToken();
|
||||
}
|
||||
_listenToMqtt();
|
||||
} else {
|
||||
_mqttSubscription?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _listenToMqtt() async {
|
||||
final mqttService = context.read<MqttService>();
|
||||
const storage = FlutterSecureStorage();
|
||||
final userID = await storage.read(key: 'userID');
|
||||
final discountId = widget.offer.id;
|
||||
|
||||
if (userID == null) {
|
||||
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
|
||||
return;
|
||||
}
|
||||
|
||||
final topic = 'user-order/$userID/$discountId';
|
||||
mqttService.subscribe(topic);
|
||||
debugPrint("✅ Subscribed to MQTT topic: $topic");
|
||||
|
||||
|
||||
_mqttSubscription = mqttService.messages.listen((message) {
|
||||
debugPrint("✅ MQTT Message received on reserved card: $message");
|
||||
final receivedDiscountId = message['Discount'];
|
||||
|
||||
if (receivedDiscountId == discountId) {
|
||||
if (mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CommentPage(discountId: discountId),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -84,6 +131,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_mqttSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -104,24 +152,47 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
margin: EdgeInsets.zero,
|
||||
child: _buildOfferPrimaryDetails(),
|
||||
),
|
||||
_buildActionsRow(),
|
||||
_buildExpansionPanel(),
|
||||
_buildActionsRow(isExpired),
|
||||
if (!isExpired) _buildExpansionPanel(),
|
||||
],
|
||||
);
|
||||
|
||||
if (isExpired) {
|
||||
return ColorFiltered(
|
||||
colorFilter: const ColorFilter.matrix(<double>[
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]),
|
||||
child: cardContent,
|
||||
);
|
||||
}
|
||||
|
||||
return cardContent;
|
||||
return Stack(
|
||||
children: [
|
||||
if (isExpired)
|
||||
ColorFiltered(
|
||||
colorFilter: const ColorFilter.matrix(<double>[
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]),
|
||||
child: cardContent,
|
||||
)
|
||||
else
|
||||
cardContent,
|
||||
if (isExpired)
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: -35,
|
||||
child: Transform.rotate(
|
||||
angle: -45 * (3.1415926535 / 180),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 4),
|
||||
color: Colors.red,
|
||||
child: const Text(
|
||||
'منقضی شده',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOfferPrimaryDetails() {
|
||||
|
|
@ -195,13 +266,13 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildActionsRow() {
|
||||
Widget _buildActionsRow(bool isExpired) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (_remaining > Duration.zero)
|
||||
if (!isExpired)
|
||||
Column(
|
||||
children: [
|
||||
Localizations.override(
|
||||
|
|
@ -220,7 +291,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
fontSize: 20,
|
||||
color: AppColors.countdown,
|
||||
),
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
shouldShowDays: (d) => d.inDays > 0,
|
||||
shouldShowHours: (d) => d.inHours > 0,
|
||||
shouldShowMinutes: (d) => d.inSeconds > 0,
|
||||
|
|
@ -231,36 +302,30 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
],
|
||||
)
|
||||
else
|
||||
const Text(
|
||||
'منقضی شده',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
const SizedBox(height: 0),
|
||||
SizedBox(width: 10),
|
||||
if (!isExpired)
|
||||
TextButton(
|
||||
onPressed: _toggleExpansion,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_isExpanded ? 'بستن' : 'اطلاعات بیشتر',
|
||||
style: TextStyle(color: AppColors.active),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SvgPicture.asset(
|
||||
Assets.icons.arrowDown.path,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
TextButton(
|
||||
onPressed: _toggleExpansion,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_isExpanded ? 'بستن' : 'اطلاعات بیشتر',
|
||||
style: TextStyle(color: AppColors.active),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SvgPicture.asset(
|
||||
Assets.icons.arrowDown.path,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -324,6 +389,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
|
||||
Widget _buildExpansionPanel() {
|
||||
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
||||
final isExpired = _remaining <= Duration.zero;
|
||||
|
||||
return AnimatedCrossFade(
|
||||
firstChild: Container(),
|
||||
|
|
@ -339,8 +405,8 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 9,
|
||||
horizontal: 15,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.singleOfferType,
|
||||
|
|
@ -351,7 +417,8 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 17,
|
||||
fontSize: 13,
|
||||
overflow: TextOverflow.ellipsis
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -365,7 +432,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
Text(
|
||||
'(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
color: AppColors.singleOfferType,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
|
|
@ -374,7 +441,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
Text(
|
||||
formatCurrency.format(widget.offer.originalPrice),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
),
|
||||
|
|
@ -386,7 +453,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
'${formatCurrency.format(widget.offer.finalPrice)} تومان',
|
||||
style: const TextStyle(
|
||||
color: AppColors.singleOfferType,
|
||||
fontSize: 18,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
|
@ -394,39 +461,41 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromARGB(255, 246, 246, 246),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
if (!isExpired) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromARGB(255, 246, 246, 246),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: FutureBuilder<String>(
|
||||
future: _qrTokenFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: Text("خطا در ساخت کد QR")),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
return QrImageView(
|
||||
data: snapshot.data!,
|
||||
version: QrVersions.auto,
|
||||
size: 280.0,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
child: FutureBuilder<String>(
|
||||
future: _qrTokenFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: Text("خطا در ساخت کد QR")),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
return QrImageView(
|
||||
data: snapshot.data!,
|
||||
version: QrVersions.auto,
|
||||
size: 280.0,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class CustomStarRating extends StatelessWidget {
|
|||
stars.add(_buildStar(Assets.icons.star2.path));
|
||||
remaining = 0;
|
||||
} else {
|
||||
stars.add(_buildStar(Assets.icons.starHalf.path,));
|
||||
stars.add(_buildStar(Assets.icons.star2.path,));
|
||||
}
|
||||
}
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: stars);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
|
||||
const notificationChannelId = 'proxibuy_foreground_service';
|
||||
const notificationId = 888;
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void onStart(ServiceInstance service) async {
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final MqttService mqttService = MqttService();
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
if (service is AndroidServiceInstance) {
|
||||
service.setForegroundNotificationInfo(
|
||||
title: "ProxiBuy فعال است",
|
||||
content: "در حال جستجو برای تخفیف های اطراف شما...",
|
||||
);
|
||||
|
||||
service.on('stopService').listen((event) {
|
||||
service.stopSelf();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> sendGpsData() async {
|
||||
var locationStatus = await Permission.location.status;
|
||||
if (!locationStatus.isGranted) {
|
||||
debugPrint("Background Service: Location permission not granted.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
final token = await storage.read(key: 'accessToken');
|
||||
final userID = await storage.read(key: 'userID');
|
||||
|
||||
if (token != null && userID != null) {
|
||||
if (!mqttService.isConnected) {
|
||||
await mqttService.connect(token);
|
||||
final topic = 'user-proxybuy/$userID';
|
||||
mqttService.subscribe(topic);
|
||||
}
|
||||
|
||||
if (mqttService.isConnected) {
|
||||
final payload = {
|
||||
"userID": userID,
|
||||
"lat": position.latitude,
|
||||
"lng": position.longitude
|
||||
};
|
||||
mqttService.publish("proxybuy/sendGps", payload);
|
||||
debugPrint("Background Service: GPS sent successfully.");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("❌ Background Service Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
mqttService.messages.listen((data) {
|
||||
service.invoke('update', {'offers': data});
|
||||
});
|
||||
|
||||
service.on('force_refresh').listen((event) async {
|
||||
debugPrint("✅ Background Service: Received force_refresh event.");
|
||||
await sendGpsData();
|
||||
});
|
||||
|
||||
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
debugPrint("✅ Background Service: Sending location via periodic timer...");
|
||||
await sendGpsData();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initializeService() async {
|
||||
final service = FlutterBackgroundService();
|
||||
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
notificationChannelId,
|
||||
'ProxiBuy Background Service',
|
||||
description: 'This channel is used for location service notifications.',
|
||||
importance: Importance.low,
|
||||
);
|
||||
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
await service.configure(
|
||||
androidConfiguration: AndroidConfiguration(
|
||||
onStart: onStart,
|
||||
isForegroundMode: true,
|
||||
autoStart: true,
|
||||
notificationChannelId: notificationChannelId,
|
||||
initialNotificationTitle: 'ProxiBuy فعال است',
|
||||
initialNotificationContent: 'در حال جستجو برای تخفیفهای اطراف شما...',
|
||||
foregroundServiceNotificationId: notificationId,
|
||||
),
|
||||
iosConfiguration: IosConfiguration(
|
||||
autoStart: true,
|
||||
onForeground: onStart,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
|
||||
const notificationChannelId = 'proxibuy_foreground_service';
|
||||
const notificationId = 888;
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void onStart(ServiceInstance service) async {
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final MqttService mqttService = MqttService();
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
if (service is AndroidServiceInstance) {
|
||||
service.setForegroundNotificationInfo(
|
||||
title: "ProxiBuy فعال است",
|
||||
content: "در حال یافتن تخفیف های اطراف شما...",
|
||||
);
|
||||
|
||||
service.on('setAsForeground').listen((event) {
|
||||
service.setAsForegroundService();
|
||||
});
|
||||
|
||||
service.on('setAsBackground').listen((event) {
|
||||
service.setAsBackgroundService();
|
||||
});
|
||||
}
|
||||
|
||||
service.on('stopService').listen((event) {
|
||||
service.stopSelf();
|
||||
});
|
||||
|
||||
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
debugPrint("✅ Background Service is running and sending location...");
|
||||
|
||||
var locationStatus = await Permission.location.status;
|
||||
var locationAlwaysStatus = await Permission.locationAlways.status;
|
||||
if (!locationStatus.isGranted || !locationAlwaysStatus.isGranted) {
|
||||
debugPrint("Background Service: Permissions not granted. Task skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||
final token = await storage.read(key: 'accessToken');
|
||||
final userID = await storage.read(key: 'userID');
|
||||
|
||||
if (token != null && token.isNotEmpty && userID != null) {
|
||||
if (!mqttService.isConnected) {
|
||||
await mqttService.connect(token);
|
||||
}
|
||||
|
||||
if (mqttService.isConnected) {
|
||||
final payload = {"userID": userID, "lat": position.latitude, "lng": position.longitude};
|
||||
mqttService.publish("proxybuy/sendGps", payload);
|
||||
debugPrint("Background Service: GPS sent successfully.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initializeService() async {
|
||||
final service = FlutterBackgroundService();
|
||||
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
notificationChannelId,
|
||||
'ProxiBuy Background Service',
|
||||
description: 'This channel is used for location service notifications.',
|
||||
importance: Importance.low,
|
||||
);
|
||||
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
await service.configure(
|
||||
androidConfiguration: AndroidConfiguration(
|
||||
onStart: onStart,
|
||||
isForegroundMode: true,
|
||||
autoStart: true,
|
||||
notificationChannelId: notificationChannelId,
|
||||
initialNotificationTitle: 'ProxiBuy فعال است',
|
||||
initialNotificationContent: 'در حال جستجو برای تخفیفهای اطراف شما...',
|
||||
foregroundServiceNotificationId: notificationId,
|
||||
),
|
||||
iosConfiguration: IosConfiguration(
|
||||
autoStart: true,
|
||||
onForeground: onStart,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:mqtt_client/mqtt_client.dart';
|
||||
import 'package:mqtt_client/mqtt_server_client.dart';
|
||||
|
||||
class MqttService {
|
||||
MqttServerClient? client;
|
||||
final String server = '5.75.197.180';
|
||||
final String server = '62.60.214.99';
|
||||
final int port = 1883;
|
||||
final StreamController<Map<String, dynamic>> _messageStreamController =
|
||||
StreamController.broadcast();
|
||||
|
|
@ -20,7 +21,9 @@ class MqttService {
|
|||
}
|
||||
|
||||
Future<void> connect(String token) async {
|
||||
final String clientId =
|
||||
const storage = FlutterSecureStorage();
|
||||
final userID = await storage.read(key: 'userID');
|
||||
final String clientId = userID??
|
||||
'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0');
|
||||
final String username = 'ignored';
|
||||
final String password = token;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import connectivity_plus
|
|||
import file_selector_macos
|
||||
import firebase_auth
|
||||
import firebase_core
|
||||
import firebase_crashlytics
|
||||
import firebase_messaging
|
||||
import firebase_storage
|
||||
import flutter_local_notifications
|
||||
import flutter_localization
|
||||
import flutter_secure_storage_macos
|
||||
import geolocator_apple
|
||||
|
|
@ -28,7 +31,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
|
|
|
|||
148
pubspec.lock
148
pubspec.lock
|
|
@ -33,6 +33,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.4.5"
|
||||
animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: animations
|
||||
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -521,6 +529,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.24.1"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.10"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.10"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.10"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.10"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.10"
|
||||
firebase_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -566,6 +614,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
flutter_background_service:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_background_service
|
||||
sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
flutter_background_service_android:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_background_service_android
|
||||
sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
flutter_background_service_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_background_service_ios
|
||||
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
flutter_background_service_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_background_service_platform_interface
|
||||
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.2"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -614,6 +694,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "20ca0a9c82ce0c855ac62a2e580ab867f3fbea82680a90647f7953832d0850ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.4.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.0"
|
||||
flutter_local_notifications_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
flutter_localization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -635,6 +747,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.28"
|
||||
flutter_rating_bar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_rating_bar
|
||||
sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1121,10 +1241,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f"
|
||||
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.0+1"
|
||||
version: "12.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1333,6 +1453,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
skeletons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: skeletons
|
||||
sha256: "5b2d08ae7f908ee1f7007ca99f8dcebb4bfc1d3cb2143dec8d112a5be5a45c8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -1466,6 +1594,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1642,6 +1778,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
workmanager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: workmanager
|
||||
sha256: "746a50c535af15b6dc225abbd9b52ab272bcd292c535a104c54b5bc02609c38a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
11
pubspec.yaml
11
pubspec.yaml
|
|
@ -42,7 +42,7 @@ dependencies:
|
|||
flutter_gen: ^5.10.0
|
||||
country_picker: ^2.0.27
|
||||
geolocator: ^14.0.1
|
||||
permission_handler: ^12.0.0+1
|
||||
permission_handler: ^12.0.1
|
||||
cached_network_image: ^3.4.1
|
||||
collection: ^1.19.1
|
||||
shared_preferences: ^2.5.3
|
||||
|
|
@ -62,6 +62,15 @@ dependencies:
|
|||
dart_jsonwebtoken: ^3.2.0
|
||||
audioplayers: ^6.5.0
|
||||
intl: ^0.19.0
|
||||
flutter_background_service: ^5.1.0
|
||||
flutter_background_service_android: ^6.3.1
|
||||
flutter_local_notifications: ^19.4.0
|
||||
workmanager: ^0.7.0
|
||||
firebase_messaging: ^15.2.10
|
||||
firebase_crashlytics: ^4.3.10
|
||||
flutter_rating_bar: ^4.0.1
|
||||
animations: ^2.0.11
|
||||
skeletons: ^0.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
flutter_local_notifications_windows
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
|||
Loading…
Reference in New Issue