added upload page

This commit is contained in:
mohamadmahdi jebeli 2025-07-02 12:10:29 +03:30
parent c0d1bee773
commit 15782a7d34
26 changed files with 1400 additions and 45 deletions

View File

@ -2,6 +2,8 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application <application
android:label="Proxibuy" android:label="Proxibuy"

View File

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 17.7298C11.3 17.7298 10.6 17.4598 10.07 16.9298L3.55002 10.4098C3.26002 10.1198 3.26002 9.63982 3.55002 9.34982C3.84002 9.05982 4.32002 9.05982 4.61002 9.34982L11.13 15.8698C11.61 16.3498 12.39 16.3498 12.87 15.8698L19.39 9.34982C19.68 9.05982 20.16 9.05982 20.45 9.34982C20.74 9.63982 20.74 10.1198 20.45 10.4098L13.93 16.9298C13.4 17.4598 12.7 17.7298 12 17.7298Z" fill="#2196F3"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 8.17643C11.8 8.17643 11.1 8.44643 10.57 8.97643L4.05002 15.4964C3.76002 15.7864 3.76002 16.2664 4.05002 16.5564C4.34002 16.8464 4.82002 16.8464 5.11002 16.5564L11.63 10.0364C12.11 9.55643 12.89 9.55643 13.37 10.0364L19.89 16.5564C20.18 16.8464 20.66 16.8464 20.95 16.5564C21.24 16.2664 21.24 15.7864 20.95 15.4964L14.43 8.97643C13.9 8.44643 13.2 8.17643 12.5 8.17643Z" fill="#2196F3"/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

5
assets/icons/camera.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="53" height="53" viewBox="0 0 53 53" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.4" d="M15.4569 48.4764H38.1636C44.1436 48.4764 46.5269 44.8147 46.8086 40.3514L47.9353 22.4547C48.2386 17.7747 44.5119 13.8097 39.8103 13.8097C38.4886 13.8097 37.2753 13.0514 36.6686 11.8814L35.1086 8.73973C34.1119 6.76807 31.5119 5.14307 29.3019 5.14307H24.3403C22.1086 5.14307 19.5086 6.76807 18.5119 8.73973L16.9519 11.8814C16.3453 13.0514 15.1319 13.8097 13.8103 13.8097C9.10859 13.8097 5.38193 17.7747 5.68526 22.4547L6.81193 40.3514C7.07193 44.8147 9.47693 48.4764 15.4569 48.4764Z" fill="#176BAD"/>
<path d="M30.0601 19.7681H23.5601C22.6717 19.7681 21.9351 19.0314 21.9351 18.1431C21.9351 17.2547 22.6717 16.5181 23.5601 16.5181H30.0601C30.9484 16.5181 31.6851 17.2547 31.6851 18.1431C31.6851 19.0314 30.9484 19.7681 30.0601 19.7681Z" fill="#176BAD"/>
<path d="M26.8101 40.0915C30.8547 40.0915 34.1335 36.8127 34.1335 32.7682C34.1335 28.7236 30.8547 25.4448 26.8101 25.4448C22.7656 25.4448 19.4868 28.7236 19.4868 32.7682C19.4868 36.8127 22.7656 40.0915 26.8101 40.0915Z" fill="#176BAD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,7 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 10.8169C10.1046 10.8169 11 9.92146 11 8.81689C11 7.71232 10.1046 6.81689 9 6.81689C7.89543 6.81689 7 7.71232 7 8.81689C7 9.92146 7.89543 10.8169 9 10.8169Z" stroke="#2196F3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 2.81689H9C4 2.81689 2 4.81689 2 9.81689V15.8169C2 20.8169 4 22.8169 9 22.8169H15C20 22.8169 22 20.8169 22 15.8169V10.8169" stroke="#2196F3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.75 5.81689H21.25" stroke="#2196F3" stroke-width="1.5" stroke-linecap="round"/>
<path d="M18.5 8.56689V3.06689" stroke="#2196F3" stroke-width="1.5" stroke-linecap="round"/>
<path d="M2.66992 19.767L7.59992 16.457C8.38992 15.927 9.52992 15.987 10.2399 16.597L10.5699 16.887C11.3499 17.557 12.6099 17.557 13.3899 16.887L17.5499 13.317C18.3299 12.647 19.5899 12.647 20.3699 13.317L21.9999 14.717" stroke="#2196F3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -16,4 +16,5 @@ class AppColors {
static const Color countdown = Color.fromARGB(255, 54, 124, 57); static const Color countdown = Color.fromARGB(255, 54, 124, 57);
static const Color countdownBorderRserve = Color.fromARGB(255, 186, 222, 251); static const Color countdownBorderRserve = Color.fromARGB(255, 186, 222, 251);
static const Color expiryReserve = Color.fromARGB(255, 183, 28, 28); static const Color expiryReserve = Color.fromARGB(255, 183, 28, 28);
static const Color uploadElevated = Color.fromARGB(255, 233, 245, 254);
} }

