This commit is contained in:
mohamadmahdi jebeli 2025-07-22 15:06:25 +03:30
parent 61611fef04
commit 690813829d
44 changed files with 1176 additions and 517 deletions

View File

@ -1,5 +1,8 @@
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
@ -24,7 +27,7 @@ android {
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 = flutter.minSdkVersion
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@ -0,0 +1,48 @@
{
"project_info": {
"project_number": "800272350428",
"project_id": "proxibuy-3b5e0",
"storage_bucket": "proxibuy-3b5e0.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:800272350428:android:d6af1e013bae09d9c78819",
"android_client_info": {
"package_name": "com.example.business_panel"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCMGweIbZBsFNXabKRJJJcVLPwcmqhuSwg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:800272350428:android:6cbc063052753bc9c78819",
"android_client_info": {
"package_name": "com.example.proxibuy"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCMGweIbZBsFNXabKRJJJcVLPwcmqhuSwg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -3,6 +3,7 @@
<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" />
<application

View File

@ -19,6 +19,9 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}

1
firebase.json Normal file
View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"proxibuy-3b5e0","appId":"1:800272350428:android:6cbc063052753bc9c78819","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"proxibuy-3b5e0","configurations":{"android":"1:800272350428:android:6cbc063052753bc9c78819"}}}}}}

View File

@ -0,0 +1,7 @@
class ApiConfig {
static const String baseUrl = "https://proxybuy.liara.run";
static const String sendCode = "/login/sendcode";
static const String verifyCode = "/login/getcode";
static const String updateUser = "/user/updateName";
static const String updateCategories = "/user/favoriteCategory";
}

View File

@ -5,7 +5,7 @@ import 'package:proxibuy/data/models/working_hours.dart';
abstract class OfferDataSource {
Future<List<OfferModel>> getNearbyOffers();
Future<OfferModel?> getOfferById(String id); // <<<<<<< جدید
Future<OfferModel?> getOfferById(String id);
}
class MockOfferDataSource implements OfferDataSource {
@ -71,7 +71,7 @@ class MockOfferDataSource implements OfferDataSource {
name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
),
comments: [ // <-- بخش نظرات اضافه شد
comments: [
CommentModel(
id: 'c1',
userName: 'سارا رضایی',
@ -154,7 +154,7 @@ class MockOfferDataSource implements OfferDataSource {
name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
),
comments: [ // <-- بخش نظرات اضافه شد
comments: [
CommentModel(
id: 'c1',
userName: 'سارا رضایی',
@ -218,7 +218,7 @@ class MockOfferDataSource implements OfferDataSource {
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
],
),
WorkingHours(day: 'جمعه', shifts: []), // تعطیل
WorkingHours(day: 'جمعه', shifts: []),
],
discountType: 'رفیق‌بازی',
isOpen: true,
@ -237,7 +237,7 @@ class MockOfferDataSource implements OfferDataSource {
name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
),
comments: [ // <-- بخش نظرات اضافه شد
comments: [
CommentModel(
id: 'c1',
userName: 'سارا رضایی',
@ -320,7 +320,7 @@ class MockOfferDataSource implements OfferDataSource {
name: "رفیق‌بازی",
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.",
),
comments: [ // <-- بخش نظرات اضافه شد
comments: [
CommentModel(
id: 'c1',
userName: 'سارا رضایی',

View File

@ -21,7 +21,6 @@ class OfferRepository {
return filteredOffers;
}
Future<OfferModel?> fetchOfferById(String id) async {
// در آینده این متد میتواند یک درخواست API برای گرفتن اطلاعات یک محصول خاص ارسال کند
return _offerDataSource.getOfferById(id);
}
}

View File

@ -1,7 +1,7 @@
import 'package:proxibuy/core/gen/assets.gen.dart';
class CategoryEntity {
final int id;
final String id;
final String name;
final SvgGenImage icon;

View File

@ -1,4 +1,3 @@
// lib/domain/entities/onboarding_entity.dart
class OnboardingEntity {
final String imagePath;
final String title;

View File

@ -28,7 +28,6 @@ class AddPhotoCubit extends Cubit<AddPhotoState> {
}
}
// متد را طوری تغییر دادیم که منبع عکس را به عنوان ورودی بگیرد
Future<void> pickImage(ImageSource source) async {
final currentState = state;
if (currentState is AddPhotoLoaded) {

62
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,62 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for ios - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyCMGweIbZBsFNXabKRJJJcVLPwcmqhuSwg',
appId: '1:800272350428:android:6cbc063052753bc9c78819',
messagingSenderId: '800272350428',
projectId: 'proxibuy-3b5e0',
storageBucket: 'proxibuy-3b5e0.firebasestorage.app',
);
}

View File

@ -1,3 +1,4 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -5,17 +6,23 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:proxibuy/data/models/datasources/offer_data_source.dart';
import 'package:proxibuy/data/repositories/offer_repository.dart';
import 'package:proxibuy/firebase_options.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:proxibuy/presentation/pages/otp_page.dart';
import 'package:proxibuy/presentation/pages/user_info_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
// import 'package:proxibuy/services/mqtt_service.dart';
import 'core/config/app_colors.dart';
import 'presentation/pages/onboarding_page.dart';
import 'package:proxibuy/presentation/pages/splash_screen.dart'; // <--- ایمپورت جدید
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
Animate.restartOnHotReload = true;
runApp(const MyApp());
}
@ -24,118 +31,136 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
return MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
),
RepositoryProvider<OfferRepository>(
create:
(context) =>
OfferRepository(offerDataSource: MockOfferDataSource()),
create: (context) =>
OfferRepository(offerDataSource: MockOfferDataSource()),
),
BlocProvider<ReservationCubit>(
create: (context) => ReservationCubit(),
),
BlocProvider<OffersBloc>(
create: (context) => OffersBloc(
offerRepository: context.read<OfferRepository>(),
),
),
BlocProvider<NotificationPreferencesBloc>(
create: (context) => NotificationPreferencesBloc(),
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(create: (context) => AuthBloc()),
BlocProvider<NotificationPreferencesBloc>(
create:
(context) =>
NotificationPreferencesBloc()..add(LoadCategories()),
),
BlocProvider<OffersBloc>(
create:
(context) => OffersBloc(
offerRepository: context.read<OfferRepository>(),
),
),
BlocProvider<ReservationCubit>(
create: (context) => ReservationCubit(),
),
child: MaterialApp(
title: 'Proxibuy',
debugShowCheckedModeBanner: false,
home: const SplashScreen(), // <--- استفاده از صفحه اسپلش
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
child: MaterialApp(
title: 'Proxibuy',
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('fa')],
locale: const Locale('fa'),
theme: ThemeData(
fontFamily: 'Dana',
scaffoldBackgroundColor: Colors.white,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
primary: AppColors.primary,
surface: Colors.white,
supportedLocales: const [Locale('fa')],
locale: const Locale('fa'),
theme: ThemeData(
fontFamily: 'Dana',
scaffoldBackgroundColor: Colors.white,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
primary: AppColors.primary,
surface: Colors.white,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
floatingLabelBehavior: FloatingLabelBehavior.always,
contentPadding: const EdgeInsets.symmetric(
vertical: 18,
horizontal: 20,
),
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 0,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.border),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
floatingLabelBehavior: FloatingLabelBehavior.always,
contentPadding: const EdgeInsets.symmetric(
vertical: 18,
horizontal: 20,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: AppColors.primary,
width: 2,
),
),
labelStyle: const TextStyle(color: Colors.black),
// ignore: deprecated_member_use
hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.border),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.black, // رنگ متن دکمه Outlined
padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: Colors.grey),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
textStyle: const TextStyle(
fontFamily: 'Dana',
fontSize: 16,
color: Colors.black,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.primary, width: 2),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.button,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
textStyle: const TextStyle(
fontFamily: 'Dana',
fontSize: 16,
fontWeight: FontWeight.bold,
),
labelStyle: const TextStyle(color: Colors.black),
// ignore: deprecated_member_use
hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: Colors.grey),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
textStyle: const TextStyle(
fontFamily: 'Dana',
fontSize: 16,
color: Colors.black,
),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.button,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
textStyle: const TextStyle(
fontFamily: 'Dana',
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
home: const OnboardingPage(),
),
),
);
}
}
class AppRouter extends StatelessWidget {
const AppRouter({super.key});
@override
Widget build(BuildContext context) {
final authState = context.select((AuthBloc bloc) => bloc.state);
if (authState is AuthCodeSentSuccess) {
return OtpPage(
phoneNumber: "+${authState.countryCode}${authState.phone}",
phone: authState.phone,
countryCode: authState.countryCode,
);
}
if (authState is AuthLoading) {
final currentState = context.read<AuthBloc>().state;
if (currentState is! AuthCodeSentSuccess) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}
if (authState is AuthSuccess) {
return const OffersPage();
}
if (authState is AuthNeedsInfo) {
return const UserInfoPage();
}
return const OnboardingPage();
}
}

View File

@ -1,43 +1,116 @@
// ignore: depend_on_referenced_packages
import 'package:bloc/bloc.dart';
// ignore: depend_on_referenced_packages
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:meta/meta.dart';
import 'dart:async';
import 'package:proxibuy/core/config/api_config.dart';
part 'auth_event.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
late final Dio _dio;
final _storage = const FlutterSecureStorage();
AuthBloc() : super(AuthInitial()) {
on<SendOTPEvent>((event, emit) async {
emit(AuthLoading());
await Future.delayed(const Duration(seconds: 1));
if (event.phoneNumber.isNotEmpty) {
emit(AuthCodeSentSuccess());
} else {
emit(AuthFailure('شماره موبایل معتبر نیست.'));
}
});
_dio = Dio();
_dio.interceptors.add(
LogInterceptor(
requestHeader: true,
requestBody: true,
responseBody: true,
error: true,
),
);
on<VerifyOTPEvent>((event, emit) async {
emit(AuthLoading());
await Future.delayed(const Duration(seconds: 1));
if (event.otp == '12345') {
emit(AuthVerified());
} else {
emit(AuthFailure('کد تایید صحیح نمی‌باشد.'));
}
});
on<SaveUserInfoEvent>((event, emit) async {
emit(AuthLoading());
await Future.delayed(const Duration(milliseconds: 500));
if (event.name.trim().isEmpty) {
emit(AuthFailure('لطفاً نام خود را وارد کنید.'));
} else {
emit(UserInfoSaved());
}
});
on<CheckAuthStatusEvent>(_onCheckAuthStatus);
on<SendOTPEvent>(_onSendOTP);
on<VerifyOTPEvent>(_onVerifyOTP);
on<UpdateUserInfoEvent>(_onUpdateUserInfo);
on<LogoutEvent>(_onLogout);
}
}
Future<void> _onCheckAuthStatus(
CheckAuthStatusEvent event, Emitter<AuthState> emit) async {
final token = await _storage.read(key: 'accessToken');
if (token != null && token.isNotEmpty) {
emit(AuthSuccess());
} else {
emit(AuthInitial());
}
}
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
final response = await _dio.post(
ApiConfig.baseUrl + ApiConfig.sendCode,
data: {'Phone': event.phoneNumber, 'Code': event.countryCode},
);
if (response.statusCode == 200) {
emit(AuthCodeSentSuccess(
phone: event.phoneNumber,
countryCode: event.countryCode,
));
} else {
emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد'));
}
} on DioException catch (e) {
emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
}
}
Future<void> _onVerifyOTP(VerifyOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
final response = await _dio.post(
ApiConfig.baseUrl + ApiConfig.verifyCode,
data: {
'Phone': event.phoneNumber,
'Code': event.countryCode,
'OTP': event.otp,
},
);
if (response.statusCode == 200) {
final accessToken = response.data['data']['accessToken'];
final refreshToken = response.data['data']['refreshToken'];
await _storage.write(key: 'accessToken', value: accessToken);
await _storage.write(key: 'refreshToken', value: refreshToken);
emit(AuthNeedsInfo());
} else {
emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست'));
}
} on DioException catch (e) {
emit(AuthFailure(e.response?.data['message'] ?? 'خطایی در سرور رخ داد'));
}
}
Future<void> _onUpdateUserInfo(
UpdateUserInfoEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
final token = await _storage.read(key: 'accessToken');
if (token == null) {
emit(const AuthFailure("شما وارد نشده‌اید."));
return;
}
final response = await _dio.post(
ApiConfig.baseUrl + ApiConfig.updateUser,
data: {'Name': event.name, 'Gender': event.gender},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
if (response.statusCode == 200) {
emit(AuthSuccess());
} else {
emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات'));
}
} on DioException catch (e) {
emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
}
}
Future<void> _onLogout(LogoutEvent event, Emitter<AuthState> emit) async {
await _storage.deleteAll();
emit(AuthInitial());
}
}

View File

@ -1,4 +1,3 @@
part of 'auth_bloc.dart';
@immutable
@ -6,19 +5,24 @@ abstract class AuthEvent {}
class SendOTPEvent extends AuthEvent {
final String phoneNumber;
SendOTPEvent({required this.phoneNumber});
final String countryCode;
SendOTPEvent({required this.phoneNumber, required this.countryCode});
}
class VerifyOTPEvent extends AuthEvent {
final String phoneNumber;
final String countryCode;
final String otp;
VerifyOTPEvent({required this.otp});
VerifyOTPEvent({required this.phoneNumber, required this.countryCode, required this.otp});
}
class SaveUserInfoEvent extends AuthEvent {
class CheckAuthStatusEvent extends AuthEvent {}
class LogoutEvent extends AuthEvent {}
class UpdateUserInfoEvent extends AuthEvent {
final String name;
final String gender;
SaveUserInfoEvent({required this.name, required this.gender});
}
UpdateUserInfoEvent({required this.name, required this.gender});
}

View File

@ -1,21 +1,34 @@
part of 'auth_bloc.dart';
@immutable
abstract class AuthState {}
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthCodeSentSuccess extends AuthState {}
class AuthCodeSentSuccess extends AuthState {
final String phone;
final String countryCode;
class AuthVerified extends AuthState {}
const AuthCodeSentSuccess({required this.phone, required this.countryCode});
class UserInfoSaved extends AuthState {}
@override
List<Object?> get props => [phone, countryCode];
}
class AuthNeedsInfo extends AuthState {}
class AuthSuccess extends AuthState {}
class AuthFailure extends AuthState {
final String message;
const AuthFailure(this.message);
AuthFailure(this.message);
@override
List<Object?> get props => [message];
}

View File

@ -1,4 +1,7 @@
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/core/gen/assets.gen.dart';
import 'package:proxibuy/domain/entities/category_entity.dart';
import 'notification_preferences_event.dart';
@ -6,31 +9,37 @@ import 'notification_preferences_state.dart';
class NotificationPreferencesBloc
extends Bloc<NotificationPreferencesEvent, NotificationPreferencesState> {
final Dio _dio = Dio();
final FlutterSecureStorage _storage = const FlutterSecureStorage();
NotificationPreferencesBloc() : super(const NotificationPreferencesState()) {
on<LoadCategories>(_onLoadCategories);
on<ToggleCategorySelection>(_onToggleCategorySelection);
on<SubmitPreferences>(_onSubmitPreferences);
add(LoadCategories());
}
void _onLoadCategories(
LoadCategories event, Emitter<NotificationPreferencesState> emit) {
final categories = [
CategoryEntity(id: 1, name: 'تریا', icon: Assets.icons.teria),
CategoryEntity(id: 2, name: 'پوشاک', icon: Assets.icons.pooshak),
CategoryEntity(id: 3, name: 'فست‌فود', icon: Assets.icons.fastfood),
CategoryEntity(id: 4, name: 'کافی‌شاپ', icon: Assets.icons.coffeeshop),
CategoryEntity(id: 5, name: 'رستوران', icon: Assets.icons.resturan),
CategoryEntity(id: 6, name: 'لوازم دیجیتال', icon: Assets.icons.digital),
CategoryEntity(id: 7, name: 'کیف‌وکفش', icon: Assets.icons.kafsh),
CategoryEntity(id: 8, name: 'سینما', icon: Assets.icons.cinama),
CategoryEntity(id: 9, name: 'لوازم آرایشی', icon: Assets.icons.arayesh),
CategoryEntity(id: 10, name: 'طلا و زیورآلات', icon: Assets.icons.tala),
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: "b5881239-bfd5-4c27-967a-187316a7e0b7", name: 'رستوران', icon: Assets.icons.resturan),
CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: 'فست‌فود', icon: Assets.icons.fastfood),
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: "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),
];
emit(state.copyWith(categories: categories));
}
void _onToggleCategorySelection(ToggleCategorySelection event,
Emitter<NotificationPreferencesState> emit) {
final selectedIds = Set<int>.from(state.selectedCategoryIds);
void _onToggleCategorySelection(
ToggleCategorySelection event, Emitter<NotificationPreferencesState> emit) {
final selectedIds = Set<String>.from(state.selectedCategoryIds);
if (selectedIds.contains(event.categoryId)) {
selectedIds.remove(event.categoryId);
} else {
@ -38,4 +47,34 @@ class NotificationPreferencesBloc
}
emit(state.copyWith(selectedCategoryIds: selectedIds));
}
Future<void> _onSubmitPreferences(
SubmitPreferences event, Emitter<NotificationPreferencesState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null, submissionSuccess: false));
try {
final token = await _storage.read(key: 'accessToken');
if (token == null) {
emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید."));
return;
}
final response = await _dio.post(
ApiConfig.baseUrl + ApiConfig.updateCategories,
data: {"FCategory": state.selectedCategoryIds.toList()},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
if (response.statusCode == 200) {
emit(state.copyWith(isLoading: false, submissionSuccess: true));
} else {
emit(state.copyWith(
isLoading: false,
errorMessage: response.data['message'] ?? 'خطا در ثبت اطلاعات'));
}
} on DioException catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
}
}
}

