diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1746dcf..987642b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
+
+
diff --git a/assets/icons/arrow-up.svg b/assets/icons/arrow-up.svg
new file mode 100644
index 0000000..27f0bd8
--- /dev/null
+++ b/assets/icons/arrow-up.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/camera.svg b/assets/icons/camera.svg
new file mode 100644
index 0000000..46b1426
--- /dev/null
+++ b/assets/icons/camera.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/gallery-add.svg b/assets/icons/gallery-add.svg
new file mode 100644
index 0000000..83219b2
--- /dev/null
+++ b/assets/icons/gallery-add.svg
@@ -0,0 +1,7 @@
+
diff --git a/lib/core/config/app_colors.dart b/lib/core/config/app_colors.dart
index 6caa5b8..5fbbd02 100644
--- a/lib/core/config/app_colors.dart
+++ b/lib/core/config/app_colors.dart
@@ -16,4 +16,5 @@ class AppColors {
static const Color countdown = Color.fromARGB(255, 54, 124, 57);
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);
}
\ No newline at end of file
diff --git a/lib/core/gen/assets.gen.dart b/lib/core/gen/assets.gen.dart
index af834ff..5c5f752 100644
--- a/lib/core/gen/assets.gen.dart
+++ b/lib/core/gen/assets.gen.dart
@@ -75,15 +75,24 @@ class $AssetsIconsGen {
/// File path: assets/icons/arayesh.svg
SvgGenImage get arayesh => const SvgGenImage('assets/icons/arayesh.svg');
+ /// File path: assets/icons/arrow-down.svg
+ SvgGenImage get arrowDown => const SvgGenImage('assets/icons/arrow-down.svg');
+
/// File path: assets/icons/arrow-left.svg
SvgGenImage get arrowLeft => const SvgGenImage('assets/icons/arrow-left.svg');
+ /// File path: assets/icons/arrow-up.svg
+ SvgGenImage get arrowUp => const SvgGenImage('assets/icons/arrow-up.svg');
+
/// File path: assets/icons/back.svg
SvgGenImage get back => const SvgGenImage('assets/icons/back.svg');
/// File path: assets/icons/backArrow.svg
SvgGenImage get backArrow => const SvgGenImage('assets/icons/backArrow.svg');
+ /// File path: assets/icons/camera.svg
+ SvgGenImage get camera => const SvgGenImage('assets/icons/camera.svg');
+
/// File path: assets/icons/card-pos.svg
SvgGenImage get cardPos => const SvgGenImage('assets/icons/card-pos.svg');
@@ -113,6 +122,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/fastfood.svg
SvgGenImage get fastfood => const SvgGenImage('assets/icons/fastfood.svg');
+ /// File path: assets/icons/gallery-add.svg
+ SvgGenImage get galleryAdd =>
+ const SvgGenImage('assets/icons/gallery-add.svg');
+
/// File path: assets/icons/global-search.svg
SvgGenImage get globalSearch =>
const SvgGenImage('assets/icons/global-search.svg');
@@ -222,9 +235,12 @@ class $AssetsIconsGen {
vector,
addImg,
arayesh,
+ arrowDown,
arrowLeft,
+ arrowUp,
back,
backArrow,
+ camera,
cardPos,
cinama,
clock,
@@ -234,6 +250,7 @@ class $AssetsIconsGen {
edit,
error,
fastfood,
+ galleryAdd,
globalSearch,
kafsh,
location,
diff --git a/lib/features/add_photo/cubit/add_photo_cubit.dart b/lib/features/add_photo/cubit/add_photo_cubit.dart
new file mode 100644
index 0000000..393b68e
--- /dev/null
+++ b/lib/features/add_photo/cubit/add_photo_cubit.dart
@@ -0,0 +1,49 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:image_picker/image_picker.dart';
+
+part 'add_photo_state.dart';
+
+class AddPhotoCubit extends Cubit {
+ AddPhotoCubit() : super(AddPhotoInitial());
+
+ final ImagePicker _picker = ImagePicker();
+
+ void fetchPhotos() {
+ emit(AddPhotoLoading());
+ try {
+ Future.delayed(const Duration(seconds: 1), () {
+ final mockImageUrls = [
+ 'https://picsum.photos/seed/a/400/600',
+ 'https://picsum.photos/seed/b/200/200',
+ 'https://picsum.photos/seed/c/200/200',
+ 'https://picsum.photos/seed/d/400/200',
+ 'https://picsum.photos/seed/e/200/200',
+ 'https://picsum.photos/seed/f/200/200',
+ ];
+ emit(AddPhotoLoaded(mockImageUrls, 10));
+ });
+ } catch (e) {
+ emit(AddPhotoError('خطا در بارگذاری تصاویر'));
+ }
+ }
+
+ // ✅ متد را طوری تغییر دادیم که منبع عکس را به عنوان ورودی بگیرد
+ Future pickImage(ImageSource source) async {
+ final currentState = state;
+ if (currentState is AddPhotoLoaded) {
+ try {
+ final XFile? pickedFile = await _picker.pickImage(source: source);
+
+ if (pickedFile != null) {
+ final updatedUrls = List.from(currentState.imageUrls)
+ ..insert(0, pickedFile.path);
+ emit(AddPhotoLoaded(updatedUrls, currentState.remainingPhotos - 1));
+ }
+ } catch (e) {
+ emit(AddPhotoError('خطا در انتخاب عکس: ${e.toString()}'));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/features/add_photo/cubit/add_photo_state.dart b/lib/features/add_photo/cubit/add_photo_state.dart
new file mode 100644
index 0000000..f9b1bb0
--- /dev/null
+++ b/lib/features/add_photo/cubit/add_photo_state.dart
@@ -0,0 +1,21 @@
+part of 'add_photo_cubit.dart';
+
+@immutable
+abstract class AddPhotoState {}
+
+class AddPhotoInitial extends AddPhotoState {}
+
+class AddPhotoLoading extends AddPhotoState {}
+
+class AddPhotoLoaded extends AddPhotoState {
+ final List imageUrls;
+ final int remainingPhotos;
+
+ AddPhotoLoaded(this.imageUrls, this.remainingPhotos);
+}
+
+class AddPhotoError extends AddPhotoState {
+ final String message;
+
+ AddPhotoError(this.message);
+}
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index 00cc52d..d17b5a7 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,11 +8,11 @@ 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'; // ✅ مسیر BLoC آفر
+import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'core/config/app_colors.dart';
import 'presentation/pages/onboarding_page.dart';
void main() {
-
Animate.restartOnHotReload = true;
runApp(const MyApp());
@@ -26,23 +26,27 @@ class MyApp extends StatelessWidget {
return MultiRepositoryProvider(
providers: [
RepositoryProvider(
- create: (context) => OfferRepository(
- offerDataSource: MockOfferDataSource(),
- ),
+ create:
+ (context) =>
+ OfferRepository(offerDataSource: MockOfferDataSource()),
),
],
child: MultiBlocProvider(
providers: [
- BlocProvider(
- create: (context) => AuthBloc(),
- ),
+ BlocProvider(create: (context) => AuthBloc()),
BlocProvider(
- create: (context) => NotificationPreferencesBloc()..add(LoadCategories()),
+ create:
+ (context) =>
+ NotificationPreferencesBloc()..add(LoadCategories()),
),
BlocProvider(
- create: (context) => OffersBloc(
- offerRepository: context.read(),
- ),
+ create:
+ (context) => OffersBloc(
+ offerRepository: context.read(),
+ ),
+ ),
+ BlocProvider(
+ create: (context) => ReservationCubit(),
),
],
child: MaterialApp(
@@ -54,9 +58,7 @@ class MyApp extends StatelessWidget {
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
- supportedLocales: const [
- Locale('fa'),
- ],
+ supportedLocales: const [Locale('fa')],
locale: const Locale('fa'),
theme: ThemeData(
@@ -76,7 +78,10 @@ class MyApp extends StatelessWidget {
filled: true,
fillColor: Colors.white,
floatingLabelBehavior: FloatingLabelBehavior.always,
- contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20),
+ contentPadding: const EdgeInsets.symmetric(
+ vertical: 18,
+ horizontal: 20,
+ ),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.border),
@@ -87,7 +92,10 @@ class MyApp extends StatelessWidget {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
- borderSide: const BorderSide(color: AppColors.primary, width: 2),
+ borderSide: const BorderSide(
+ color: AppColors.primary,
+ width: 2,
+ ),
),
labelStyle: const TextStyle(color: Colors.black),
hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)),
@@ -97,7 +105,9 @@ class MyApp extends StatelessWidget {
foregroundColor: Colors.black, // رنگ متن دکمه Outlined
padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: Colors.grey),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(50),
+ ),
textStyle: const TextStyle(
fontFamily: 'Dana',
fontSize: 16,
@@ -110,7 +120,9 @@ class MyApp extends StatelessWidget {
backgroundColor: AppColors.button,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(50),
+ ),
textStyle: const TextStyle(
fontFamily: 'Dana',
fontSize: 16,
@@ -124,4 +136,4 @@ class MyApp extends StatelessWidget {
),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/presentation/pages/add_photo_screen.dart b/lib/presentation/pages/add_photo_screen.dart
new file mode 100644
index 0000000..e0b713d
--- /dev/null
+++ b/lib/presentation/pages/add_photo_screen.dart
@@ -0,0 +1,346 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_animate/flutter_animate.dart';
+import 'package:flutter_bloc/flutter_bloc.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/features/add_photo/cubit/add_photo_cubit.dart';
+import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
+import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
+import 'package:proxibuy/presentation/widgets/flutter_staggered_grid_view.dart';
+import 'package:image_picker/image_picker.dart'; // ✅ ایمپورت جدید
+
+class AddPhotoScreen extends StatelessWidget {
+ final String storeName;
+ final String productId;
+ final OfferModel offer;
+
+ const AddPhotoScreen({
+ super.key,
+ required this.storeName,
+ required this.productId,
+ required this.offer,
+ });
+
+ // ✅ متد جدید برای نمایش منوی انتخاب
+ void _showImageSourceActionSheet(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
+ ),
+ builder: (bottomSheetContext) {
+ return SafeArea(
+ child: Wrap(
+ children: [
+ ListTile(
+ leading: const Icon(Icons.photo_library, color: AppColors.primary),
+ title: const Text('انتخاب از گالری'),
+ onTap: () {
+ Navigator.of(bottomSheetContext).pop();
+ context.read().pickImage(ImageSource.gallery);
+ },
+ ),
+ ListTile(
+ leading: const Icon(Icons.camera_alt, color: AppColors.primary),
+ title: const Text('گرفتن عکس با دوربین'),
+ onTap: () {
+ Navigator.of(bottomSheetContext).pop();
+ context.read().pickImage(ImageSource.camera);
+ },
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider(
+ create: (context) => AddPhotoCubit()..fetchPhotos(),
+ child: Builder(builder: (context) {
+ return Scaffold(
+ appBar: _buildCustomAppBar(context),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ SizedBox(height: 10,),
+ _buildHeader()
+ .animate()
+ .fadeIn(duration: 500.ms)
+ .slideY(begin: -0.2, curve: Curves.easeOut),
+ const SizedBox(height: 24),
+ SizedBox(
+ child: BlocBuilder(
+ builder: (context, state) {
+ if (state is AddPhotoLoading) {
+ return const Center(child: CircularProgressIndicator());
+ }
+ if (state is AddPhotoLoaded) {
+ return PhotoGalleryView(
+ imageUrls: state.imageUrls,
+ remainingPhotos: state.remainingPhotos,
+ ).animate().scale(
+ delay: 200.ms,
+ duration: 400.ms,
+ curve: Curves.easeOutBack);
+ }
+ if (state is AddPhotoError) {
+ return Center(child: Text(state.message));
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ ),
+ const SizedBox(height: 24),
+ _buildUploadButton(context)
+ .animate()
+ .fadeIn(delay: 400.ms)
+ .slideY(begin: 0.5, curve: Curves.easeOut),
+ ],
+ ),
+ ),
+ );
+ }),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ SvgPicture.asset(
+ Assets.icons.shop.path,
+ height: 30,
+ colorFilter: const ColorFilter.mode(
+ Color.fromARGB(255, 95, 95, 95),
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Text(
+ storeName,
+ style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ 'یه عکس جذاب از محصولی که از ما خریدی بارگذاری کن تا عضو کلاب مشتریان وفادارمون بشی!',
+ style: TextStyle(fontSize: 16, color: Colors.black, height: 1.5),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildUploadButton(BuildContext context) {
+ return ElevatedButton(
+ onPressed: () {
+ final isReserved =
+ context.read().isProductReserved(productId);
+
+ if (isReserved) {
+ // ✅ به جای فراخوانی مستقیم، منوی انتخاب را نمایش میدهیم
+ _showImageSourceActionSheet(context);
+ } else {
+ _showReservationPopup(context);
+ }
+ },
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ backgroundColor: AppColors.uploadElevated,
+ side: BorderSide(color: AppColors.active, width: 1.7),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(Assets.icons.galleryAdd.path),
+ const SizedBox(width: 10),
+ const Text(
+ 'بارگذاری',
+ style: TextStyle(fontSize: 16, color: AppColors.active),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
+ // ... این متد بدون تغییر باقی میماند
+ 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.fromLTRB(16, 8, 16, 8),
+ 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(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showReservationPopup(BuildContext context) {
+ // ... این متد بدون تغییر باقی میماند
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (BuildContext dialogContext) {
+ return Dialog(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15),
+ ),
+ elevation: 10,
+ backgroundColor: Colors.white,
+ child: Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.topCenter,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 40,
+ left: 20,
+ right: 20,
+ bottom: 20,
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const SizedBox(height: 10),
+ const Padding(
+ padding: EdgeInsets.all(15.0),
+ child: Text(
+ "اول خرید کن، بعدا عکس بگیر!",
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const SizedBox(height: 5),
+ const Text(
+ "یه محصول رو از فروشگاهمون رزرو کن و ازمون تحویلش بگیر، بعدش میتونی عکسشو اینجا آپلود کنی.",
+ style: TextStyle(color: AppColors.hint, fontSize: 16),
+ textAlign: TextAlign.start,
+ ),
+ const SizedBox(height: 20),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ GestureDetector(
+ onTap: () => Navigator.of(dialogContext).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),
+ ),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 45,
+ vertical: 7,
+ ),
+ ),
+ onPressed: () async {
+ context.read().reserveProduct(
+ productId,
+ );
+ Navigator.of(dialogContext).pop();
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => ReservationConfirmationPage(
+ offer: offer,
+ ),
+ ),
+ );
+ },
+ child: const Text(
+ "رزرو محصول",
+ style: TextStyle(color: Colors.white),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ Positioned(
+ top: -40,
+ child: Container(
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.3),
+ blurRadius: 8,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: CircleAvatar(
+ backgroundColor: Colors.white,
+ radius: 40,
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: SvgPicture.asset(Assets.icons.camera.path),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/pages/offers_page.dart b/lib/presentation/pages/offers_page.dart
index 3934891..1c1594f 100644
--- a/lib/presentation/pages/offers_page.dart
+++ b/lib/presentation/pages/offers_page.dart
@@ -14,6 +14,8 @@ import 'package:proxibuy/presentation/offer/bloc/offer_event.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_state.dart';
import 'package:proxibuy/presentation/offer/bloc/widgets/category_offers_row.dart';
import 'package:proxibuy/presentation/pages/notification_preferences_page.dart';
+import 'package:proxibuy/presentation/pages/reserved_list_page.dart';
+import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/gps_dialog.dart';
import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -97,7 +99,10 @@ class _OffersPageState extends State {
children: [
SvgPicture.asset(Assets.icons.edit.path),
const SizedBox(width: 4),
- const Text('ویرایش',style: TextStyle(color: AppColors.active),),
+ const Text(
+ 'ویرایش',
+ style: TextStyle(color: AppColors.active),
+ ),
],
),
),
@@ -153,10 +158,58 @@ class _OffersPageState extends State {
child: Assets.icons.logoWithName.svg(height: 40, width: 200),
),
actions: [
- IconButton(onPressed: () {}, icon: Assets.icons.notification.svg()),
- IconButton(onPressed: () {}, icon: Assets.icons.scanBarcode.svg()),
- const SizedBox(width: 8),
- ],
+ IconButton(onPressed: () {}, icon: Assets.icons.notification.svg()),
+ BlocBuilder(
+ builder: (context, state) {
+ final reservedCount = state.reservedProductIds.length;
+
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ IconButton(
+ onPressed: () {
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => const ReservedListPage()),
+ );
+ },
+ icon: Assets.icons.scanBarcode.svg(),
+ ),
+ if (reservedCount > 0)
+ Positioned(
+ top: 0,
+ right: 2,
+ child: Container(
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: Colors.green,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 1.5),
+ ),
+ constraints: const BoxConstraints(
+ minWidth: 18,
+ minHeight: 18,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(2, 4, 2, 2),
+ child: Text(
+ '$reservedCount',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 11,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ const SizedBox(width: 8),
+ ],
),
body: SingleChildScrollView(
child: Column(
@@ -201,7 +254,10 @@ class OffersView extends StatelessWidget {
backgroundColor: AppColors.confirm,
foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey,
- padding: const EdgeInsets.symmetric(vertical: 12,horizontal: 125),
+ padding: const EdgeInsets.symmetric(
+ vertical: 12,
+ horizontal: 125,
+ ),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
@@ -216,7 +272,7 @@ class OffersView extends StatelessWidget {
),
),
const SizedBox(height: 15),
- const Text('جستوجوی تصادفی')
+ const Text('جستوجوی تصادفی'),
],
),
),
@@ -240,12 +296,12 @@ class OffersView extends StatelessWidget {
final offersForCategory = groupedOffers[category]!;
return CategoryOffersRow(
- categoryTitle: category,
- offers: offersForCategory,
- )
- .animate()
- .fade(duration: 500.ms)
- .slideY(begin: 0.3, duration: 400.ms, curve: Curves.easeOut);
+ categoryTitle: category,
+ offers: offersForCategory,
+ )
+ .animate()
+ .fade(duration: 500.ms)
+ .slideY(begin: 0.3, duration: 400.ms, curve: Curves.easeOut);
},
);
}
@@ -259,4 +315,4 @@ class OffersView extends StatelessWidget {
},
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/presentation/pages/product_detail_page.dart b/lib/presentation/pages/product_detail_page.dart
index 79ae772..97146c6 100644
--- a/lib/presentation/pages/product_detail_page.dart
+++ b/lib/presentation/pages/product_detail_page.dart
@@ -7,10 +7,12 @@ 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/data/repositories/offer_repository.dart';
+import 'package:proxibuy/presentation/pages/add_photo_screen.dart';
import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_bloc.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
+import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/comments_section.dart';
import 'package:slide_countdown/slide_countdown.dart';
@@ -55,15 +57,12 @@ class ProductDetailPage extends StatelessWidget {
bottom: 30,
left: 24,
right: 24,
- // دکمه را در یک BlocBuilder قرار میدهیم تا به state دسترسی داشته باشد
child: BlocBuilder(
builder: (context, state) {
- // دکمه فقط زمانی نمایش داده میشود که اطلاعات محصول با موفقیت لود شده باشد
if (state is ProductDetailLoadSuccess) {
return ElevatedButton(
onPressed: () {
- // با کلیک روی دکمه، به صفحه تایید رزرو منتقل میشویم
- // و اطلاعات محصول را به آن پاس میدهیم
+ context.read().reserveProduct(state.offer.id);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ReservationConfirmationPage(offer: state.offer),
@@ -71,11 +70,11 @@ class ProductDetailPage extends StatelessWidget {
);
},
style: ElevatedButton.styleFrom(
- backgroundColor: AppColors.confirm, // رنگ دکمه را به سبز تغییر دادم تا با مفهوم رزرو همخوانی داشته باشد
+ backgroundColor: AppColors.confirm,
elevation: 5,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(50), // گردی بیشتر برای زیبایی
+ borderRadius: BorderRadius.circular(50),
),
),
child: Row(
@@ -97,7 +96,6 @@ class ProductDetailPage extends StatelessWidget {
.fadeIn(delay: 200.ms, duration: 400.ms, curve: Curves.easeOut)
.slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut);
}
- // اگر اطلاعات در حال لود شدن باشد، چیزی نمایش داده نمیشود
return const SizedBox.shrink();
},
),
@@ -310,7 +308,13 @@ class _ProductDetailViewState extends State {
Widget _buildUploadButton() {
return GestureDetector(
onTap: () {
- print("Upload image tapped!");
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => AddPhotoScreen(storeName: widget.offer.storeName, productId: widget.offer.id, offer: widget.offer,),
+ ),
+ );
+
},
child: Container(
width: 90,
@@ -593,7 +597,7 @@ class _ProductDetailViewState extends State {
shouldShowMinutes:
(d) =>
d.inSeconds >
- 0, // دقیقه تا آخرین ثانیه نمایش داده میشود
+ 0,
),
),
const SizedBox(height: 4),
diff --git a/lib/presentation/pages/reservation_details_screen.dart b/lib/presentation/pages/reservation_details_screen.dart
index 66ca24a..f3efe74 100644
--- a/lib/presentation/pages/reservation_details_screen.dart
+++ b/lib/presentation/pages/reservation_details_screen.dart
@@ -68,7 +68,6 @@ class _ReservationConfirmationPageState extends State createState() => _ReservedListPageState();
+}
+
+class _ReservedListPageState extends State {
+ late final List _reservedIds;
+ Future>? _reservedOffersFuture;
+
+ @override
+ void initState() {
+ super.initState();
+ _reservedIds = context.read().state.reservedProductIds;
+ _reservedOffersFuture = _fetchReservedOffers();
+ }
+
+ Future> _fetchReservedOffers() {
+ final offerRepo = context.read();
+ final offerFutures =
+ _reservedIds.map((id) => offerRepo.fetchOfferById(id)).toList();
+ return Future.wait(offerFutures);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Directionality(
+ textDirection: TextDirection.rtl,
+ child: Scaffold(
+ appBar: _buildCustomAppBar(context),
+ body: FutureBuilder>(
+ future: _reservedOffersFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ if (snapshot.hasError) {
+ return Center(
+ child: Text('خطا در بارگذاری اطلاعات: ${snapshot.error}'),
+ );
+ }
+
+ if (!snapshot.hasData || snapshot.data!.isEmpty) {
+ return const Center(
+ child: Text('هیچ آیتم رزرو شدهای وجود ندارد.'),
+ );
+ }
+
+ final reservedOffers =
+ snapshot.data!.whereType().toList();
+
+ if (reservedOffers.isEmpty) {
+ return const Center(
+ child: Text('هیچ آیتم رزرو شدهای یافت نشد.'),
+ );
+ }
+
+ return ListView.builder(
+ padding: const EdgeInsets.all(16.0),
+ itemCount: reservedOffers.length,
+ itemBuilder: (context, index) {
+ final offer = reservedOffers[index];
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 16.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'تخفیف ${offer.discountType} رزرو شد!',
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Colors.black87,
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Divider(thickness: 1.5),
+ const SizedBox(height: 2),
+ ReservedListItemCard(offer: offer),
+ SizedBox(
+ height: 15,
+ )
+ ],
+ ),
+ ).animate().fadeIn(delay: (100 * index).ms).slideY(
+ begin: 0.5,
+ duration: 500.ms,
+ curve: Curves.easeOutCubic,
+ );
+ },
+ );
+ },
+ ),
+ ),
+ );
+ }
+
+ PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
+ 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: Column(
+ children: [
+ SizedBox(height: 15),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Row(
+ children: [
+ const Text(
+ 'رزرو شدهها',
+ style: TextStyle(
+ color: Colors.black,
+ fontWeight: FontWeight.normal,
+ fontSize: 16,
+ ),
+ ),
+ IconButton(
+ icon: SvgPicture.asset(Assets.icons.arrowLeft.path),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/reservation/cubit/reservation_cubit.dart b/lib/presentation/reservation/cubit/reservation_cubit.dart
new file mode 100644
index 0000000..f3e5c00
--- /dev/null
+++ b/lib/presentation/reservation/cubit/reservation_cubit.dart
@@ -0,0 +1,20 @@
+import 'package:bloc/bloc.dart';
+
+part 'reservation_state.dart';
+
+class ReservationCubit extends Cubit {
+ ReservationCubit() : super(const ReservationState());
+
+ void reserveProduct(String productId) {
+ if (state.reservedProductIds.contains(productId)) return;
+
+ final updatedList = List.from(state.reservedProductIds)..add(productId);
+ emit(state.copyWith(reservedProductIds: updatedList));
+ // در اینجا میتوانید لاگیک مربوط به ارسال درخواست به API را نیز اضافه کنید
+ }
+
+ // متد برای بررسی اینکه آیا یک محصول خاص رزرو شده است یا نه
+ bool isProductReserved(String productId) {
+ return state.reservedProductIds.contains(productId);
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/reservation/cubit/reservation_state.dart b/lib/presentation/reservation/cubit/reservation_state.dart
new file mode 100644
index 0000000..ea1db7a
--- /dev/null
+++ b/lib/presentation/reservation/cubit/reservation_state.dart
@@ -0,0 +1,13 @@
+part of 'reservation_cubit.dart';
+
+class ReservationState {
+ final List reservedProductIds;
+
+ const ReservationState({this.reservedProductIds = const []});
+
+ ReservationState copyWith({List? reservedProductIds}) {
+ return ReservationState(
+ reservedProductIds: reservedProductIds ?? this.reservedProductIds,
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/widgets/flutter_staggered_grid_view.dart b/lib/presentation/widgets/flutter_staggered_grid_view.dart
new file mode 100644
index 0000000..f09e039
--- /dev/null
+++ b/lib/presentation/widgets/flutter_staggered_grid_view.dart
@@ -0,0 +1,114 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
+
+class PhotoGalleryView extends StatelessWidget {
+ final List imageUrls;
+ final int remainingPhotos;
+
+ const PhotoGalleryView({
+ super.key,
+ required this.imageUrls,
+ required this.remainingPhotos,
+ });
+
+ // این ویجت کمکی تشخیص میدهد که عکس از فایل محلی است یا از اینترنت
+ Widget _buildSmartImage(String imageUrl) {
+ bool isFile = !imageUrl.startsWith('http');
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(12.0),
+ child: isFile
+ ? Image.file(
+ File(imageUrl),
+ fit: BoxFit.cover,
+ )
+ : Image.network(
+ imageUrl,
+ fit: BoxFit.cover,
+ ),
+ );
+ }
+
+ Widget _buildLastImageOverlay(String imageUrl) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(12.0),
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ _buildSmartImage(imageUrl), // از ویجت هوشمند استفاده میکنیم
+ Container(
+ color: Colors.white.withOpacity(0.7),
+ child: Center(
+ child: Text(
+ '+$remainingPhotos',
+ style: const TextStyle(
+ color: Colors.black,
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (imageUrls.isEmpty) {
+ return const Center(child: Text("هنوز عکسی وجود ندارد."));
+ }
+
+ // این بخش برای جلوگیری از خطا در صورتی که تعداد عکسها کمتر از نیاز گرید باشد، اضافه شده است.
+ final displayUrls = List.from(imageUrls);
+ while (displayUrls.length < 6) {
+ displayUrls.add('https://via.placeholder.com/200'); // یک عکس جایگزین
+ }
+
+
+ return StaggeredGrid.count(
+ crossAxisCount: 4,
+ mainAxisSpacing: 8,
+ crossAxisSpacing: 7,
+ children: [
+ if (imageUrls.isNotEmpty)
+ StaggeredGridTile.count(
+ crossAxisCellCount: 3,
+ mainAxisCellCount: 2,
+ child: _buildSmartImage(displayUrls[0]),
+ ),
+ if (imageUrls.length > 1)
+ StaggeredGridTile.count(
+ crossAxisCellCount: 1,
+ mainAxisCellCount: 1,
+ child: _buildSmartImage(displayUrls[1]),
+ ),
+ if (imageUrls.length > 2)
+ StaggeredGridTile.count(
+ crossAxisCellCount: 1,
+ mainAxisCellCount: 1,
+ child: _buildSmartImage(displayUrls[2]),
+ ),
+ if (imageUrls.length > 3)
+ StaggeredGridTile.count(
+ crossAxisCellCount: 2,
+ mainAxisCellCount: 1,
+ child: _buildSmartImage(displayUrls[3]),
+ ),
+ if (imageUrls.length > 4)
+ StaggeredGridTile.count(
+ crossAxisCellCount: 1,
+ mainAxisCellCount: 1,
+ child: _buildSmartImage(displayUrls[4]),
+ ),
+ if (imageUrls.length > 5)
+ StaggeredGridTile.count(
+ crossAxisCellCount: 1,
+ mainAxisCellCount: 1,
+ child: _buildLastImageOverlay(displayUrls[5]),
+ ),
+ ],
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/widgets/reserved_list_item_card.dart b/lib/presentation/widgets/reserved_list_item_card.dart
new file mode 100644
index 0000000..9411ce4
--- /dev/null
+++ b/lib/presentation/widgets/reserved_list_item_card.dart
@@ -0,0 +1,390 @@
+// lib/presentation/widgets/reserved_list_item_card.dart
+
+import 'dart:async';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:proxibuy/core/gen/assets.gen.dart';
+import 'package:qr_flutter/qr_flutter.dart';
+import 'package:proxibuy/core/config/app_colors.dart';
+import 'package:proxibuy/data/models/offer_model.dart';
+import 'package:slide_countdown/slide_countdown.dart';
+
+class ReservedListItemCard extends StatefulWidget {
+ final OfferModel offer;
+
+ const ReservedListItemCard({super.key, required this.offer});
+
+ @override
+ State createState() => _ReservedListItemCardState();
+}
+
+class _ReservedListItemCardState extends State {
+ bool _isExpanded = false;
+ Timer? _timer;
+ Duration _remaining = Duration.zero;
+
+ @override
+ void initState() {
+ super.initState();
+ _calculateRemainingTime();
+ _timer = Timer.periodic(const Duration(seconds: 1), (_) {
+ _calculateRemainingTime();
+ });
+ }
+
+ void _calculateRemainingTime() {
+ final now = DateTime.now();
+ if (widget.offer.expiryTime.isAfter(now)) {
+ if (mounted) {
+ setState(() {
+ _remaining = widget.offer.expiryTime.difference(now);
+ });
+ }
+ } else {
+ if (mounted) {
+ setState(() => _remaining = Duration.zero);
+ }
+ _timer?.cancel();
+ }
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ String _formatDuration(Duration duration) {
+ if (duration.inSeconds <= 0) return "پایان یافته";
+ final hours = duration.inHours.toString().padLeft(2, '0');
+ final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0');
+ final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
+ return '$hours:$minutes:$seconds';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // ویجت اصلی به Column تغییر کرده است
+ return Column(
+ children: [
+ // بخش اول: کارت اصلی که فقط شامل اطلاعات محصول است
+ Card(
+ color: Colors.white,
+ shape: RoundedRectangleBorder(
+ side: BorderSide(color: Colors.grey.shade300, width: 1),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ elevation: 0,
+ clipBehavior: Clip.antiAlias,
+ margin: EdgeInsets.zero,
+ child: _buildOfferPrimaryDetails(), // فقط اطلاعات اصلی داخل کارت
+ ),
+ _buildActionsRow(),
+ // بخش سوم: پنل باز شونده QR کد
+ _buildExpansionPanel(),
+ ],
+ );
+ }
+
+ // این متد فقط اطلاعات اصلی داخل کارت را میسازد
+ Widget _buildOfferPrimaryDetails() {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(15, 25, 15, 25),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: CachedNetworkImage(
+ imageUrl: widget.offer.coverImageUrl,
+ width: 90,
+ height: 90,
+ fit: BoxFit.cover,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ SvgPicture.asset(Assets.icons.shop.path),
+ SizedBox(width: 8),
+ Text(
+ widget.offer.storeName,
+ style: const TextStyle(
+ fontWeight: FontWeight.normal,
+ fontSize: 16,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ SvgPicture.asset(Assets.icons.shoppingCart.path),
+ SizedBox(width: 8),
+ Text(
+ widget.offer.title,
+ style: TextStyle(color: AppColors.hint, fontSize: 14),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ SvgPicture.asset(Assets.icons.location.path),
+ SizedBox(width: 8),
+ // برای جلوگیری از سرریز شدن متن، از Flexible استفاده میکنیم
+ Flexible(
+ child: Text(
+ "${widget.offer.address} (${widget.offer.distanceInMeters} متر تا تخفیف)",
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ style: const TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.normal,
+ color: AppColors.hint,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildActionsRow() {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ if (_remaining > Duration.zero)
+ Column(
+ children: [
+ Localizations.override(
+ context: context,
+ locale: const Locale('en'),
+ child: SlideCountdown(
+ duration: _remaining,
+ slideDirection: SlideDirection.up,
+ separator: ':',
+ style: const TextStyle(
+ fontSize: 25,
+ fontWeight: FontWeight.bold,
+ color: AppColors.countdown,
+ ),
+ separatorStyle: const TextStyle(
+ fontSize: 20,
+ color: AppColors.countdown,
+ ),
+ decoration: const BoxDecoration(color: Colors.white),
+ shouldShowDays: (d) => d.inDays > 0,
+ shouldShowHours: (d) => d.inHours > 0,
+ shouldShowMinutes: (d) => d.inSeconds > 0,
+ ),
+ ),
+ const SizedBox(height: 4),
+ _buildTimerLabels(_remaining),
+ ],
+ ),
+ SizedBox(width: 10),
+ TextButton(
+ onPressed: () => setState(() => _isExpanded = !_isExpanded),
+ 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,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTimerLabels(Duration duration) {
+ const double columnWidth = 40;
+ const labelStyle = TextStyle(fontSize: 10, color: AppColors.selectedImg);
+
+ List labels = [];
+
+ if (duration.inDays > 0) {
+ labels = [
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("ثانیه", style: labelStyle)),
+ ),
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("دقیقه", style: labelStyle)),
+ ),
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("ساعت", style: labelStyle)),
+ ),
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("روز", style: labelStyle)),
+ ),
+ ];
+ } else if (duration.inHours > 0) {
+ labels = [
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("ثانیه", style: labelStyle)),
+ ),
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("دقیقه", style: labelStyle)),
+ ),
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("ساعت", style: labelStyle)),
+ ),
+ ];
+ } else if (duration.inSeconds > 0) {
+ labels = [
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("ثانیه", style: labelStyle)),
+ ),
+ SizedBox(
+ width: columnWidth,
+ child: Center(child: Text("دقیقه", style: labelStyle)),
+ ),
+ ];
+ }
+
+ return Row(mainAxisAlignment: MainAxisAlignment.center, children: labels);
+ }
+
+ Widget _buildExpansionPanel() {
+ return AnimatedCrossFade(
+ firstChild: Container(),
+ secondChild: Container(
+ padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
+ width: double.infinity,
+ child: Center(
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 9,
+ ),
+ decoration: BoxDecoration(
+ color: AppColors.singleOfferType,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Text(
+ "تخفیف ${widget.offer.discountType}",
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.normal,
+ fontSize: 17,
+ ),
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Text(
+ '(${widget.offer.discount})',
+ style: const TextStyle(
+ fontSize: 14,
+ color: AppColors.singleOfferType,
+ fontWeight: FontWeight.normal,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ widget.offer.originalPrice.toStringAsFixed(0),
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey.shade600,
+ decoration: TextDecoration.lineThrough,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 1),
+ Text(
+ '${widget.offer.finalPrice.toStringAsFixed(0)} تومان',
+ style: const TextStyle(
+ color: AppColors.singleOfferType,
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ SizedBox(
+ height: 20,
+ ),
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Color.fromARGB(255, 246, 246, 246),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(15.0),
+ child: QrImageView(
+ data: widget.offer.qrCodeData,
+ version: QrVersions.auto,
+ size: 280.0,
+ ),
+ ),
+ SizedBox(height: 10),
+ Text(
+ widget.offer.qrCodeData,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ crossFadeState:
+ _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
+ duration: const Duration(milliseconds: 300),
+ );
+ }
+}
\ No newline at end of file
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index bfafdd0..020f725 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -6,11 +6,15 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
+ file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_localization_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin");
flutter_localization_plugin_register_with_registrar(flutter_localization_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 866b317..a021899 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ file_selector_linux
flutter_localization
maps_launcher
url_launcher_linux
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index f2712cd..5f18724 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
+import file_selector_macos
import flutter_localization
import geolocator_apple
import maps_launcher
@@ -14,6 +15,7 @@ import sqflite_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index 3eb44e7..7adbb89 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -209,6 +209,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.27"
+ cross_file:
+ dependency: transitive
+ description:
+ name: cross_file
+ sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.4+2"
crypto:
dependency: transitive
description:
@@ -289,6 +297,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
+ file_selector_linux:
+ dependency: transitive
+ description:
+ name: file_selector_linux
+ sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3+2"
+ file_selector_macos:
+ dependency: transitive
+ description:
+ name: file_selector_macos
+ sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.4+3"
+ file_selector_platform_interface:
+ dependency: transitive
+ description:
+ name: file_selector_platform_interface
+ sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.6.2"
+ file_selector_windows:
+ dependency: transitive
+ description:
+ name: file_selector_windows
+ sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3+4"
fixnum:
dependency: transitive
description:
@@ -371,6 +411,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.28"
flutter_shaders:
dependency: transitive
description:
@@ -379,6 +427,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.3"
+ flutter_staggered_grid_view:
+ dependency: "direct main"
+ description:
+ name: flutter_staggered_grid_view
+ sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.0"
flutter_svg:
dependency: "direct main"
description:
@@ -501,6 +557,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ image_picker_android:
+ dependency: transitive
+ description:
+ name: image_picker_android
+ sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.12+23"
+ image_picker_for_web:
+ dependency: transitive
+ description:
+ name: image_picker_for_web
+ sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.6"
+ image_picker_ios:
+ dependency: transitive
+ description:
+ name: image_picker_ios
+ sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.12+2"
+ image_picker_linux:
+ dependency: transitive
+ description:
+ name: image_picker_linux
+ sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+2"
+ image_picker_macos:
+ dependency: transitive
+ description:
+ name: image_picker_macos
+ sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+2"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.10.1"
+ image_picker_windows:
+ dependency: transitive
+ description:
+ name: image_picker_windows
+ sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+1"
image_size_getter:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 28f555c..cefb72b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -50,6 +50,8 @@ dependencies:
maps_launcher: ^3.0.0+1
slide_countdown: ^2.0.2
qr_flutter: ^4.1.0
+ flutter_staggered_grid_view: ^0.7.0
+ image_picker: ^1.1.2
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index a125364..ccecd30 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
@@ -13,6 +14,8 @@
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ FileSelectorWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterLocalizationPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi"));
GeolocatorWindowsRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 1888886..f5f5fec 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ file_selector_windows
flutter_localization
geolocator_windows
maps_launcher