View File

@ -75,15 +75,24 @@ class $AssetsIconsGen {
/// File path: assets/icons/arayesh.svg /// File path: assets/icons/arayesh.svg
SvgGenImage get arayesh => const SvgGenImage('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 /// File path: assets/icons/arrow-left.svg
SvgGenImage get arrowLeft => const SvgGenImage('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 /// File path: assets/icons/back.svg
SvgGenImage get back => const SvgGenImage('assets/icons/back.svg'); SvgGenImage get back => const SvgGenImage('assets/icons/back.svg');
/// File path: assets/icons/backArrow.svg /// File path: assets/icons/backArrow.svg
SvgGenImage get backArrow => const SvgGenImage('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 /// File path: assets/icons/card-pos.svg
SvgGenImage get cardPos => const SvgGenImage('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 /// File path: assets/icons/fastfood.svg
SvgGenImage get fastfood => const SvgGenImage('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 /// File path: assets/icons/global-search.svg
SvgGenImage get globalSearch => SvgGenImage get globalSearch =>
const SvgGenImage('assets/icons/global-search.svg'); const SvgGenImage('assets/icons/global-search.svg');
@ -222,9 +235,12 @@ class $AssetsIconsGen {
vector, vector,
addImg, addImg,
arayesh, arayesh,
arrowDown,
arrowLeft, arrowLeft,
arrowUp,
back, back,
backArrow, backArrow,
camera,
cardPos, cardPos,
cinama, cinama,
clock, clock,
@ -234,6 +250,7 @@ class $AssetsIconsGen {
edit, edit,
error, error,
fastfood, fastfood,
galleryAdd,
globalSearch, globalSearch,
kafsh, kafsh,
location, location,

View File

@ -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<AddPhotoState> {
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<void> 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<String>.from(currentState.imageUrls)
..insert(0, pickedFile.path);
emit(AddPhotoLoaded(updatedUrls, currentState.remainingPhotos - 1));
}
} catch (e) {
emit(AddPhotoError('خطا در انتخاب عکس: ${e.toString()}'));
}
}
}
}

View File

@ -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<String> imageUrls;
final int remainingPhotos;
AddPhotoLoaded(this.imageUrls, this.remainingPhotos);
}
class AddPhotoError extends AddPhotoState {
final String message;
AddPhotoError(this.message);
}

View File

@ -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_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.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/offer/bloc/offer_bloc.dart'; // مسیر BLoC آفر
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'core/config/app_colors.dart'; import 'core/config/app_colors.dart';
import 'presentation/pages/onboarding_page.dart'; import 'presentation/pages/onboarding_page.dart';
void main() { void main() {
Animate.restartOnHotReload = true; Animate.restartOnHotReload = true;
runApp(const MyApp()); runApp(const MyApp());
@ -26,23 +26,27 @@ class MyApp extends StatelessWidget {
return MultiRepositoryProvider( return MultiRepositoryProvider(
providers: [ providers: [
RepositoryProvider<OfferRepository>( RepositoryProvider<OfferRepository>(
create: (context) => OfferRepository( create:
offerDataSource: MockOfferDataSource(), (context) =>
), OfferRepository(offerDataSource: MockOfferDataSource()),
), ),
], ],
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider<AuthBloc>( BlocProvider<AuthBloc>(create: (context) => AuthBloc()),
create: (context) => AuthBloc(),
),
BlocProvider<NotificationPreferencesBloc>( BlocProvider<NotificationPreferencesBloc>(
create: (context) => NotificationPreferencesBloc()..add(LoadCategories()), create:
(context) =>
NotificationPreferencesBloc()..add(LoadCategories()),
), ),
BlocProvider<OffersBloc>( BlocProvider<OffersBloc>(
create: (context) => OffersBloc( create:
offerRepository: context.read<OfferRepository>(), (context) => OffersBloc(
), offerRepository: context.read<OfferRepository>(),
),
),
BlocProvider<ReservationCubit>(
create: (context) => ReservationCubit(),
), ),
], ],
child: MaterialApp( child: MaterialApp(
@ -54,9 +58,7 @@ class MyApp extends StatelessWidget {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: const [ supportedLocales: const [Locale('fa')],
Locale('fa'),
],
locale: const Locale('fa'), locale: const Locale('fa'),
theme: ThemeData( theme: ThemeData(
@ -76,7 +78,10 @@ class MyApp extends StatelessWidget {
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), contentPadding: const EdgeInsets.symmetric(
vertical: 18,
horizontal: 20,
),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.border), borderSide: const BorderSide(color: AppColors.border),
@ -87,7 +92,10 @@ class MyApp extends StatelessWidget {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), 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), labelStyle: const TextStyle(color: Colors.black),
hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)), hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)),
@ -97,7 +105,9 @@ class MyApp extends StatelessWidget {
foregroundColor: Colors.black, // رنگ متن دکمه Outlined foregroundColor: Colors.black, // رنگ متن دکمه Outlined
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: Colors.grey), side: const BorderSide(color: Colors.grey),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
textStyle: const TextStyle( textStyle: const TextStyle(
fontFamily: 'Dana', fontFamily: 'Dana',
fontSize: 16, fontSize: 16,
@ -110,7 +120,9 @@ class MyApp extends StatelessWidget {
backgroundColor: AppColors.button, backgroundColor: AppColors.button,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
textStyle: const TextStyle( textStyle: const TextStyle(
fontFamily: 'Dana', fontFamily: 'Dana',
fontSize: 16, fontSize: 16,
@ -124,4 +136,4 @@ class MyApp extends StatelessWidget {
), ),
); );
} }
} }

View File

@ -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: <Widget>[
ListTile(
leading: const Icon(Icons.photo_library, color: AppColors.primary),
title: const Text('انتخاب از گالری'),
onTap: () {
Navigator.of(bottomSheetContext).pop();
context.read<AddPhotoCubit>().pickImage(ImageSource.gallery);
},
),
ListTile(
leading: const Icon(Icons.camera_alt, color: AppColors.primary),
title: const Text('گرفتن عکس با دوربین'),
onTap: () {
Navigator.of(bottomSheetContext).pop();
context.read<AddPhotoCubit>().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<AddPhotoCubit, AddPhotoState>(
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<ReservationCubit>().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<ReservationCubit>().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),
),
),
),
),
],
),
);
},
);
}
}