View File

@ -10,10 +10,12 @@ abstract class NotificationPreferencesEvent extends Equatable {
class LoadCategories extends NotificationPreferencesEvent {}
class ToggleCategorySelection extends NotificationPreferencesEvent {
final int categoryId;
final String categoryId;
const ToggleCategorySelection(this.categoryId);
@override
List<Object> get props => [categoryId];
}
}
class SubmitPreferences extends NotificationPreferencesEvent {}

View File

@ -1,27 +1,43 @@
import 'package:equatable/equatable.dart';
import 'package:proxibuy/domain/entities/category_entity.dart';
class NotificationPreferencesState extends Equatable {
final List<CategoryEntity> categories;
final Set<int> selectedCategoryIds;
final Set<String> selectedCategoryIds;
final bool isLoading;
final String? errorMessage;
final bool submissionSuccess;
const NotificationPreferencesState({
this.categories = const [],
this.selectedCategoryIds = const {},
this.isLoading = false,
this.errorMessage,
this.submissionSuccess = false,
});
NotificationPreferencesState copyWith({
List<CategoryEntity>? categories,
Set<int>? selectedCategoryIds,
Set<String>? selectedCategoryIds,
bool? isLoading,
String? errorMessage,
bool? submissionSuccess,
}) {
return NotificationPreferencesState(
categories: categories ?? this.categories,
selectedCategoryIds: selectedCategoryIds ?? this.selectedCategoryIds,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
submissionSuccess: submissionSuccess ?? this.submissionSuccess,
);
}
@override
List<Object> get props => [categories, selectedCategoryIds];
List<Object?> get props => [
categories,
selectedCategoryIds,
isLoading,
errorMessage,
submissionSuccess,
];
}