View File

@ -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/offer_state.dart';
import 'package:proxibuy/presentation/offer/bloc/widgets/category_offers_row.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/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/gps_dialog.dart';
import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart'; import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -97,7 +99,10 @@ class _OffersPageState extends State<OffersPage> {
children: [ children: [
SvgPicture.asset(Assets.icons.edit.path), SvgPicture.asset(Assets.icons.edit.path),
const SizedBox(width: 4), 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<OffersPage> {
child: Assets.icons.logoWithName.svg(height: 40, width: 200), child: Assets.icons.logoWithName.svg(height: 40, width: 200),
), ),
actions: [ actions: [
IconButton(onPressed: () {}, icon: Assets.icons.notification.svg()), IconButton(onPressed: () {}, icon: Assets.icons.notification.svg()),
IconButton(onPressed: () {}, icon: Assets.icons.scanBarcode.svg()), BlocBuilder<ReservationCubit, ReservationState>(
const SizedBox(width: 8), 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( body: SingleChildScrollView(
child: Column( child: Column(
@ -201,7 +254,10 @@ class OffersView extends StatelessWidget {
backgroundColor: AppColors.confirm, backgroundColor: AppColors.confirm,
foregroundColor: Colors.white, foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey, disabledBackgroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(vertical: 12,horizontal: 125), padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 125,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
), ),
@ -216,7 +272,7 @@ class OffersView extends StatelessWidget {
), ),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
const Text('جست‌وجوی تصادفی') const Text('جست‌وجوی تصادفی'),
], ],
), ),
), ),
@ -240,12 +296,12 @@ class OffersView extends StatelessWidget {
final offersForCategory = groupedOffers[category]!; final offersForCategory = groupedOffers[category]!;
return CategoryOffersRow( return CategoryOffersRow(
categoryTitle: category, categoryTitle: category,
offers: offersForCategory, offers: offersForCategory,
) )
.animate() .animate()
.fade(duration: 500.ms) .fade(duration: 500.ms)
.slideY(begin: 0.3, duration: 400.ms, curve: Curves.easeOut); .slideY(begin: 0.3, duration: 400.ms, curve: Curves.easeOut);
}, },
); );
} }
@ -259,4 +315,4 @@ class OffersView extends StatelessWidget {
}, },
); );
} }
} }