View File

@ -9,7 +9,6 @@ abstract class OffersEvent extends Equatable {
}
class OffersFetchRequested extends OffersEvent {
// ۱. یک فیلد برای نگهداری دستهبندیها اضافه کنید
final List<String> selectedCategories;
const OffersFetchRequested({required this.selectedCategories});

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/presentation/offer/bloc/widgets/offer_card.dart';
import 'package:proxibuy/presentation/pages/product_detail_page.dart'; // <-- این خط را اضافه کن
import 'package:proxibuy/presentation/pages/product_detail_page.dart';
class CategoryOffersRow extends StatelessWidget {
final String categoryTitle;

View File

@ -118,23 +118,19 @@ class _OfferCardState extends State<OfferCard> {
),
const SizedBox(height: 10),
// ================== شروع تغییرات ==================
Row(
children: [
SvgPicture.asset(Assets.icons.location.path),
const SizedBox(width: 4),
// ویجت Flexible باعث میشود که آدرس، فضای باقیمانده را پر کند
// و در صورت طولانی بودن، کوتاه شود.
Flexible(
child: Text(
widget.offer.address,
style: textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis, // نمایش سه نقطه در انتهای متن طولانی
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
// این بخش همیشه به طور کامل نمایش داده میشود
Text(
'(${widget.offer.distanceInMeters.toString()}متر تا تخفیف)',
style: textTheme.bodySmall,

View File

@ -23,7 +23,6 @@ class AddPhotoScreen extends StatelessWidget {
required this.offer,
});
// متد جدید برای نمایش منوی انتخاب
void _showImageSourceActionSheet(BuildContext context) {
showModalBottomSheet(
context: context,
@ -147,7 +146,6 @@ class AddPhotoScreen extends StatelessWidget {
context.read<ReservationCubit>().isProductReserved(productId);
if (isReserved) {
// به جای فراخوانی مستقیم، منوی انتخاب را نمایش میدهیم
_showImageSourceActionSheet(context);
} else {
_showReservationPopup(context);
@ -177,7 +175,6 @@ class AddPhotoScreen extends StatelessWidget {
}
PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
// ... این متد بدون تغییر باقی میماند
return PreferredSize(
preferredSize: const Size.fromHeight(70.0),
child: Container(
@ -222,7 +219,6 @@ class AddPhotoScreen extends StatelessWidget {
}
void _showReservationPopup(BuildContext context) {
// ... این متد بدون تغییر باقی میماند
showDialog(
context: context,
barrierDismissible: false,

View File

@ -1,11 +1,8 @@
// lib/presentation/pages/login_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:country_picker/country_picker.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/pages/otp_page.dart';
import '../../core/config/app_colors.dart';
import '../../core/gen/assets.gen.dart';
class LoginPage extends StatefulWidget {
@ -18,7 +15,16 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> {
final TextEditingController _phoneController = TextEditingController();
Country _selectedCountry = Country.parse('IR');
bool _keepSignedIn = false;
void _sendOtp() {
context.read<AuthBloc>().add(
SendOTPEvent(
phoneNumber: _phoneController.text,
countryCode: _selectedCountry.phoneCode,
),
);
}
// bool _keepSignedIn = false;
@override
void dispose() {
@ -92,17 +98,18 @@ class _LoginPageState extends State<LoginPage> {
),
),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: _keepSignedIn,
onChanged: (value) =>
setState(() => _keepSignedIn = value ?? false),
activeColor: AppColors.primary,
),
Text("مرا به خاطر بسپار", style: textTheme.bodyMedium),
],
),
// Row(
// children: [
// Checkbox(
// value: _keepSignedIn,
// onChanged:
// (value) =>
// setState(() => _keepSignedIn = value ?? false),
// activeColor: AppColors.primary,
// ),
// Text("مرا به خاطر بسپار", style: textTheme.bodyMedium),
// ],
// ),
const SizedBox(height: 24),
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
@ -115,23 +122,38 @@ class _LoginPageState extends State<LoginPage> {
);
}
if (state is AuthCodeSentSuccess) {
final fullPhoneNumber = "0${_phoneController.text}";
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
OtpPage(phoneNumber: fullPhoneNumber)));
context,
MaterialPageRoute(
builder:
(_) => OtpPage(
phoneNumber:
"${state.countryCode}${state.phone}",
phone: state.phone,
countryCode: state.countryCode,
),
),
);
}
},
builder: (context, state) {
if (state is AuthLoading) {
return const Center(child: CircularProgressIndicator());
}
bool isLoading = state is AuthLoading;
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _sendOtp,
child: const Text("کد یکبار مصرف"),
onPressed: isLoading ? null : _sendOtp,
child:
isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
)
: const Text("کد یکبار مصرف"),
),
);
},
@ -192,10 +214,4 @@ class _LoginPageState extends State<LoginPage> {
onSelect: (Country country) => setState(() => _selectedCountry = country),
);
}
void _sendOtp() {
context
.read<AuthBloc>()
.add(SendOTPEvent(phoneNumber: _phoneController.text));
}
}
}

View File

@ -5,8 +5,6 @@ import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_state.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_event.dart';
import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:proxibuy/presentation/pages/reserved_list_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
@ -18,7 +16,10 @@ class NotificationPreferencesPage extends StatelessWidget {
static Route<void> route() {
return MaterialPageRoute<void>(
builder: (_) => const NotificationPreferencesPage(),
builder: (_) => BlocProvider(
create: (context) => NotificationPreferencesBloc(),
child: const NotificationPreferencesPage(),
),
);
}
@ -96,182 +97,160 @@ class NotificationPreferencesPage extends StatelessWidget {
const SizedBox(width: 8),
],
),
body: 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,
body: BlocListener<NotificationPreferencesBloc, NotificationPreferencesState>(
listener: (context, state) {
if (state.submissionSuccess) {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(true);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const OffersPage(showDialogsOnLoad: true),
),
);
}
}
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage!),
backgroundColor: Colors.red,
),
),
const SizedBox(height: 4),
const Divider(),
RichText(
text: TextSpan(
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'دریافت اعلان',
style: TextStyle(
fontFamily: 'Dana',
fontSize: 14,
color: AppColors.hint,
height: 1.5,
fontSize: 20,
fontWeight: FontWeight.bold,
),
children: const <TextSpan>[
TextSpan(
text:
'ترجیح می‌دی از کدام دسته‌بندی‌ها اعلان تخفیف دریافت کنی؟ ',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'),
],
),
),
const SizedBox(height: 24),
Expanded(
child: BlocBuilder<
NotificationPreferencesBloc,
NotificationPreferencesState
>(
builder: (context, state) {
if (state.categories.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
const double sidePadding = 24.0;
const double crossAxisSpacing = 16.0;
const int crossAxisCount = 3;
const double mainAxisSpacing = 24.0;
const double childAspectRatio = 0.9;
final screenWidth = MediaQuery.of(context).size.width;
final totalHorizontalPadding = sidePadding * 2;
final totalSpacing = crossAxisSpacing * (crossAxisCount - 1);
final availableWidth = screenWidth - totalHorizontalPadding;
final itemWidth =
(availableWidth - totalSpacing) / crossAxisCount;
final itemHeight = itemWidth / childAspectRatio;
return SingleChildScrollView(
child: Center(
child: Wrap(
alignment: WrapAlignment.center,
spacing: crossAxisSpacing,
runSpacing: mainAxisSpacing,
children:
state.categories.map((category) {
final isSelected = state.selectedCategoryIds
.contains(category.id);
return SizedBox(
width: 100,
height: itemHeight,
child: Center(
child: CategorySelectionCard(
name: category.name,
icon: category.icon,
isSelected: isSelected,
showSelectableIndicator:
state.selectedCategoryIds.isNotEmpty,
onTap: () {
context
.read<NotificationPreferencesBloc>()
.add(
ToggleCategorySelection(
category.id,
),
);
},
),
),
);
}).toList(),
),
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: 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(),
),
);
},
),
),
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();
}
},
),
),
BlocBuilder<
NotificationPreferencesBloc,
NotificationPreferencesState
>(
builder: (context, state) {
final isEnabled = state.selectedCategoryIds.isNotEmpty;
return SizedBox(
width: double.infinity,
child:
isEnabled
? ElevatedButton(
onPressed:
isEnabled
? () async {
final selectedCategoryNames =
state.categories
.where(
(cat) => state
.selectedCategoryIds
.contains(cat.id),
)
.map((cat) => cat.name)
.toList();
final prefs =
await SharedPreferences.getInstance();
await prefs.setStringList(
'user_selected_categories',
selectedCategoryNames,
);
if (context.mounted) {
context.read<OffersBloc>().add(
OffersFetchRequested(
selectedCategories:
selectedCategoryNames,
),
);
}
if (!context.mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder:
(context) => const OffersPage(
showDialogsOnLoad: true,
),
),
);
}
: 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: const Text(
'اعمال',
style: TextStyle(
fontFamily: 'Dana',
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
)
: const SizedBox(),
);
},
),
const SizedBox(height: 20),
],
const SizedBox(height: 20),
],
),
),
),
);
}
}
}