View File

@ -7,10 +7,12 @@ import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/data/repositories/offer_repository.dart'; import 'package:proxibuy/data/repositories/offer_repository.dart';
import 'package:proxibuy/presentation/pages/add_photo_screen.dart';
import 'package:proxibuy/presentation/pages/reservation_details_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_bloc.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart'; import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart'; import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/comments_section.dart'; import 'package:proxibuy/presentation/widgets/comments_section.dart';
import 'package:slide_countdown/slide_countdown.dart'; import 'package:slide_countdown/slide_countdown.dart';
@ -55,15 +57,12 @@ class ProductDetailPage extends StatelessWidget {
bottom: 30, bottom: 30,
left: 24, left: 24,
right: 24, right: 24,
// دکمه را در یک BlocBuilder قرار میدهیم تا به state دسترسی داشته باشد
child: BlocBuilder<ProductDetailBloc, ProductDetailState>( child: BlocBuilder<ProductDetailBloc, ProductDetailState>(
builder: (context, state) { builder: (context, state) {
// دکمه فقط زمانی نمایش داده میشود که اطلاعات محصول با موفقیت لود شده باشد
if (state is ProductDetailLoadSuccess) { if (state is ProductDetailLoadSuccess) {
return ElevatedButton( return ElevatedButton(
onPressed: () { onPressed: () {
// با کلیک روی دکمه، به صفحه تایید رزرو منتقل میشویم context.read<ReservationCubit>().reserveProduct(state.offer.id);
// و اطلاعات محصول را به آن پاس میدهیم
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => ReservationConfirmationPage(offer: state.offer), builder: (_) => ReservationConfirmationPage(offer: state.offer),
@ -71,11 +70,11 @@ class ProductDetailPage extends StatelessWidget {
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm, // رنگ دکمه را به سبز تغییر دادم تا با مفهوم رزرو همخوانی داشته باشد backgroundColor: AppColors.confirm,
elevation: 5, elevation: 5,
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), // گردی بیشتر برای زیبایی borderRadius: BorderRadius.circular(50),
), ),
), ),
child: Row( child: Row(
@ -97,7 +96,6 @@ class ProductDetailPage extends StatelessWidget {
.fadeIn(delay: 200.ms, duration: 400.ms, curve: Curves.easeOut) .fadeIn(delay: 200.ms, duration: 400.ms, curve: Curves.easeOut)
.slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut); .slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut);
} }
// اگر اطلاعات در حال لود شدن باشد، چیزی نمایش داده نمیشود
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
), ),
@ -310,7 +308,13 @@ class _ProductDetailViewState extends State<ProductDetailView> {
Widget _buildUploadButton() { Widget _buildUploadButton() {
return GestureDetector( return GestureDetector(
onTap: () { 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( child: Container(
width: 90, width: 90,
@ -593,7 +597,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
shouldShowMinutes: shouldShowMinutes:
(d) => (d) =>
d.inSeconds > d.inSeconds >
0, // دقیقه تا آخرین ثانیه نمایش داده میشود 0,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),

View File

@ -68,7 +68,6 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// انیمیشن برای عنوان اصلی
Text( Text(
'تخفیف ${widget.offer.discountType} رزرو شد!', 'تخفیف ${widget.offer.discountType} رزرو شد!',
style: const TextStyle( style: const TextStyle(
@ -79,7 +78,6 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
).animate().fadeIn(delay: 300.ms, duration: 500.ms).slideY(begin: -0.2, end: 0), ).animate().fadeIn(delay: 300.ms, duration: 500.ms).slideY(begin: -0.2, end: 0),
const SizedBox(height: 8), const SizedBox(height: 8),
// انیمیشن برای خط جداکننده
const Divider(thickness: 1.5) const Divider(thickness: 1.5)
.animate() .animate()
.fadeIn(delay: 400.ms) .fadeIn(delay: 400.ms)
@ -178,7 +176,6 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// جزئیات متنی
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -0,0 +1,162 @@
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/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/data/repositories/offer_repository.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/reserved_list_item_card.dart';
class ReservedListPage extends StatefulWidget {
const ReservedListPage({super.key});
@override
State<ReservedListPage> createState() => _ReservedListPageState();
}
class _ReservedListPageState extends State<ReservedListPage> {
late final List<String> _reservedIds;
Future<List<OfferModel?>>? _reservedOffersFuture;
@override
void initState() {
super.initState();
_reservedIds = context.read<ReservationCubit>().state.reservedProductIds;
_reservedOffersFuture = _fetchReservedOffers();
}
Future<List<OfferModel?>> _fetchReservedOffers() {
final offerRepo = context.read<OfferRepository>();
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<List<OfferModel?>>(
future: _reservedOffersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text('خطا در بارگذاری اطلاعات: ${snapshot.error}'),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(
child: Text('هیچ آیتم رزرو شده‌ای وجود ندارد.'),
);
}
final reservedOffers =
snapshot.data!.whereType<OfferModel>().toList();
if (reservedOffers.isEmpty) {
return const Center(
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(),
),
],
),
],
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,20 @@
import 'package:bloc/bloc.dart';
part 'reservation_state.dart';
class ReservationCubit extends Cubit<ReservationState> {
ReservationCubit() : super(const ReservationState());
void reserveProduct(String productId) {
if (state.reservedProductIds.contains(productId)) return;
final updatedList = List<String>.from(state.reservedProductIds)..add(productId);
emit(state.copyWith(reservedProductIds: updatedList));
// در اینجا میتوانید لاگیک مربوط به ارسال درخواست به API را نیز اضافه کنید
}
// متد برای بررسی اینکه آیا یک محصول خاص رزرو شده است یا نه
bool isProductReserved(String productId) {
return state.reservedProductIds.contains(productId);
}
}

View File

@ -0,0 +1,13 @@
part of 'reservation_cubit.dart';
class ReservationState {
final List<String> reservedProductIds;
const ReservationState({this.reservedProductIds = const []});
ReservationState copyWith({List<String>? reservedProductIds}) {
return ReservationState(
reservedProductIds: reservedProductIds ?? this.reservedProductIds,
);
}
}

View File

@ -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<String> 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<String>.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]),
),
],
);
}
}

View File

@ -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<ReservedListItemCard> createState() => _ReservedListItemCardState();
}
class _ReservedListItemCardState extends State<ReservedListItemCard> {
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<Widget> 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),
);
}
}

View File

@ -6,11 +6,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_localization/flutter_localization_plugin.h> #include <flutter_localization/flutter_localization_plugin.h>
#include <maps_launcher/maps_launcher_plugin.h> #include <maps_launcher/maps_launcher_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { 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 = g_autoptr(FlPluginRegistrar) flutter_localization_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin");
flutter_localization_plugin_register_with_registrar(flutter_localization_registrar); flutter_localization_plugin_register_with_registrar(flutter_localization_registrar);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_localization flutter_localization
maps_launcher maps_launcher
url_launcher_linux url_launcher_linux

View File

@ -5,6 +5,7 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_selector_macos
import flutter_localization import flutter_localization
import geolocator_apple import geolocator_apple
import maps_launcher import maps_launcher
@ -14,6 +15,7 @@ import sqflite_darwin
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin")) MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin"))

View File

@ -209,6 +209,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.27" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@ -289,6 +297,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" 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: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -371,6 +411,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_shaders:
dependency: transitive dependency: transitive
description: description:
@ -379,6 +427,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3" 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: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -501,6 +557,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" 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: image_size_getter:
dependency: transitive dependency: transitive
description: description:

View File

@ -50,6 +50,8 @@ dependencies:
maps_launcher: ^3.0.0+1 maps_launcher: ^3.0.0+1
slide_countdown: ^2.0.2 slide_countdown: ^2.0.2
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
flutter_staggered_grid_view: ^0.7.0
image_picker: ^1.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_localization/flutter_localization_plugin_c_api.h> #include <flutter_localization/flutter_localization_plugin_c_api.h>
#include <geolocator_windows/geolocator_windows.h> #include <geolocator_windows/geolocator_windows.h>
#include <maps_launcher/maps_launcher_plugin.h> #include <maps_launcher/maps_launcher_plugin.h>
@ -13,6 +14,8 @@
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterLocalizationPluginCApiRegisterWithRegistrar( FlutterLocalizationPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi")); registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi"));
GeolocatorWindowsRegisterWithRegistrar( GeolocatorWindowsRegisterWithRegistrar(

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_localization flutter_localization
geolocator_windows geolocator_windows
maps_launcher maps_launcher