View File

@ -1,14 +1,12 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; // <-- این خط را اضافه کن
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:geolocator/geolocator.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/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_event.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_state.dart';
@ -81,17 +79,11 @@ class _OffersPageState extends State<OffersPage> {
onPressed: () async {
final result = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder:
(_) => BlocProvider.value(
value:
context.read<NotificationPreferencesBloc>()
..add(LoadCategories()),
child: const NotificationPreferencesPage(),
),
builder: (_) => const NotificationPreferencesPage(),
),
);
if (result == true) {
if (result == true && mounted) {
_loadOffersAndPreferences();
}
},

View File

@ -1,15 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:proxibuy/data/models/datasources/offer_data_source.dart';
import 'package:proxibuy/data/repositories/offer_repository.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/pages/user_info_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import '../../core/config/app_colors.dart';
import '../../core/gen/assets.gen.dart';
import '../utils/otp_timer_helper.dart';
class OtpPage extends StatefulWidget {
final String phoneNumber;
const OtpPage({super.key, required this.phoneNumber});
final String phone;
final String countryCode;
const OtpPage({
super.key,
required this.phoneNumber,
required this.phone,
required this.countryCode,
});
@override
State<OtpPage> createState() => _OtpPageState();
@ -77,19 +90,24 @@ class _OtpPageState extends State<OtpPage> {
height: 1.5,
),
children: <TextSpan>[
const TextSpan(text: 'کد تایید به شماره ',style: TextStyle(fontSize: 15)),
const TextSpan(
text: 'کد تایید به شماره ',
style: TextStyle(fontSize: 15),
),
TextSpan(
text: widget.phoneNumber,
style: const TextStyle(
fontWeight:
FontWeight.bold,
fontSize: 15
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const TextSpan(text: ' ارسال شد.',style: TextStyle(fontSize: 15)),
const TextSpan(
text: ' ارسال شد.',
style: TextStyle(fontSize: 15),
),
],
),
textDirection: TextDirection.rtl, // جهت متن برای RichText
textDirection: TextDirection.rtl,
),
SizedBox(height: 15),
Row(
@ -133,22 +151,57 @@ class _OtpPageState extends State<OtpPage> {
_errorMessage = state.message;
});
}
if (state is AuthVerified) {
if (state is AuthNeedsInfo) {
final offerRepository = OfferRepository(
offerDataSource: MockOfferDataSource(),
);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const UserInfoPage()),
MaterialPageRoute(
builder:
(_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<AuthBloc>(),
),
BlocProvider<OffersBloc>(
create:
(_) => OffersBloc(
offerRepository: offerRepository,
),
),
BlocProvider<ReservationCubit>(
create: (_) => ReservationCubit(),
),
BlocProvider<NotificationPreferencesBloc>(
create:
(_) => NotificationPreferencesBloc(),
),
],
child: const UserInfoPage(),
),
),
(route) => false,
);
}
},
builder: (context, state) {
bool isLoading = state is AuthLoading;
if (state is AuthLoading) {
return const Center(child: CircularProgressIndicator());
}
return SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: _isOtpComplete ? _verifyOtp : null,
child: const Text("ورود"),
onPressed:
(_isOtpComplete && !isLoading) ? _verifyOtp : null,
child:
isLoading
? const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
)
: const Text("ورود"),
),
);
},
@ -268,20 +321,26 @@ class _OtpPageState extends State<OtpPage> {
void _verifyOtp() {
final otpCode = _controllers.map((c) => c.text).join();
if (otpCode.length == 5) {
context.read<AuthBloc>().add(VerifyOTPEvent(otp: otpCode));
context.read<AuthBloc>().add(
VerifyOTPEvent(
otp: otpCode,
phoneNumber: widget.phone,
countryCode: widget.countryCode,
),
);
}
}
void _resendOtp() {
setState(() {
_hasError = false;
_errorMessage = null;
for (var controller in _controllers) {
controller.clear();
}
_isOtpComplete = false;
});
context.read<AuthBloc>().add(SendOTPEvent(phoneNumber: widget.phoneNumber));
for (var controller in _controllers) {
controller.clear();
}
FocusScope.of(context).requestFocus(_focusNodes[0]);
setState(() => _isOtpComplete = false);
context.read<AuthBloc>().add(
SendOTPEvent(phoneNumber: widget.phone, countryCode: widget.countryCode),
);
_otpTimer.resetTimer();
}
}

View File

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/pages/onboarding_page.dart';
import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:proxibuy/core/gen/assets.gen.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
late final StreamSubscription _authSubscription;
@override
void initState() {
super.initState();
final authBloc = context.read<AuthBloc>();
_authSubscription = authBloc.stream.listen((state) {
_authSubscription.cancel();
if (state is AuthSuccess) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const OffersPage()),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const OnboardingPage()),
);
}
});
}
@override
void dispose() {
_authSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Assets.icons.logo.svg(height: 160),
),
);
}
}

View File

@ -1,4 +1,3 @@
// lib/presentation/pages/user_info_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
@ -15,7 +14,7 @@ class UserInfoPage extends StatefulWidget {
class _UserInfoPageState extends State<UserInfoPage> {
final _nameController = TextEditingController();
String _selectedGender = 'مرد';
String _selectedGender = 'male';
@override
void dispose() {
@ -32,8 +31,9 @@ class _UserInfoPageState extends State<UserInfoPage> {
Radio<String>(
value: value,
groupValue: _selectedGender,
onChanged: (newValue) => setState(() => _selectedGender = newValue!),
activeColor: AppColors.primary,
onChanged:
(newValue) => setState(() => _selectedGender = newValue!),
activeColor: AppColors.primary,
),
Text(title, style: const TextStyle(color: Colors.grey)),
],
@ -41,6 +41,24 @@ class _UserInfoPageState extends State<UserInfoPage> {
);
}
void _submitUserInfo() {
if (_nameController.text.isNotEmpty) {
context.read<AuthBloc>().add(
UpdateUserInfoEvent(
name: _nameController.text,
gender: _selectedGender,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('لطفا نام خود را وارد کنید.'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
@ -49,10 +67,7 @@ class _UserInfoPageState extends State<UserInfoPage> {
body: Stack(
children: [
Positioned.fill(
child: Image.asset(
Assets.images.userinfo.path,
fit: BoxFit.cover,
),
child: Image.asset(Assets.images.userinfo.path, fit: BoxFit.cover),
),
DraggableScrollableSheet(
@ -77,7 +92,7 @@ class _UserInfoPageState extends State<UserInfoPage> {
height: 5,
decoration: BoxDecoration(
// ignore: deprecated_member_use
color:Colors.grey.withOpacity(0.5),
color: Colors.grey.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
),
@ -93,7 +108,10 @@ class _UserInfoPageState extends State<UserInfoPage> {
fontSize: 20,
),
hintText: "مثلا نام کوچک شما",
hintStyle: TextStyle(fontSize: 15, color: Colors.grey),
hintStyle: TextStyle(
fontSize: 15,
color: Colors.grey,
),
),
),
const SizedBox(height: 24),
@ -109,44 +127,48 @@ class _UserInfoPageState extends State<UserInfoPage> {
spacing: 10.0,
runSpacing: 8.0,
children: [
_buildGenderRadio('مرد', 'مرد'),
_buildGenderRadio('زن', 'زن'),
_buildGenderRadio('تمایلی به پاسخ ندارم', 'نامشخص'),
_buildGenderRadio('مرد', 'male'),
_buildGenderRadio('زن', 'female'),
_buildGenderRadio('تمایلی به پاسخ ندارم', 'none'),
],
),
const SizedBox(height: 55),
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is UserInfoSaved) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NotificationPreferencesPage()),
);
} else if (state is AuthFailure) {
if (state is AuthFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
builder: (context, state) {
if (state is AuthLoading) {
return const Center(child: CircularProgressIndicator());
}
return SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm,
foregroundColor: Colors.white,
final isLoading = state is AuthLoading;
return Column(
children: [
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm,
foregroundColor: Colors.white,
),
onPressed: isLoading ? null : _submitUserInfo,
child:
isLoading
? const CircularProgressIndicator(
color: Colors.white,
)
: const Text("اعمال"),
),
),
onPressed: () {
context.read<AuthBloc>().add(SaveUserInfoEvent(
name: _nameController.text,
gender: _selectedGender,
));
},
child: const Text("اعمال"),
),
const SizedBox(height: 9),
],
);
},
),
@ -155,7 +177,9 @@ class _UserInfoPageState extends State<UserInfoPage> {
Center(
child: TextButton(
onPressed: () {
Navigator.of(context).pushReplacement(NotificationPreferencesPage.route());
Navigator.of(context).pushReplacement(
NotificationPreferencesPage.route(),
);
},
child: const Text(
"رد شدن",
@ -173,4 +197,4 @@ class _UserInfoPageState extends State<UserInfoPage> {
),
);
}
}
}

View File

@ -8,7 +8,6 @@ abstract class ProductDetailEvent extends Equatable {
List<Object> get props => [];
}
// این ایونت زمانی فراخوانی میشود که بخواهیم جزئیات یک محصول را دریافت کنیم
class ProductDetailFetchRequested extends ProductDetailEvent {
final String offerId;

View File

@ -1,4 +1,3 @@
// lib/presentation/utils/otp_timer_helper.dart
import 'dart:async';
import 'package:flutter/foundation.dart';

View File

@ -21,7 +21,6 @@ class CategorySelectionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
// رنگ بردر و متن بر اساس انتخاب شدن تغییر میکند
final textAndBorderColor = isSelected ? AppColors.confirm : AppColors.unselectedBorder;
return GestureDetector(
@ -32,7 +31,6 @@ class CategorySelectionCard extends StatelessWidget {
child: Stack(
alignment: Alignment.center,
children: [
// این ویجت تغییرات در دکوریشن را به صورت انیمیشنی نمایش میدهد
AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
@ -58,14 +56,12 @@ class CategorySelectionCard extends StatelessWidget {
),
),
// این ویجت انیمیشن بین نمایش تیک، دایره یا هیچکدام را مدیریت میکند
Positioned(
top: 5,
right: 5,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (child, animation) {
// انیمیشن محو شدن و بزرگ شدن همزمان
return FadeTransition(
opacity: animation,
child: ScaleTransition(
@ -81,7 +77,6 @@ class CategorySelectionCard extends StatelessWidget {
),
),
const SizedBox(height: 8),
// رنگ متن هم به صورت انیمیشنی (توسط بازسازی ویجت) تغییر میکند
Text(
name,
style: TextStyle(
@ -98,20 +93,17 @@ class CategorySelectionCard extends StatelessWidget {
);
}
/// این متد کمکی مشخص میکند کدام نشانگر (تیک، دایره یا هیچکدام) نمایش داده شود
Widget _buildIndicator() {
if (isSelected) {
// نمایش تیک در صورت انتخاب
return SvgPicture.asset(
Assets.icons.tickCircle.path,
key: const ValueKey('tick'), // کلید برای کارکرد صحیح AnimatedSwitcher
key: const ValueKey('tick'),
);
}
if (showSelectableIndicator) {
// نمایش دایره در صورتی که آیتمهای دیگر انتخاب شدهاند
return Container(
key: const ValueKey('circle'), // کلید برای کارکرد صحیح AnimatedSwitcher
key: const ValueKey('circle'),
width: 12,
height: 12,
decoration: BoxDecoration(
@ -122,7 +114,6 @@ class CategorySelectionCard extends StatelessWidget {
);
}
// در غیر این صورت، چیزی نمایش نده
return const SizedBox.shrink(key: ValueKey('empty'));
}
}

View File

@ -12,7 +12,6 @@ class PhotoGalleryView extends StatelessWidget {
required this.remainingPhotos,
});
// این ویجت کمکی تشخیص میدهد که عکس از فایل محلی است یا از اینترنت
Widget _buildSmartImage(String imageUrl) {
bool isFile = !imageUrl.startsWith('http');
return ClipRRect(
@ -61,10 +60,9 @@ class PhotoGalleryView extends StatelessWidget {
return const Center(child: Text("هنوز عکسی وجود ندارد."));
}
// این بخش برای جلوگیری از خطا در صورتی که تعداد عکسها کمتر از نیاز گرید باشد، اضافه شده است.
final displayUrls = List<String>.from(imageUrls);
while (displayUrls.length < 6) {
displayUrls.add('https://via.placeholder.com/200'); // یک عکس جایگزین
displayUrls.add('https://via.placeholder.com/200');
}

View File

@ -55,27 +55,27 @@ Future<void> showGPSDialog(BuildContext context) async {
onTap: () => Navigator.of(context).pop(),
child: Text("الان نه",style: TextStyle(color: AppColors.primary,fontWeight: FontWeight.bold),),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(color: AppColors.border),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(color: AppColors.border),
),
padding: const EdgeInsets.symmetric(
horizontal: 45, vertical: 7),
),
onPressed: () async {
await Geolocator.openLocationSettings();
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
},
child: const Text(
"فعالسازی",
style: TextStyle(color: Colors.white),
),
padding: const EdgeInsets.symmetric(
horizontal: 45, vertical: 7),
),
onPressed: () async {
await Geolocator.openLocationSettings();
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
},
child: const Text(
"فعالسازی",
style: TextStyle(color: Colors.white),
),
),
],
),
],

View File

@ -24,19 +24,11 @@ class OnboardingIndicatorPainter extends CustomPainter {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2;
// ================== شروع تغییرات ==================
// 1. تعریف اندازه فاصله بین قوسها (بر حسب درجه)
// این عدد را میتوانید برای کم و زیاد کردن فاصله تغییر دهید
const double gapInDegrees = 30.0;
const double gapInRadians = gapInDegrees * (pi / 180);
// 2. محاسبه طول جدید هر قوس (۹۰ درجه منهای اندازه فاصله)
const double arcAngle = (pi / 2) - gapInRadians;
// =================== پایان تغییرات ===================
final startAngles = [-pi / 2, 0.0, pi / 2, pi];
for (int i = 0; i < pageCount; i++) {
@ -44,15 +36,14 @@ class OnboardingIndicatorPainter extends CustomPainter {
..color = (i == currentPage) ? activeColor : inactiveColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round; // StrokeCap.round لبههای خط را گرد میکند که زیباتر است
..strokeCap = StrokeCap.round;
// 3. محاسبه نقطه شروع جدید (با افزودن نصف فاصله برای وسطچین شدن)
final correctedStartAngle = startAngles[i] + (gapInRadians / 2);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
correctedStartAngle, // استفاده از نقطه شروع اصلاح شده
arcAngle, // استفاده از طول قوس جدید
correctedStartAngle,
arcAngle,
false,
paint,
);

View File

@ -66,10 +66,9 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
@override
Widget build(BuildContext context) {
// ویجت اصلی به Column تغییر کرده است
return Column(
children: [
// بخش اول: کارت اصلی که فقط شامل اطلاعات محصول است
Card(
color: Colors.white,
shape: RoundedRectangleBorder(
@ -79,16 +78,14 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
elevation: 0,
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
child: _buildOfferPrimaryDetails(), // فقط اطلاعات اصلی داخل کارت
child: _buildOfferPrimaryDetails(),
),
_buildActionsRow(),
// بخش سوم: پنل باز شونده QR کد
_buildExpansionPanel(),
],
);
}
// این متد فقط اطلاعات اصلی داخل کارت را میسازد
Widget _buildOfferPrimaryDetails() {
return Padding(
padding: const EdgeInsets.fromLTRB(15, 25, 15, 25),
@ -138,7 +135,6 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
children: [
SvgPicture.asset(Assets.icons.location.path),
SizedBox(width: 8),
// برای جلوگیری از سرریز شدن متن، از Flexible استفاده میکنیم
Flexible(
child: Text(
"${widget.offer.address} (${widget.offer.distanceInMeters} متر تا تخفیف)",

View File

@ -0,0 +1,60 @@
// import 'dart:async';
// import 'dart:math';
// import 'package:mqtt_client/mqtt_client.dart';
// import 'package:mqtt_client/mqtt_server_client.dart';
// class MqttService {
// late MqttServerClient client;
// final String server = '5.75.200.241';
// final int port = 1883;
// Future<void> connect(String token) async {
// // 1. معادلسازی پارامترها
// final String clientId = 'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0');
// final String username = 'ignored';
// final String password = token; // توکن شما مستقیماً به عنوان پسورد در نظر گرفته میشود
// // 2. ساخت کلاینت
// client = MqttServerClient.withPort(server, clientId, port);
// client.logging(on: true);
// client.keepAlivePeriod = 60;
// client.autoReconnect = false; // معادل reconnectPeriod: 0
// client.setProtocolV311();
// // 3. ساخت پیام اتصال با پارامترهای تعریف شده
// final connMessage = MqttConnectMessage()
// .withClientIdentifier(clientId)
// .startClean()
// .authenticateAs(username, password); // ارسال نام کاربری و رمز عبور (توکن)
// client.connectionMessage = connMessage;
// // 4. تعریف Callbackها و اتصال
// client.onConnected = () {
// print('✅ MQTT Connected');
// client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) {
// final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage;
// final String payload =
// MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
// print('Received message: "$payload" from topic: ${c[0].topic}');
// });
// client.subscribe('test/topic', MqttQos.atLeastOnce);
// };
// client.onDisconnected = () {
// print('❌ MQTT Disconnected');
// };
// client.onSubscribed = (String topic) {
// print('✅ Subscribed to $topic');
// };
// try {
// await client.connect();
// } catch (e) {
// print('Exception: $e');
// client.disconnect();
// }
// }
// }

View File

@ -8,6 +8,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_localization/flutter_localization_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <maps_launcher/maps_launcher_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_localization_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin");
flutter_localization_plugin_register_with_registrar(flutter_localization_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) maps_launcher_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MapsLauncherPlugin");
maps_launcher_plugin_register_with_registrar(maps_launcher_registrar);

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_localization
flutter_secure_storage_linux
maps_launcher
url_launcher_linux
)

View File

@ -5,8 +5,13 @@
import FlutterMacOS
import Foundation
import cloud_firestore
import file_selector_macos
import firebase_auth
import firebase_core
import firebase_storage
import flutter_localization
import flutter_secure_storage_macos
import geolocator_apple
import maps_launcher
import path_provider_foundation
@ -15,8 +20,13 @@ import sqflite_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "82.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
url: "https://pub.dev"
source: hosted
version: "1.3.59"
analyzer:
dependency: transitive
description:
@ -169,6 +177,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
cloud_firestore:
dependency: "direct main"
description:
name: cloud_firestore
sha256: "2d33da4465bdb81b6685c41b535895065adcb16261beb398f5f3bbc623979e9c"
url: "https://pub.dev"
source: hosted
version: "5.6.12"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
sha256: "413c4e01895cf9cb3de36fa5c219479e06cd4722876274ace5dfc9f13ab2e39b"
url: "https://pub.dev"
source: hosted
version: "6.6.12"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
sha256: c1e30fc4a0fcedb08723fb4b1f12ee4e56d937cbf9deae1bda43cbb6367bb4cf
url: "https://pub.dev"
source: hosted
version: "4.4.12"
code_builder:
dependency: transitive
description:
@ -273,6 +305,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
event_bus:
dependency: transitive
description:
name: event_bus
sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
fake_async:
dependency: transitive
description:
@ -329,6 +369,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff"
url: "https://pub.dev"
source: hosted
version: "5.7.0"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386"
url: "https://pub.dev"
source: hosted
version: "7.7.3"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e
url: "https://pub.dev"
source: hosted
version: "5.15.3"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
url: "https://pub.dev"
source: hosted
version: "3.15.2"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
url: "https://pub.dev"
source: hosted
version: "2.24.1"
firebase_storage:
dependency: "direct main"
description:
name: firebase_storage
sha256: "958fc88a7ef0b103e694d30beed515c8f9472dde7e8459b029d0e32b8ff03463"
url: "https://pub.dev"
source: hosted
version: "12.4.10"
firebase_storage_platform_interface:
dependency: transitive
description:
name: firebase_storage_platform_interface
sha256: d2661c05293c2a940c8ea4bc0444e1b5566c79dd3202c2271140c082c8cd8dd4
url: "https://pub.dev"
source: hosted
version: "5.2.10"
firebase_storage_web:
dependency: transitive
description:
name: firebase_storage_web
sha256: "629a557c5e1ddb97a3666cbf225e97daa0a66335dbbfdfdce113ef9f881e833f"
url: "https://pub.dev"
source: hosted
version: "3.10.17"
fixnum:
dependency: transitive
description:
@ -419,6 +531,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_shaders:
dependency: transitive
description:
@ -649,10 +809,10 @@ packages:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.6.7"
json_annotation:
dependency: transitive
description:
@ -741,6 +901,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mqtt_client:
dependency: "direct main"
description:
name: mqtt_client
sha256: "85fa7e9aad03fbd6a54fcaf46127579f3e049b4834557a06e49aea7869b04c94"
url: "https://pub.dev"
source: hosted
version: "10.8.0"
nested:
dependency: transitive
description:
@ -1346,6 +1514,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.dev"
source: hosted
version: "5.13.0"
xdg_directories:
dependency: transitive
description:

View File

@ -52,6 +52,12 @@ dependencies:
qr_flutter: ^4.1.0
flutter_staggered_grid_view: ^0.7.0
image_picker: ^1.1.2
mqtt_client: ^10.8.0
firebase_core: ^3.15.2
firebase_auth: ^5.7.0
cloud_firestore: ^5.6.12
firebase_storage: ^12.4.10
flutter_secure_storage: ^9.2.4
dev_dependencies:
flutter_test:

View File

@ -6,18 +6,33 @@
#include "generated_plugin_registrant.h"
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_auth/firebase_auth_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <firebase_storage/firebase_storage_plugin_c_api.h>
#include <flutter_localization/flutter_localization_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <geolocator_windows/geolocator_windows.h>
#include <maps_launcher/maps_launcher_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
CloudFirestorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseAuthPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
FirebaseStoragePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi"));
FlutterLocalizationPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
MapsLauncherPluginRegisterWithRegistrar(

View File

@ -3,8 +3,13 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
cloud_firestore
file_selector_windows
firebase_auth
firebase_core
firebase_storage
flutter_localization
flutter_secure_storage_windows
geolocator_windows
maps_launcher
permission_handler_windows