From 791fc52bed8af251e165ede74250d2026d1184fc Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Sun, 29 Jun 2025 16:58:49 +0330 Subject: [PATCH] offer screen --- android/app/src/main/AndroidManifest.xml | 6 +- assets/icons/addImg.svg | 3 + assets/icons/back.svg | 4 + assets/icons/clockProduct.svg | 4 + assets/icons/edit.svg | 4 + assets/icons/location.svg | 4 + assets/icons/map.svg | 5 + assets/icons/routing.svg | 7 + assets/icons/shop.svg | 7 + assets/icons/shopping-cart.svg | 6 + assets/icons/star.svg | 3 + assets/icons/timer-pause.svg | 7 + assets/icons/volume-high.svg | 6 + assets/images/empty home.svg | 268 ++++++++++ lib/core/config/app_colors.dart | 9 +- lib/core/gen/assets.gen.dart | 59 ++- .../models/datasources/offer_data_source.dart | 277 ++++++++-- lib/data/models/offer_model.dart | 66 ++- lib/data/models/working_hours.dart | 24 + lib/data/repositories/offer_repository.dart | 27 + lib/main.dart | 183 +++---- lib/presentation/auth/bloc/auth_bloc.dart | 45 +- lib/presentation/auth/bloc/auth_event.dart | 36 +- lib/presentation/auth/bloc/auth_state.dart | 33 +- .../bloc/notification_preferences_bloc.dart | 1 - .../bloc/notification_preferences_state.dart | 2 + lib/presentation/offer/bloc/offer_bloc.dart | 44 +- lib/presentation/offer/bloc/offer_event.dart | 19 +- lib/presentation/offer/bloc/offer_state.dart | 34 +- .../bloc/widgets/category_offers_row.dart | 61 +++ .../offer/bloc/widgets/offer_card.dart | 192 +++++-- lib/presentation/pages/login_page.dart | 40 +- .../pages/notification_preferences_page.dart | 165 +++--- lib/presentation/pages/offers_page.dart | 286 +++++++++-- lib/presentation/pages/onboarding_page.dart | 37 +- lib/presentation/pages/otp_page.dart | 326 ++++++------ .../pages/product_detail_page.dart | 476 ++++++++++++++++++ lib/presentation/pages/user_info_page.dart | 71 ++- .../bloc/product_detail_bloc.dart | 35 ++ .../bloc/product_detail_event.dart | 19 + .../bloc/product_detail_state.dart | 31 ++ lib/presentation/widgets/gps_dialog.dart | 5 +- .../notification_permission_dialog.dart | 123 +++++ linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 8 + pubspec.lock | 276 +++++++++- pubspec.yaml | 7 + .../flutter/generated_plugin_registrant.cc | 9 + windows/flutter/generated_plugins.cmake | 3 + 50 files changed, 2697 insertions(+), 676 deletions(-) create mode 100644 assets/icons/addImg.svg create mode 100644 assets/icons/back.svg create mode 100644 assets/icons/clockProduct.svg create mode 100644 assets/icons/edit.svg create mode 100644 assets/icons/location.svg create mode 100644 assets/icons/map.svg create mode 100644 assets/icons/routing.svg create mode 100644 assets/icons/shop.svg create mode 100644 assets/icons/shopping-cart.svg create mode 100644 assets/icons/star.svg create mode 100644 assets/icons/timer-pause.svg create mode 100644 assets/icons/volume-high.svg create mode 100644 assets/images/empty home.svg create mode 100644 lib/data/models/working_hours.dart create mode 100644 lib/data/repositories/offer_repository.dart create mode 100644 lib/presentation/offer/bloc/widgets/category_offers_row.dart create mode 100644 lib/presentation/pages/product_detail_page.dart create mode 100644 lib/presentation/product_detail/bloc/product_detail_bloc.dart create mode 100644 lib/presentation/product_detail/bloc/product_detail_event.dart create mode 100644 lib/presentation/product_detail/bloc/product_detail_state.dart create mode 100644 lib/presentation/widgets/notification_permission_dialog.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c5f1fd5..1746dcf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,10 @@ + + + + + + diff --git a/assets/icons/back.svg b/assets/icons/back.svg new file mode 100644 index 0000000..e8f85f6 --- /dev/null +++ b/assets/icons/back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/clockProduct.svg b/assets/icons/clockProduct.svg new file mode 100644 index 0000000..a7ce302 --- /dev/null +++ b/assets/icons/clockProduct.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg new file mode 100644 index 0000000..8c2485b --- /dev/null +++ b/assets/icons/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/location.svg b/assets/icons/location.svg new file mode 100644 index 0000000..b3f53a7 --- /dev/null +++ b/assets/icons/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/map.svg b/assets/icons/map.svg new file mode 100644 index 0000000..f29e012 --- /dev/null +++ b/assets/icons/map.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/routing.svg b/assets/icons/routing.svg new file mode 100644 index 0000000..f2ec483 --- /dev/null +++ b/assets/icons/routing.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/shop.svg b/assets/icons/shop.svg new file mode 100644 index 0000000..04decc0 --- /dev/null +++ b/assets/icons/shop.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/shopping-cart.svg b/assets/icons/shopping-cart.svg new file mode 100644 index 0000000..c974fa0 --- /dev/null +++ b/assets/icons/shopping-cart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 0000000..b874e53 --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/timer-pause.svg b/assets/icons/timer-pause.svg new file mode 100644 index 0000000..0e21de0 --- /dev/null +++ b/assets/icons/timer-pause.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/volume-high.svg b/assets/icons/volume-high.svg new file mode 100644 index 0000000..5fd9753 --- /dev/null +++ b/assets/icons/volume-high.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/empty home.svg b/assets/images/empty home.svg new file mode 100644 index 0000000..d29c544 --- /dev/null +++ b/assets/images/empty home.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/core/config/app_colors.dart b/lib/core/config/app_colors.dart index 1b1ab7b..60660ff 100644 --- a/lib/core/config/app_colors.dart +++ b/lib/core/config/app_colors.dart @@ -1,10 +1,8 @@ -// lib/core/config/app_colors.dart import 'package:flutter/material.dart'; class AppColors { AppColors._(); - // رنگ‌های اصلی static const Color primary = Color.fromARGB(255, 23, 107, 173); static const Color unselected = Color.fromARGB(255, 186, 222, 251); static const Color border = Color.fromARGB(255, 14, 63, 102); @@ -13,8 +11,7 @@ class AppColors { static const Color confirm = Color.fromARGB(255, 69, 159, 73); static const Color unselectedBorder = Color.fromARGB(255, 233, 245, 254); static const Color hint = Color.fromARGB(255, 112, 112, 110); - - // رنگ‌های دیگر - static const Color grey = Color(0xFFBDBDBD); - static const Color white = Color(0xFFFFFFFF); + static const Color selectedImg = Color.fromARGB(255, 76, 175, 80); + static const Color singleOfferType = Color.fromARGB(255, 244, 67, 54); + static const Color countdown = Color.fromARGB(255, 54, 124, 57); } \ No newline at end of file diff --git a/lib/core/gen/assets.gen.dart b/lib/core/gen/assets.gen.dart index 706eb9c..20937d4 100644 --- a/lib/core/gen/assets.gen.dart +++ b/lib/core/gen/assets.gen.dart @@ -66,9 +66,15 @@ class $AssetsIconsGen { /// File path: assets/icons/Vector.svg SvgGenImage get vector => const SvgGenImage('assets/icons/Vector.svg'); + /// File path: assets/icons/addImg.svg + SvgGenImage get addImg => const SvgGenImage('assets/icons/addImg.svg'); + /// File path: assets/icons/arayesh.svg SvgGenImage get arayesh => const SvgGenImage('assets/icons/arayesh.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'); @@ -78,6 +84,10 @@ class $AssetsIconsGen { /// File path: assets/icons/clock.svg SvgGenImage get clock => const SvgGenImage('assets/icons/clock.svg'); + /// File path: assets/icons/clockProduct.svg + SvgGenImage get clockProduct => + const SvgGenImage('assets/icons/clockProduct.svg'); + /// File path: assets/icons/coffeeshop.svg SvgGenImage get coffeeshop => const SvgGenImage('assets/icons/coffeeshop.svg'); @@ -85,6 +95,9 @@ class $AssetsIconsGen { /// File path: assets/icons/digital.svg SvgGenImage get digital => const SvgGenImage('assets/icons/digital.svg'); + /// File path: assets/icons/edit.svg + SvgGenImage get edit => const SvgGenImage('assets/icons/edit.svg'); + /// File path: assets/icons/error.svg SvgGenImage get error => const SvgGenImage('assets/icons/error.svg'); @@ -98,9 +111,15 @@ class $AssetsIconsGen { /// File path: assets/icons/kafsh.svg SvgGenImage get kafsh => const SvgGenImage('assets/icons/kafsh.svg'); + /// File path: assets/icons/location.svg + SvgGenImage get location => const SvgGenImage('assets/icons/location.svg'); + /// File path: assets/icons/logo.svg SvgGenImage get logo => const SvgGenImage('assets/icons/logo.svg'); + /// File path: assets/icons/map.svg + SvgGenImage get map => const SvgGenImage('assets/icons/map.svg'); + /// File path: assets/icons/notification.svg SvgGenImage get notification => const SvgGenImage('assets/icons/notification.svg'); @@ -111,10 +130,23 @@ class $AssetsIconsGen { /// File path: assets/icons/resturan.svg SvgGenImage get resturan => const SvgGenImage('assets/icons/resturan.svg'); + /// File path: assets/icons/routing.svg + SvgGenImage get routing => const SvgGenImage('assets/icons/routing.svg'); + /// File path: assets/icons/scan-barcode.svg SvgGenImage get scanBarcode => const SvgGenImage('assets/icons/scan-barcode.svg'); + /// File path: assets/icons/shop.svg + SvgGenImage get shop => const SvgGenImage('assets/icons/shop.svg'); + + /// File path: assets/icons/shopping-cart.svg + SvgGenImage get shoppingCart => + const SvgGenImage('assets/icons/shopping-cart.svg'); + + /// File path: assets/icons/star.svg + SvgGenImage get star => const SvgGenImage('assets/icons/star.svg'); + /// File path: assets/icons/tala.svg SvgGenImage get tala => const SvgGenImage('assets/icons/tala.svg'); @@ -128,6 +160,14 @@ class $AssetsIconsGen { /// File path: assets/icons/tickPb.svg SvgGenImage get tickPb => const SvgGenImage('assets/icons/tickPb.svg'); + /// File path: assets/icons/timer-pause.svg + SvgGenImage get timerPause => + const SvgGenImage('assets/icons/timer-pause.svg'); + + /// File path: assets/icons/volume-high.svg + SvgGenImage get volumeHigh => + const SvgGenImage('assets/icons/volume-high.svg'); + /// List of all assets List get values => [ arrowRight2, @@ -146,31 +186,47 @@ class $AssetsIconsGen { tria, tshirt, vector, + addImg, arayesh, + back, backArrow, cinama, clock, + clockProduct, coffeeshop, digital, + edit, error, fastfood, globalSearch, kafsh, + location, logo, + map, notification, pooshak, resturan, + routing, scanBarcode, + shop, + shoppingCart, + star, tala, teria, tickCircle, tickPb, + timerPause, + volumeHigh, ]; } class $AssetsImagesGen { const $AssetsImagesGen(); + /// File path: assets/images/empty home.svg + SvgGenImage get emptyHome => + const SvgGenImage('assets/images/empty home.svg'); + /// File path: assets/images/onboarding1.png AssetGenImage get onboarding1 => const AssetGenImage('assets/images/onboarding1.png'); @@ -192,7 +248,8 @@ class $AssetsImagesGen { const AssetGenImage('assets/images/userinfo.png'); /// List of all assets - List get values => [ + List get values => [ + emptyHome, onboarding1, onboarding2, onboarding3, diff --git a/lib/data/models/datasources/offer_data_source.dart b/lib/data/models/datasources/offer_data_source.dart index 9afb1cb..6b8b032 100644 --- a/lib/data/models/datasources/offer_data_source.dart +++ b/lib/data/models/datasources/offer_data_source.dart @@ -1,57 +1,240 @@ - -// تعریف یک قرارداد که هم منبع داده واقعی و هم ساختگی از آن پیروی کنند import 'package:proxibuy/data/models/offer_model.dart'; +import 'package:proxibuy/data/models/working_hours.dart'; abstract class OfferDataSource { Future> getNearbyOffers(); + Future getOfferById(String id); // <<<<<<< جدید } - -// پیاده‌سازی منبع داده ساختگی class MockOfferDataSource implements OfferDataSource { + final List _mockOffers = [ + OfferModel( + id: '1', + storeName: 'روچیک (Ruchik)', + title: 'چیزبرگر', + discount: '۲۰٪', + imageUrls: [ + 'https://picsum.photos/seed/food/400/200', + 'https://picsum.photos/seed/burger1/400/400', + 'https://picsum.photos/seed/burger2/400/400', + ], + category: 'فست‌فود', + distanceInMeters: 130, + expiryTime: DateTime.now().add(const Duration(hours: 2, minutes: 30, seconds: 10)), + rating: 4.8, + workingHours: [ + WorkingHours( + day: 'شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'یکشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'دوشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'سه‌شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'چهارشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'پنج‌شنبه', + shifts: [ + Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), + Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), + ], + ), + WorkingHours(day: 'جمعه', shifts: []), + ], + discountType: 'رفیق‌بازی', + isOpen: false, + address: 'چهارباغ پایین ', + ratingCount: 340, + latitude: 32.660, + longitude: 51.670, + originalPrice: 150000, + finalPrice: 120000, + ), + OfferModel( + id: '2', + storeName: 'کاخ سرهنگ', + title: 'عصرانه', + discount: '۲۰% ', + imageUrls: [ + 'https://picsum.photos/seed/food/400/200', + 'https://picsum.photos/seed/burger1/400/400', + 'https://picsum.photos/seed/burger2/400/400', + ], + category: 'رستوران', + distanceInMeters: 130, + expiryTime: DateTime.now().add(const Duration(hours: 5)), + rating: 4.8, + workingHours: [ + WorkingHours( + day: 'شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'یکشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'دوشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'سه‌شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'چهارشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'پنج‌شنبه', + shifts: [ + Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), + Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), + ], + ), + WorkingHours(day: 'جمعه', shifts: []), // تعطیل + ], + discountType: 'رفیق‌بازی', + isOpen: true, + address: 'چهارباغ پایین ', + ratingCount: 340, + latitude: 32.660, + longitude: 51.670, + originalPrice: 150000, + finalPrice: 120000, + ), + OfferModel( + id: '3', + storeName: 'روچیک (Ruchik)', + title: 'چیزبرگر', + discount: '۲۰٪', + imageUrls: [ + 'https://picsum.photos/seed/food/400/200', + 'https://picsum.photos/seed/burger1/400/400', + 'https://picsum.photos/seed/burger2/400/400', + ], + category: 'فست‌فود', + distanceInMeters: 130, + expiryTime: DateTime.now().add(const Duration(hours: 5)), + rating: 4.8, + workingHours: [ + WorkingHours( + day: 'شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'یکشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'دوشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'سه‌شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'چهارشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'پنج‌شنبه', + shifts: [ + Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), + Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), + ], + ), + WorkingHours(day: 'جمعه', shifts: []), // تعطیل + ], + discountType: 'رفیق‌بازی', + isOpen: true, + address: 'چهارباغ پایین ', + ratingCount: 340, + latitude: 32.660, + longitude: 51.670, + originalPrice: 150000, + finalPrice: 120000, + ), + OfferModel( + id: '4', + storeName: 'روچیک (Ruchik)', + title: 'چیزبرگر', + discount: '۲۰٪', + imageUrls: [ + 'https://picsum.photos/seed/food/400/200', + 'https://picsum.photos/seed/burger1/400/400', + 'https://picsum.photos/seed/burger2/400/400', + ], + category: 'فست‌فود', + distanceInMeters: 130, + expiryTime: DateTime.now().add(const Duration(hours: 5)), + rating: 4.8, + workingHours: [ + WorkingHours( + day: 'شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'یکشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'دوشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'سه‌شنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'چهارشنبه', + shifts: [Shift(openAt: '۱۰ صبح', closeAt: '۱۰ شب')], + ), + WorkingHours( + day: 'پنج‌شنبه', + shifts: [ + Shift(openAt: '۱۰ صبح', closeAt: '۱ ظهر'), + Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), + ], + ), + WorkingHours(day: 'جمعه', shifts: []), + ], + discountType: 'رفیق‌بازی', + isOpen: false, + address: 'چهارباغ پایین ', + ratingCount: 340, + latitude: 32.660, + longitude: 51.670, + originalPrice: 150000, + finalPrice: 120000, + ), + ]; + @override Future> getNearbyOffers() async { - // شبیه‌سازی یک تاخیر ۲ ثانیه‌ای شبکه - await Future.delayed(const Duration(seconds: 2)); - - // برگرداندن لیستی از داده‌های هاردکد شده و ساختگی - return const [ - OfferModel( - id: '1', - storeName: 'رستوران شاندیز', - title: 'چلوکباب کوبیده مخصوص', - discount: '۲۰٪ تخفیف', - imageUrl: 'https://picsum.photos/seed/picsum/200/300', - category: 'رستوران', - distanceInMeters: 350, - ), - OfferModel( - id: '2', - storeName: 'کافه کتاب', - title: 'نوشیدنی گرم به همراه کیک روز', - discount: '۱۵٪ تخفیف', - imageUrl: 'https://picsum.photos/seed/coffee/200/300', - category: 'کافه', - distanceInMeters: 800, - ), - OfferModel( - id: '3', - storeName: 'فروشگاه پوشاک خانواده', - title: 'تمام اجناس فصل', - discount: 'تا ۵۰٪ تخفیف', - imageUrl: 'https://picsum.photos/seed/fashion/200/300', - category: 'پوشاک', - distanceInMeters: 1200, - ), - OfferModel( - id: '4', - storeName: 'پیتزا هات', - title: 'پیتزا پپرونی دو نفره', - discount: 'خرید یکی، دوتا ببر', - imageUrl: 'https://picsum.photos/seed/pizza/200/300', - category: 'فست فود', - distanceInMeters: 550, - ), - ]; + await Future.delayed(const Duration(seconds: 1)); + return _mockOffers; } -} \ No newline at end of file + + @override + Future getOfferById(String id) async { + await Future.delayed(const Duration(milliseconds: 300)); + try { + return _mockOffers.firstWhere((offer) => offer.id == id); + } catch (e) { + return null; + } + } +} diff --git a/lib/data/models/offer_model.dart b/lib/data/models/offer_model.dart index 98ea237..46ebd78 100644 --- a/lib/data/models/offer_model.dart +++ b/lib/data/models/offer_model.dart @@ -1,37 +1,67 @@ import 'package:equatable/equatable.dart'; +import 'package:proxibuy/data/models/working_hours.dart'; class OfferModel extends Equatable { final String id; final String storeName; final String title; - final String discount; // e.g., "۵۰٪ تخفیف" or "با خرید یکی، دوتا ببر" - final String imageUrl; - final String category; // e.g., "رستوران", "پوشاک" - final double distanceInMeters; + final String discount; + final List imageUrls; + final String category; + final int distanceInMeters; + final DateTime expiryTime; + final String address; + final List workingHours; + final String discountType; + final bool isOpen; + final double rating; + final int ratingCount; + final double latitude; + final double longitude; + final double originalPrice; + final double finalPrice; const OfferModel({ required this.id, required this.storeName, required this.title, required this.discount, - required this.imageUrl, + required this.imageUrls, required this.category, required this.distanceInMeters, + required this.expiryTime, + required this.address, + required this.workingHours, + required this.discountType, + required this.isOpen, + required this.rating, + required this.ratingCount, + required this.latitude, + required this.longitude, + required this.originalPrice, + required this.finalPrice, }); - // این تابع بعدا برای اتصال به API واقعی استفاده خواهد شد - factory OfferModel.fromJson(Map json) { - return OfferModel( - id: json['id'], - storeName: json['storeName'], - title: json['title'], - discount: json['discount'], - imageUrl: json['imageUrl'], - category: json['category'], - distanceInMeters: json['distanceInMeters'].toDouble(), - ); - } + String get coverImageUrl => + imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image'; @override - List get props => [id, storeName, title, discount, imageUrl, category, distanceInMeters]; + List get props => [ + id, + title, + storeName, + rating, + ratingCount, + latitude, + longitude, + ]; + + String get distanceAsString { + if (distanceInMeters < 1000) { + return "$distanceInMeters متر"; + } else { + final distanceInKm = (distanceInMeters / 1000).toStringAsFixed(1); + return "$distanceInKm کیلومتر"; + } + } } \ No newline at end of file diff --git a/lib/data/models/working_hours.dart b/lib/data/models/working_hours.dart new file mode 100644 index 0000000..b08a70b --- /dev/null +++ b/lib/data/models/working_hours.dart @@ -0,0 +1,24 @@ + +import 'package:equatable/equatable.dart'; + +class Shift extends Equatable { + final String openAt; + final String closeAt; + + const Shift({required this.openAt, required this.closeAt}); + + @override + List get props => [openAt, closeAt]; +} + +class WorkingHours extends Equatable { + final String day; + final List shifts; + + const WorkingHours({required this.day, required this.shifts}); + + bool get isOpen => shifts.isNotEmpty; + + @override + List get props => [day, shifts]; +} \ No newline at end of file diff --git a/lib/data/repositories/offer_repository.dart b/lib/data/repositories/offer_repository.dart new file mode 100644 index 0000000..165b18f --- /dev/null +++ b/lib/data/repositories/offer_repository.dart @@ -0,0 +1,27 @@ +import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; +import 'package:proxibuy/data/models/offer_model.dart'; + +class OfferRepository { + final OfferDataSource _offerDataSource; + + OfferRepository({required OfferDataSource offerDataSource}) + : _offerDataSource = offerDataSource; + + Future> fetchOffers({required List selectedCategories}) async { + final allOffers = await _offerDataSource.getNearbyOffers(); + + if (selectedCategories.isEmpty) { + return allOffers; + } + + final filteredOffers = allOffers + .where((offer) => selectedCategories.contains(offer.category)) + .toList(); + + return filteredOffers; + } + Future fetchOfferById(String id) async { + // در آینده این متد می‌تواند یک درخواست API برای گرفتن اطلاعات یک محصول خاص ارسال کند + return _offerDataSource.getOfferById(id); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b246eef..00cc52d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,20 @@ -// lib/main.dart - import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; +import 'package:proxibuy/data/repositories/offer_repository.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; -import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; -import 'core/config/app_colors.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 'core/config/app_colors.dart'; import 'presentation/pages/onboarding_page.dart'; void main() { + + Animate.restartOnHotReload = true; + runApp(const MyApp()); } @@ -18,102 +23,104 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - // استفاده از MultiBlocProvider برای ارائه تمام BLoC ها - return MultiBlocProvider( + return MultiRepositoryProvider( providers: [ - BlocProvider( - create: (context) => OfferBloc(dataSource: MockOfferDataSource()), - ), - BlocProvider( - create: (context) => AuthBloc(), + RepositoryProvider( + create: (context) => OfferRepository( + offerDataSource: MockOfferDataSource(), + ), ), ], - child: MaterialApp( - title: 'تخفیف یاب', // نام اپ - debugShowCheckedModeBanner: false, - - // ===== تنظیمات زبان فارسی و راست‌چین ===== - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => AuthBloc(), + ), + BlocProvider( + create: (context) => NotificationPreferencesBloc()..add(LoadCategories()), + ), + BlocProvider( + create: (context) => OffersBloc( + offerRepository: context.read(), + ), + ), ], - supportedLocales: const [ - Locale('fa'), // فارسی - ], - locale: const Locale('fa'), + child: MaterialApp( + title: 'Proxibuy', + debugShowCheckedModeBanner: false, - // ===== تعریف تم به روش مدرن ===== - theme: ThemeData( - fontFamily: 'Dana', // فونت وزیرمتن که قبلا تعریف کردیم - scaffoldBackgroundColor: Colors.white, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('fa'), + ], + locale: const Locale('fa'), - // ۱. تعریف ColorScheme به عنوان منبع اصلی رنگ‌ها - colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, // رنگ اصلی شما - primary: AppColors.primary, - // background: AppColors., - surface: Colors.white, - ), - - // ۲. تعریف تم برای ویجت‌های خاص - appBarTheme: const AppBarTheme( - backgroundColor: AppColors.primary, - foregroundColor: Colors.grey, // رنگ آیکون و متن روی AppBar - elevation: 0, - ), - - inputDecorationTheme: InputDecorationTheme( - filled: true, // <--- ۱. این خط اضافه شد - fillColor: Colors.white, // <--- ۲. این خط اضافه شد - floatingLabelBehavior: FloatingLabelBehavior.always, - contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), // <--- ۳. این خط برای پدینگ داخلی بهتر اضافه شد - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), // رنگ بوردر در حالت عادی + theme: ThemeData( + fontFamily: 'Dana', + scaffoldBackgroundColor: Colors.white, + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + primary: AppColors.primary, + surface: Colors.white, ), - enabledBorder: OutlineInputBorder( // استایل بوردر در حالت فعال ولی بدون فوکوس - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.primary, width: 2), - ), - labelStyle: const TextStyle(color: Colors.black), - hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)), - ), - - outlinedButtonTheme: OutlinedButtonThemeData( // <--- تم دکمه گوگل - style: OutlinedButton.styleFrom( + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.primary, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - side: const BorderSide(color: AppColors.grey), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), - textStyle: const TextStyle( - fontFamily: 'Dana', - fontSize: 16, - color: Colors.black + elevation: 0, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.white, + floatingLabelBehavior: FloatingLabelBehavior.always, + contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.primary, width: 2), + ), + labelStyle: const TextStyle(color: Colors.black), + hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.black, // رنگ متن دکمه Outlined + padding: const EdgeInsets.symmetric(vertical: 16), + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), + textStyle: const TextStyle( + fontFamily: 'Dana', + fontSize: 16, + color: Colors.black, + ), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.button, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), + textStyle: const TextStyle( + fontFamily: 'Dana', + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ), ), - - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.button, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), - textStyle: const TextStyle( - fontFamily: 'Dana', - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - + home: const OnboardingPage(), ), - home: const OnboardingPage(), ), ); } diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart index 16c3d3b..842d12d 100644 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ b/lib/presentation/auth/bloc/auth_bloc.dart @@ -1,40 +1,43 @@ -// lib/presentation/bloc/auth/auth_bloc.dart -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'dart:async'; part 'auth_event.dart'; part 'auth_state.dart'; class AuthBloc extends Bloc { AuthBloc() : super(AuthInitial()) { - - // مدیریت رویداد ارسال کد - on((event, emit) async { + on((event, emit) async { emit(AuthLoading()); - await Future.delayed(const Duration(seconds: 2)); - if (event.phoneNumber.endsWith('0000')) { - emit(const AuthFailure('شماره وارد شده نامعتبر است.')); - } else { + await Future.delayed(const Duration(seconds: 1)); + if (event.phoneNumber.isNotEmpty) { emit(AuthCodeSentSuccess()); + } else { + emit(AuthFailure('شماره موبایل معتبر نیست.')); } }); - // مدیریت رویداد تایید کد - on((event, emit) async { + on((event, emit) async { emit(AuthLoading()); - await Future.delayed(const Duration(seconds: 2)); - if (event.otpCode == '12345') { // یک کد جادویی برای تست + await Future.delayed(const Duration(seconds: 1)); + if (event.otp == '12345') { emit(AuthVerified()); } else { - emit(const AuthFailure('کد وارد شده صحیح نمی‌باشد.')); + emit(AuthFailure('کد تایید صحیح نمی‌باشد.')); } }); - on((event, emit) async { + + on((event, emit) async { emit(AuthLoading()); - await Future.delayed(const Duration(seconds: 2)); - // در اینجا منطق واقعی ذخیره در سرور یا دیتابیس قرار میگیرد - print('User Info Saved: Name=${event.name}, Gender=${event.gender}'); - emit(UserInfoUpdateSuccess()); + await Future.delayed(const Duration(milliseconds: 500)); + + print('User info to save: Name: ${event.name}, Gender: ${event.gender}'); + + if (event.name.trim().isEmpty) { + emit(AuthFailure('لطفاً نام خود را وارد کنید.')); + } else { + emit(UserInfoSaved()); + } }); } -} \ No newline at end of file +} diff --git a/lib/presentation/auth/bloc/auth_event.dart b/lib/presentation/auth/bloc/auth_event.dart index 5ff693c..ae15595 100644 --- a/lib/presentation/auth/bloc/auth_event.dart +++ b/lib/presentation/auth/bloc/auth_event.dart @@ -1,32 +1,24 @@ -// lib/presentation/bloc/auth/auth_event.dart + part of 'auth_bloc.dart'; -abstract class AuthEvent extends Equatable { - const AuthEvent(); - @override - List get props => []; -} +@immutable +abstract class AuthEvent {} -// برای ارسال اولیه کد -class SendOtpEvent extends AuthEvent { +class SendOTPEvent extends AuthEvent { final String phoneNumber; - const SendOtpEvent(this.phoneNumber); - @override - List get props => [phoneNumber]; + + SendOTPEvent({required this.phoneNumber}); } -// برای تایید کد وارد شده -class VerifyOtpEvent extends AuthEvent { - final String otpCode; - const VerifyOtpEvent(this.otpCode); - @override - List get props => [otpCode]; +class VerifyOTPEvent extends AuthEvent { + final String otp; + + VerifyOTPEvent({required this.otp}); } -class UpdateUserInfoEvent extends AuthEvent { +class SaveUserInfoEvent extends AuthEvent { final String name; final String gender; - const UpdateUserInfoEvent({required this.name, required this.gender}); - @override - List get props => [name, gender]; -} \ No newline at end of file + + SaveUserInfoEvent({required this.name, required this.gender}); +} diff --git a/lib/presentation/auth/bloc/auth_state.dart b/lib/presentation/auth/bloc/auth_state.dart index 8c00e98..497c253 100644 --- a/lib/presentation/auth/bloc/auth_state.dart +++ b/lib/presentation/auth/bloc/auth_state.dart @@ -1,20 +1,21 @@ -// lib/presentation/bloc/auth/auth_state.dart + part of 'auth_bloc.dart'; -abstract class AuthState extends Equatable { - const AuthState(); - @override - List get props => []; -} +@immutable +abstract class AuthState {} -class AuthInitial extends AuthState {} // وضعیت اولیه -class AuthLoading extends AuthState {} // در حال پردازش (برای همه رویدادها) -class AuthCodeSentSuccess extends AuthState {} // کد با موفقیت ارسال شد -class AuthVerified extends AuthState {} // کد با موفقیت تایید شد و کاربر وارد شد -class AuthFailure extends AuthState { // بروز خطا +class AuthInitial extends AuthState {} + +class AuthLoading extends AuthState {} + +class AuthCodeSentSuccess extends AuthState {} + +class AuthVerified extends AuthState {} + +class UserInfoSaved extends AuthState {} + +class AuthFailure extends AuthState { final String message; - const AuthFailure(this.message); - @override - List get props => [message]; -} -class UserInfoUpdateSuccess extends AuthState {} + + AuthFailure(this.message); +} \ No newline at end of file diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart index 2544b47..29ed792 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart @@ -13,7 +13,6 @@ class NotificationPreferencesBloc void _onLoadCategories( LoadCategories event, Emitter emit) { - // Mock data, replace with API call later final categories = [ CategoryEntity(id: 1, name: 'تریا', icon: Assets.icons.teria), CategoryEntity(id: 2, name: 'پوشاک', icon: Assets.icons.pooshak), diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart index 1fc9ef6..0e346d6 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart @@ -1,3 +1,4 @@ + import 'package:equatable/equatable.dart'; import 'package:proxibuy/domain/entities/category_entity.dart'; @@ -20,6 +21,7 @@ class NotificationPreferencesState extends Equatable { ); } + @override List get props => [categories, selectedCategoryIds]; } \ No newline at end of file diff --git a/lib/presentation/offer/bloc/offer_bloc.dart b/lib/presentation/offer/bloc/offer_bloc.dart index 33f9511..7c42783 100644 --- a/lib/presentation/offer/bloc/offer_bloc.dart +++ b/lib/presentation/offer/bloc/offer_bloc.dart @@ -1,24 +1,30 @@ -// lib/presentation/bloc/offer/offer_bloc.dart -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; -import '../../../../data/models/offer_model.dart'; +import 'package:bloc/bloc.dart'; +import 'package:proxibuy/data/repositories/offer_repository.dart'; +import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; +import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; -part 'offer_event.dart'; -part 'offer_state.dart'; -class OfferBloc extends Bloc { - final OfferDataSource dataSource; +class OffersBloc extends Bloc { + final OfferRepository _offerRepository; - OfferBloc({required this.dataSource}) : super(OfferInitial()) { - on((event, emit) async { - emit(OfferLoading()); - try { - final offers = await dataSource.getNearbyOffers(); - emit(OfferSuccess(offers)); - } catch (e) { - emit(OfferFailure('متاسفانه خطایی رخ داد: ${e.toString()}')); - } - }); + OffersBloc({required OfferRepository offerRepository}) + : _offerRepository = offerRepository, + super(OffersInitial()) { + on(_onFetchRequested); + } + + Future _onFetchRequested( + OffersFetchRequested event, + Emitter emit, + ) async { + emit(OffersLoadInProgress()); + try { + final offers = await _offerRepository.fetchOffers( + selectedCategories: event.selectedCategories, + ); + emit(OffersLoadSuccess(offers)); + } catch (e) { + emit(OffersLoadFailure(e.toString())); + } } } \ No newline at end of file diff --git a/lib/presentation/offer/bloc/offer_event.dart b/lib/presentation/offer/bloc/offer_event.dart index fbc83eb..b656e96 100644 --- a/lib/presentation/offer/bloc/offer_event.dart +++ b/lib/presentation/offer/bloc/offer_event.dart @@ -1,10 +1,19 @@ -// lib/presentation/bloc/offer/offer_event.dart -part of 'offer_bloc.dart'; -abstract class OfferEvent extends Equatable { - const OfferEvent(); +import 'package:equatable/equatable.dart'; + +abstract class OffersEvent extends Equatable { + const OffersEvent(); + @override List get props => []; } -class FetchNearbyOffers extends OfferEvent {} \ No newline at end of file +class OffersFetchRequested extends OffersEvent { + // ✅ ۱. یک فیلد برای نگهداری دسته‌بندی‌ها اضافه کنید + final List selectedCategories; + + const OffersFetchRequested({required this.selectedCategories}); + + @override + List get props => [selectedCategories]; +} \ No newline at end of file diff --git a/lib/presentation/offer/bloc/offer_state.dart b/lib/presentation/offer/bloc/offer_state.dart index d6f9197..9aae2f6 100644 --- a/lib/presentation/offer/bloc/offer_state.dart +++ b/lib/presentation/offer/bloc/offer_state.dart @@ -1,23 +1,33 @@ -// lib/presentation/bloc/offer/offer_state.dart -part of 'offer_bloc.dart'; -abstract class OfferState extends Equatable { - const OfferState(); + +import 'package:equatable/equatable.dart'; +import 'package:proxibuy/data/models/offer_model.dart'; + +abstract class OffersState extends Equatable { + const OffersState(); + @override List get props => []; } -class OfferInitial extends OfferState {} -class OfferLoading extends OfferState {} -class OfferSuccess extends OfferState { +class OffersInitial extends OffersState {} + +class OffersLoadInProgress extends OffersState {} + +class OffersLoadSuccess extends OffersState { final List offers; - const OfferSuccess(this.offers); + + const OffersLoadSuccess(this.offers); + @override List get props => [offers]; } -class OfferFailure extends OfferState { - final String message; - const OfferFailure(this.message); + +class OffersLoadFailure extends OffersState { + final String error; + + const OffersLoadFailure(this.error); + @override - List get props => [message]; + List get props => [error]; } \ No newline at end of file diff --git a/lib/presentation/offer/bloc/widgets/category_offers_row.dart b/lib/presentation/offer/bloc/widgets/category_offers_row.dart new file mode 100644 index 0000000..eb38f0c --- /dev/null +++ b/lib/presentation/offer/bloc/widgets/category_offers_row.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:proxibuy/data/models/offer_model.dart'; +import 'package:proxibuy/presentation/offer/bloc/widgets/offer_card.dart'; +import 'package:proxibuy/presentation/pages/product_detail_page.dart'; // <-- این خط را اضافه کن + +class CategoryOffersRow extends StatelessWidget { + final String categoryTitle; + final List offers; + + const CategoryOffersRow({ + super.key, + required this.categoryTitle, + required this.offers, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text( + categoryTitle, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox( + height: 300, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: offers.length, + itemBuilder: (context, index) { + final offer = offers[index]; + return Padding( + padding: const EdgeInsets.only(left: 12.0), + child: OfferCard( + offer: offer, + width: 320, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return ProductDetailPage(offerId: offer.id,); + }, + ), + ); + }, + ), + ); + }, + ), + ), + // const SizedBox(height: 16), + ], + ); + } +} \ No newline at end of file diff --git a/lib/presentation/offer/bloc/widgets/offer_card.dart b/lib/presentation/offer/bloc/widgets/offer_card.dart index eac7b69..eee6d5d 100644 --- a/lib/presentation/offer/bloc/widgets/offer_card.dart +++ b/lib/presentation/offer/bloc/widgets/offer_card.dart @@ -1,54 +1,162 @@ -// lib/presentation/widgets/offer_card.dart +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:proxibuy/data/models/offer_model.dart'; -class OfferCard extends StatelessWidget { +class OfferCard extends StatefulWidget { final OfferModel offer; - const OfferCard({super.key, required this.offer}); + final VoidCallback? onTap; + final double width; + + const OfferCard({ + super.key, + required this.offer, + this.onTap, + this.width = 150, + }); + @override + State createState() => _OfferCardState(); +} + +class _OfferCardState extends State { @override Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - elevation: 4, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - offer.storeName, - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - Text( - offer.title, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.red.shade100, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - offer.discount, - style: TextStyle( - color: Colors.red.shade800, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 8), - Text( - 'فاصله: ${offer.distanceInMeters.toInt()} متر', - style: Theme.of(context).textTheme.bodySmall, - ), - ], + final textTheme = Theme.of(context).textTheme; + + return SizedBox( + width: 270, + child: GestureDetector( + onTap: widget.onTap, + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Column( + children: [ + Stack(children: [_buildOfferImage()]), + _buildInfoContainer(textTheme), + ], + ), ), ), ); } + + Widget _buildOfferImage() { + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: CachedNetworkImage( + imageUrl: widget.offer.coverImageUrl, + height: 140, + width: double.infinity, + fit: BoxFit.cover, + placeholder: + (context, url) => Container( + height: 140, + color: Colors.grey[300], + child: const Center(child: CircularProgressIndicator()), + ), + errorWidget: + (context, url, error) => Container( + height: 140, + color: Colors.grey[300], + child: const Icon(Icons.broken_image, color: Colors.grey), + ), + ), + ); + } + + Widget _buildInfoContainer(TextTheme textTheme) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset(Assets.icons.shop.path), + const SizedBox(width: 4), + Text( + widget.offer.storeName, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + SvgPicture.asset(Assets.icons.shoppingCart.path), + const SizedBox(width: 4), + Text( + widget.offer.title, + style: textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + const SizedBox(height: 10), + + // ================== شروع تغییرات ================== + Row( + children: [ + SvgPicture.asset(Assets.icons.location.path), + const SizedBox(width: 4), + // ویجت Flexible باعث می‌شود که آدرس، فضای باقی‌مانده را پر کند + // و در صورت طولانی بودن، کوتاه شود. + Flexible( + child: Text( + widget.offer.address, + style: textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, // نمایش سه نقطه در انتهای متن طولانی + ), + ), + const SizedBox(width: 4), + // این بخش همیشه به طور کامل نمایش داده می‌شود + Text( + '(${widget.offer.distanceInMeters.toString()}متر تا تخفیف)', + style: textTheme.bodySmall, + ), + ], + ), + + const SizedBox(height: 10), + + Row( + children: [ + SvgPicture.asset(Assets.icons.routing.path), + const SizedBox(width: 4), + Text( + 'نوع تخفیف : ${widget.offer.discount} ${widget.offer.discountType}', + style: const TextStyle(color: Color.fromARGB(255, 183, 28, 28),fontSize: 14), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + ); + } } \ No newline at end of file diff --git a/lib/presentation/pages/login_page.dart b/lib/presentation/pages/login_page.dart index b5a15bb..ece61eb 100644 --- a/lib/presentation/pages/login_page.dart +++ b/lib/presentation/pages/login_page.dart @@ -1,4 +1,5 @@ // lib/presentation/pages/login_page.dart + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:country_picker/country_picker.dart'; @@ -19,11 +20,16 @@ class _LoginPageState extends State { Country _selectedCountry = Country.parse('IR'); bool _keepSignedIn = false; + @override + void dispose() { + _phoneController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - // ۱. حل مشکل چپ‌چینی با Directionality return Directionality( textDirection: TextDirection.rtl, child: Scaffold( @@ -43,7 +49,6 @@ class _LoginPageState extends State { ), ), const SizedBox(height: 48), - TextField( readOnly: true, controller: TextEditingController( @@ -67,7 +72,6 @@ class _LoginPageState extends State { ), ), const SizedBox(height: 16), - TextField( controller: _phoneController, keyboardType: TextInputType.phone, @@ -76,8 +80,6 @@ class _LoginPageState extends State { decoration: InputDecoration( labelText: "شماره موبایل", hintText: "- - - - - - - - - -", - // --- تغییر اصلی اینجاست --- - // prefixIcon با suffix جایگزین شد تا در سمت چپ قرار گیرد suffix: Padding( padding: const EdgeInsets.symmetric(horizontal: 2.0), child: Text( @@ -90,21 +92,18 @@ class _LoginPageState extends State { ), ), const SizedBox(height: 16), - Row( children: [ Checkbox( value: _keepSignedIn, - onChanged: - (value) => - setState(() => _keepSignedIn = value ?? false), + onChanged: (value) => + setState(() => _keepSignedIn = value ?? false), activeColor: AppColors.primary, ), Text("مرا به خاطر بسپار", style: textTheme.bodyMedium), ], ), const SizedBox(height: 24), - BlocConsumer( listener: (context, state) { if (state is AuthFailure) { @@ -116,11 +115,12 @@ class _LoginPageState extends State { ); } if (state is AuthCodeSentSuccess) { - final fullPhoneNumber = - "0${_phoneController.text}"; - Navigator.push(context, MaterialPageRoute( - builder: (_) => OtpPage(phoneNumber: fullPhoneNumber) - )); + final fullPhoneNumber = "0${_phoneController.text}"; + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + OtpPage(phoneNumber: fullPhoneNumber))); } }, builder: (context, state) { @@ -137,8 +137,6 @@ class _LoginPageState extends State { }, ), const SizedBox(height: 32), - - // ۲. افزودن بخش "ادامه با" و دکمه گوگل Row( children: [ const Expanded(child: Divider()), @@ -196,8 +194,8 @@ class _LoginPageState extends State { } void _sendOtp() { - final fullPhoneNumber = - "+${_selectedCountry.phoneCode}${_phoneController.text}"; - context.read().add(SendOtpEvent(fullPhoneNumber)); + context + .read() + .add(SendOTPEvent(phoneNumber: _phoneController.text)); } -} +} \ No newline at end of file diff --git a/lib/presentation/pages/notification_preferences_page.dart b/lib/presentation/pages/notification_preferences_page.dart index 2a61ae2..7c9c129 100644 --- a/lib/presentation/pages/notification_preferences_page.dart +++ b/lib/presentation/pages/notification_preferences_page.dart @@ -2,24 +2,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; -import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_state.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; +import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; import 'package:proxibuy/presentation/pages/offers_page.dart'; import 'package:proxibuy/presentation/widgets/category_selection_card.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class NotificationPreferencesPage extends StatelessWidget { - const NotificationPreferencesPage({Key? key}) : super(key: key); + const NotificationPreferencesPage({super.key}); static Route route() { return MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => - NotificationPreferencesBloc()..add(LoadCategories()), - child: const NotificationPreferencesPage(), - ), + builder: (_) => const NotificationPreferencesPage(), ); } @@ -84,14 +81,15 @@ class NotificationPreferencesPage extends StatelessWidget { ), const SizedBox(height: 24), Expanded( - child: BlocBuilder( + child: BlocBuilder< + NotificationPreferencesBloc, + NotificationPreferencesState + >( builder: (context, state) { if (state.categories.isEmpty) { return const Center(child: CircularProgressIndicator()); } - // START OF CHANGE: Replacing GridView with Wrap const double sidePadding = 24.0; const double crossAxisSpacing = 16.0; const int crossAxisCount = 3; @@ -111,73 +109,106 @@ class NotificationPreferencesPage extends StatelessWidget { alignment: WrapAlignment.center, spacing: crossAxisSpacing, runSpacing: mainAxisSpacing, - children: state.categories.map((category) { - final isSelected = - state.selectedCategoryIds.contains(category.id); - return SizedBox( - width: itemWidth, - height: itemHeight, - child: CategorySelectionCard( - name: category.name, - icon: category.icon, - isSelected: isSelected, - showSelectableIndicator: - state.selectedCategoryIds.isNotEmpty, - onTap: () { - context - .read() - .add(ToggleCategorySelection(category.id)); - }, - ), - ); - }).toList(), + children: + state.categories.map((category) { + final isSelected = state.selectedCategoryIds + .contains(category.id); + return SizedBox( + width: itemWidth, + height: itemHeight, + child: CategorySelectionCard( + name: category.name, + icon: category.icon, + isSelected: isSelected, + showSelectableIndicator: + state.selectedCategoryIds.isNotEmpty, + onTap: () { + context + .read() + .add( + ToggleCategorySelection(category.id), + ); + }, + ), + ); + }).toList(), ), ); - // END OF CHANGE }, ), ), - BlocBuilder( + BlocBuilder< + NotificationPreferencesBloc, + NotificationPreferencesState + >( builder: (context, state) { final isEnabled = state.selectedCategoryIds.isNotEmpty; return SizedBox( width: double.infinity, - child: isEnabled - ? ElevatedButton( - onPressed: isEnabled - ? () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => OfferBloc( - dataSource: - MockOfferDataSource()), - child: const OffersPage(), - ), - ), - ); - } - : null, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.confirm, - foregroundColor: Colors.white, - disabledBackgroundColor: Colors.grey, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + child: + isEnabled + ? ElevatedButton( + onPressed: + isEnabled + ? () async { + final selectedCategoryNames = + state.categories + .where( + (cat) => state + .selectedCategoryIds + .contains(cat.id), + ) + .map((cat) => cat.name) + .toList(); + + final prefs = + await SharedPreferences.getInstance(); + await prefs.setStringList( + 'user_selected_categories', + selectedCategoryNames, + ); + + if (context.mounted) { + context.read().add( + OffersFetchRequested( + selectedCategories: + selectedCategoryNames, + ), + ); + } + + if (!context.mounted) return; + + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: + (context) => const OffersPage( + showDialogsOnLoad: true, + ), + ), + ); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.confirm, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), ), - ), - child: const Text( - 'اعمال', - style: TextStyle( - fontFamily: 'Dana', - fontSize: 16, - fontWeight: FontWeight.bold, + child: const Text( + 'اعمال', + style: TextStyle( + fontFamily: 'Dana', + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), - ), - ) - : const SizedBox(), + ) + : const SizedBox(), ); }, ), @@ -187,4 +218,4 @@ class NotificationPreferencesPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/pages/offers_page.dart b/lib/presentation/pages/offers_page.dart index 33ecdac..e320eab 100644 --- a/lib/presentation/pages/offers_page.dart +++ b/lib/presentation/pages/offers_page.dart @@ -1,69 +1,263 @@ -// lib/presentation/pages/offers_page.dart +import 'package:collection/collection.dart'; 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:geolocator/geolocator.dart'; +import 'package:proxibuy/core/config/app_colors.dart'; +import 'package:proxibuy/core/gen/assets.gen.dart'; +import 'package:proxibuy/data/models/offer_model.dart'; +import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; +import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; -import 'package:proxibuy/presentation/offer/bloc/widgets/offer_card.dart'; +import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; +import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; +import 'package:proxibuy/presentation/offer/bloc/widgets/category_offers_row.dart'; +import 'package:proxibuy/presentation/pages/notification_preferences_page.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'; class OffersPage extends StatefulWidget { - const OffersPage({super.key}); + final bool showDialogsOnLoad; + + const OffersPage({super.key, this.showDialogsOnLoad = false}); @override State createState() => _OffersPageState(); } class _OffersPageState extends State { + List _selectedCategories = []; + @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - await showGPSDialog(context); - if (mounted) { - context.read().add(FetchNearbyOffers()); - } - }); + _loadOffersAndPreferences(); + + if (widget.showDialogsOnLoad) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + await showGPSDialog(context); + } + if (mounted) { + await showNotificationPermissionDialog(context); + } + }); + } + } + + Future _loadOffersAndPreferences() async { + final prefs = await SharedPreferences.getInstance(); + final savedCategories = + prefs.getStringList('user_selected_categories') ?? []; + + if (mounted) { + setState(() { + _selectedCategories = savedCategories; + }); + context.read().add( + OffersFetchRequested(selectedCategories: savedCategories), + ); + } + } + + Widget _buildFavoriteCategoriesSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'دسته‌بندی‌های مورد علاقه شما', + style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold), + ), + TextButton( + onPressed: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: + (_) => BlocProvider.value( + value: + context.read() + ..add(LoadCategories()), + child: const NotificationPreferencesPage(), + ), + ), + ); + + if (result == true) { + _loadOffersAndPreferences(); + } + }, + child: Row( + children: [ + // چون asset مربوط به ویرایش وجود نداشت، از آیکون فلاتر استفاده شد + const Icon(Icons.edit, size: 18, color: AppColors.primary), + const SizedBox(width: 4), + const Text('ویرایش'), + ], + ), + ), + ], + ), + const Divider(height: 1), + const SizedBox(height: 12), + if (_selectedCategories.isEmpty) + const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + 'شما هنوز دسته‌بندی مورد علاقه خود را انتخاب نکرده‌اید.', + style: TextStyle(color: Colors.grey), + ), + ) + else + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: + _selectedCategories.map((category) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 6.0, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(20.0), + ), + child: Text(category), + ); + }).toList(), + ), + ], + ), + ); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('تخفیف‌های اطراف من'), - ), - body: BlocBuilder( - builder: (context, state) { - if (state is OfferLoading) { - return const Center(child: CircularProgressIndicator()); - } - if (state is OfferSuccess) { - return ListView.builder( - itemCount: state.offers.length, - itemBuilder: (context, index) { - final offer = state.offers[index]; - return OfferCard(offer: offer); - }, - ); - } - if (state is OfferFailure) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(state.message), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - context.read().add(FetchNearbyOffers()); - }, - child: const Text('تلاش مجدد'), - ) - ], - ), - ); - } - return const Center(child: Text('برای شروع، صفحه را رفرش کنید')); - }, + return Directionality( + textDirection: TextDirection.rtl, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + automaticallyImplyLeading: false, + title: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15.0, + vertical: 0.0, + ), + 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), + ], + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_buildFavoriteCategoriesSection(), const OffersView()], + ), + ), ), ); } +} + +class OffersView extends StatelessWidget { + const OffersView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is OffersLoadInProgress || state is OffersInitial) { + return const SizedBox( + height: 300, + child: Center(child: CircularProgressIndicator()), + ); + } + if (state is OffersLoadSuccess) { + if (state.offers.isEmpty) { + return Center( + child: SizedBox( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 85), + SvgPicture.asset(Assets.images.emptyHome.path), + const SizedBox(height: 60), + ElevatedButton( + onPressed: () async { + await Geolocator.openLocationSettings(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.confirm, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey, + padding: const EdgeInsets.symmetric(vertical: 12,horizontal: 125), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: const Text( + 'فعال‌سازی GPS', + style: TextStyle( + fontFamily: 'Dana', + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + ), + const SizedBox(height: 15), + const Text('جست‌وجوی تصادفی') + ], + ), + ), + ), + ); + } + + final groupedOffers = groupBy( + state.offers, + (OfferModel offer) => offer.category, + ); + final categories = groupedOffers.keys.toList(); + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 16), + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + 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); + }, + ); + } + if (state is OffersLoadFailure) { + return SizedBox( + height: 200, + child: Center(child: Text("خطا در بارگذاری: ${state.error}")), + ); + } + return const SizedBox.shrink(); + }, + ); + } } \ No newline at end of file diff --git a/lib/presentation/pages/onboarding_page.dart b/lib/presentation/pages/onboarding_page.dart index ad63e81..f569cdf 100644 --- a/lib/presentation/pages/onboarding_page.dart +++ b/lib/presentation/pages/onboarding_page.dart @@ -1,17 +1,12 @@ -// lib/presentation/pages/onboarding_page.dart - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/pages/login_page.dart'; -import 'dart:ui'; // برای استفاده از ImageFilter import '../../core/config/app_colors.dart'; import '../../core/gen/assets.gen.dart'; import '../../domain/entities/onboarding_entity.dart'; import '../widgets/onboarding_indicator_painter.dart'; -import 'offers_page.dart'; -// داده‌های مربوط به هر صفحه آنبوردینگ (بدون تغییر) final List onboardingPages = [ OnboardingEntity( imagePath: Assets.images.onboarding1.path, @@ -75,7 +70,7 @@ class _OnboardingPageState extends State { } else { Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (context) => BlocProvider( // یک BLoC جدید برای صفحه لاگین فراهم می‌کنیم + builder: (context) => BlocProvider( create: (context) => AuthBloc(), child: const LoginPage(), ), @@ -89,9 +84,8 @@ class _OnboardingPageState extends State { return Scaffold( body: Stack( children: [ - // لایه اول: نمایش عکس‌ها با PageView (تمام صفحه) Directionality( - textDirection: TextDirection.ltr, // <--- این خط کلیدی است + textDirection: TextDirection.ltr, child: PageView.builder( controller: _pageController, itemCount: onboardingPages.length, @@ -107,7 +101,6 @@ class _OnboardingPageState extends State { ), ), - // لایه سوم: دکمه بازگشت if (_currentPage > 0) Positioned( top: 50, @@ -122,15 +115,11 @@ class _OnboardingPageState extends State { }, ), ), - - // لایه چهارم: محتوای اصلی (دکمه، ایندیکیتور و متن) Positioned( - top: 580, // فاصله از پایین صفحه - left: 24, - right: 24, - child: Column( - children: [ - SizedBox( + bottom: 250, + left: 0, + right: 0, + child: SizedBox( width: 80, height: 80, child: Stack( @@ -166,9 +155,14 @@ class _OnboardingPageState extends State { ), ], ), - ), + ),), + Positioned( + bottom: 85, + left: 24, + right: 24, + child: Column( + children: [ const SizedBox(height: 40), - // بخش عنوان و توضیحات Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -181,8 +175,7 @@ class _OnboardingPageState extends State { fontWeight: FontWeight.bold, fontSize: 20, color: - AppColors - .white, // متن سفید برای خوانایی روی گرادیانت + Colors.white ), ), const SizedBox(height: 16), @@ -190,7 +183,7 @@ class _OnboardingPageState extends State { onboardingPages[_currentPage].description, textAlign: TextAlign.right, style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppColors.white.withOpacity(0.8), // کمی شفاف + color: Colors.white.withOpacity(0.8), height: 1.5, ), ), diff --git a/lib/presentation/pages/otp_page.dart b/lib/presentation/pages/otp_page.dart index 27e6165..ede236b 100644 --- a/lib/presentation/pages/otp_page.dart +++ b/lib/presentation/pages/otp_page.dart @@ -1,4 +1,4 @@ -// lib/presentation/pages/otp_page.dart + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; @@ -18,22 +18,21 @@ class OtpPage extends StatefulWidget { class _OtpPageState extends State { final List _focusNodes = List.generate(5, (_) => FocusNode()); - final List _controllers = List.generate( - 5, - (_) => TextEditingController(), - ); + final List _controllers = + List.generate(5, (_) => TextEditingController()); late final OtpTimerHelper _otpTimer; bool _hasError = false; String? _errorMessage; - - // ۱. افزودن متغیر وضعیت برای کنترل دکمه bool _isOtpComplete = false; @override void initState() { super.initState(); _otpTimer = OtpTimerHelper()..startTimer(); + WidgetsBinding.instance.addPostFrameCallback((_) { + FocusScope.of(context).requestFocus(_focusNodes[0]); + }); } @override @@ -48,6 +47,136 @@ class _OtpPageState extends State { super.dispose(); } + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return Directionality( + textDirection: TextDirection.rtl, + child: Scaffold( + appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Assets.icons.logo.svg(height: 160)), + const SizedBox(height: 40), + Text( + "کد یکبار مصرف", + style: textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + "کد تایید به شماره ${widget.phoneNumber} ارسال شد.", + style: textTheme.titleMedium?.copyWith( + color: Colors.grey, + height: 1.5, + ), + ), + SizedBox(height: 15,), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(Assets.icons.vector.path), + const SizedBox(width: 4), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("ویرایش شماره همراه",style: TextStyle(color: AppColors.active),), + ), + ], + ), + const SizedBox(height: 15), + _buildOtpFields(), + const SizedBox(height: 15), + if (_errorMessage != null) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Center( + child: Text( + _errorMessage!, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ) + else + const SizedBox(height: 32), + + BlocConsumer( + listener: (context, state) { + if (state is AuthFailure) { + setState(() { + _hasError = true; + _errorMessage = state.message; + }); + } + if (state is AuthVerified) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => const UserInfoPage(), + ), + (route) => false, + ); + } + }, + builder: (context, state) { + if (state is AuthLoading) { + return const Center(child: CircularProgressIndicator()); + } + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isOtpComplete ? _verifyOtp : null, + child: const Text("ورود"), + ), + ); + }, + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.icons.clock.path, + colorFilter: const ColorFilter.mode( + Colors.grey, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 8), + ValueListenableBuilder( + valueListenable: _otpTimer.canResend, + builder: (context, canResend, child) { + return canResend + ? TextButton( + onPressed: _resendOtp, + child: const Text("ارسال مجدد کد",style: TextStyle(color: AppColors.active),), + ) + : ValueListenableBuilder( + valueListenable: _otpTimer.remainingSeconds, + builder: (context, seconds, child) => Text( + "${_otpTimer.formatTime()} تا دریافت مجدد", + style: const TextStyle(color: Colors.grey), + ), + ); + }, + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + Widget _buildOtpFields() { return Directionality( textDirection: TextDirection.ltr, @@ -73,21 +202,20 @@ class _OtpPageState extends State { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( - color: - _hasError - ? Colors.red - : (Theme.of(context) - .inputDecorationTheme - .enabledBorder - ?.borderSide - .color ?? - AppColors.grey), + color: _hasError + ? Colors.red + : (Theme.of(context) + .inputDecorationTheme + .enabledBorder + ?.borderSide + .color ?? + Colors.grey), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( - color: _hasError ? Colors.red : AppColors.primary, + color: _hasError ? Colors.red : AppColors.active, width: 2, ), ), @@ -105,8 +233,6 @@ class _OtpPageState extends State { if (value.isEmpty && index > 0) { FocusScope.of(context).requestFocus(_focusNodes[index - 1]); } - - // ۲. به‌روزرسانی وضعیت کامل بودن کد final otpCode = _controllers.map((c) => c.text).join(); setState(() { _isOtpComplete = otpCode.length == 5; @@ -119,162 +245,10 @@ class _OtpPageState extends State { ); } - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - - return Directionality( - textDirection: TextDirection.rtl, - child: Scaffold( - appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - - children: [ - Center(child: Assets.icons.logo.svg(height: 160)), - - const SizedBox(height: 40), - - Text( - "کد یکبار مصرف", - style: textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - - const SizedBox(height: 12), - - // --- تغییر اصلی: افزودن دکمه ویرایش شماره --- - Text( - "کد تایید به شماره ${widget.phoneNumber} ارسال شد.", - - style: textTheme.titleMedium?.copyWith( - color: Colors.grey, - height: 1.5, - ), - ), - - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset(Assets.icons.vector.path), - Center( - child: TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("ویرایش شماره همراه"), - ), - ), - ], - ), - const SizedBox(height: 15), - _buildOtpFields(), - const SizedBox(height: 15), - - if (_errorMessage != null) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Center( - child: Text( - _errorMessage!, - style: const TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ), - ) - else - const SizedBox(height: 40), - - BlocConsumer( - listener: (context, state) { - if (state is AuthLoading) { - setState(() { - _hasError = false; - _errorMessage = null; - }); - } else if (state is AuthFailure) { - setState(() { - _hasError = true; - _errorMessage = state.message; - }); - } else if (state is AuthVerified) { - if (state is AuthVerified) { - // این بخش را از کامنت خارج کنید - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (_) => const UserInfoPage(), - ), - (route) => false, - ); - } - } - }, - builder: (context, state) { - if (state is AuthLoading) { - return const Center(child: CircularProgressIndicator()); - } - return SizedBox( - width: double.infinity, - child: ElevatedButton( - // ۳. شرطی کردن دکمه "ورود" - onPressed: _isOtpComplete ? _verifyOtp : null, - child: const Text("ورود"), - ), - ); - }, - ), - const SizedBox(height: 32), - - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.icons.clock.path, - colorFilter: const ColorFilter.mode( - Colors.grey, - BlendMode.srcIn, - ), - ), - const SizedBox(width: 8), - ValueListenableBuilder( - valueListenable: _otpTimer.canResend, - builder: (context, canResend, child) { - return canResend - ? TextButton( - onPressed: _resendOtp, - child: const Text("ارسال مجدد کد"), - ) - : ValueListenableBuilder( - valueListenable: _otpTimer.remainingSeconds, - builder: - (context, seconds, child) => Text( - "${_otpTimer.formatTime()} تا دریافت مجدد", - style: const TextStyle( - color: Colors.grey, - ), - ), - ); - }, - ), - ], - ), - ], - ), - ), - ), - ), - ); - } - void _verifyOtp() { final otpCode = _controllers.map((c) => c.text).join(); - // یک بررسی اضافه می‌کنیم که اگر دکمه فعال بود حتما کد کامل باشد if (otpCode.length == 5) { - context.read().add(VerifyOtpEvent(otpCode)); + context.read().add(VerifyOTPEvent(otp: otpCode)); } } @@ -282,8 +256,12 @@ class _OtpPageState extends State { setState(() { _hasError = false; _errorMessage = null; + for (var controller in _controllers) { + controller.clear(); + } + _isOtpComplete = false; }); - context.read().add(SendOtpEvent(widget.phoneNumber)); + context.read().add(SendOTPEvent(phoneNumber: widget.phoneNumber)); _otpTimer.resetTimer(); } -} +} \ No newline at end of file diff --git a/lib/presentation/pages/product_detail_page.dart b/lib/presentation/pages/product_detail_page.dart new file mode 100644 index 0000000..b448c8f --- /dev/null +++ b/lib/presentation/pages/product_detail_page.dart @@ -0,0 +1,476 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:maps_launcher/maps_launcher.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/data/repositories/offer_repository.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:slide_countdown/slide_countdown.dart'; + +class ProductDetailPage extends StatelessWidget { + final String offerId; + + const ProductDetailPage({super.key, required this.offerId}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ProductDetailBloc( + offerRepository: context.read(), + )..add(ProductDetailFetchRequested(offerId: offerId)), + child: Scaffold( + body: BlocBuilder( + builder: (context, state) { + if (state is ProductDetailLoadInProgress || state is ProductDetailInitial) { + return const Center(child: CircularProgressIndicator()); + } + if (state is ProductDetailLoadFailure) { + return Center(child: Text('خطا: ${state.error}')); + } + if (state is ProductDetailLoadSuccess) { + return ProductDetailView(offer: state.offer); + } + return const SizedBox.shrink(); + }, + ), + ), + ); + } +} + +class ProductDetailView extends StatefulWidget { + final OfferModel offer; + + const ProductDetailView({super.key, required this.offer}); + + @override + State createState() => _ProductDetailViewState(); +} + +class _ProductDetailViewState extends State { + late List imageList; + late String selectedImage; + final String _uploadKey = 'upload_image'; + + @override + void initState() { + super.initState(); + imageList = List.from(widget.offer.imageUrls)..add(_uploadKey); + selectedImage = imageList.first; + } + + void _launchMaps(double lat, double lon, String title) { + MapsLauncher.launchCoordinates(lat, lon, title); + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: SingleChildScrollView( + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMainImage(context), + const SizedBox(height: 52.5), + _buildProductInfo(), + ], + ), + Positioned( + top: 400 - 52.5, + left: 0, + right: 0, + child: _buildThumbnailList(), + ), + ], + ), + ), + ); + } + + Widget _buildMainImage(BuildContext context) { + return Stack( + children: [ + SizedBox( + height: 400, + width: double.infinity, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: Image.network( + selectedImage, + key: ValueKey(selectedImage), + fit: BoxFit.cover, + width: double.infinity, + height: 400, + loadingBuilder: (context, child, progress) { + return progress == null + ? child + : const Center(child: CircularProgressIndicator()); + }, + ), + ), + ), + Positioned( + top: 40, + left: 16, + child: GestureDetector( + child: SvgPicture.asset(Assets.icons.back.path), + onTap: () { + Navigator.pop(context); + }, + ), + ), + ], + ); + } + + Widget _buildThumbnailList() { + return Directionality( + textDirection: TextDirection.ltr, + child: Container( + height: 105, + padding: const EdgeInsets.symmetric(vertical: 8), + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: imageList.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) { + final img = imageList[index]; + if (img == _uploadKey) { + return _buildUploadButton(); + } + final isSelected = selectedImage == img; + return _buildThumbnail(img, isSelected); + }, + ), + ), + ); + } + + Widget _buildThumbnail(String img, bool isSelected) { + const grayscaleMatrix = [ + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, + ]; + + return GestureDetector( + onTap: () => setState(() => selectedImage = img), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + border: Border.all( + color: isSelected ? AppColors.selectedImg : Colors.transparent, + width: 2.5, + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 7, + spreadRadius: 0, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: ColorFiltered( + colorFilter: ColorFilter.matrix(isSelected ? [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + ] : grayscaleMatrix), + child: Image.network(img, width: 90, height: 90, fit: BoxFit.cover), + ), + ), + ), + ); + } + + Widget _buildUploadButton() { + return GestureDetector( + onTap: () { + print("Upload image tapped!"); + }, + child: Container( + width: 90, + height: 90, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade400, width: 1.5), + ), + child: Padding( + padding: const EdgeInsets.all(9.0), + child: SvgPicture.asset(Assets.icons.addImg.path), + ), + ), + ); + } + + Widget _buildProductInfo() { + final remainingDuration = widget.offer.expiryTime.isAfter(DateTime.now()) + ? widget.offer.expiryTime.difference(DateTime.now()) + : Duration.zero; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0).copyWith(bottom: 24.0, top: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset(Assets.icons.shop.path, height: 30), + const SizedBox(width: 6), + Text(widget.offer.storeName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 16), + _buildInfoRow(icon: Assets.icons.location, text: widget.offer.address), + const SizedBox(height: 12), + ExpandableInfoRow( + icon: Assets.icons.clock, + titleWidget: Row( + children: [ + Text( + widget.offer.isOpen ? "باز است" : "بسته است", + style: TextStyle( + fontSize: 16, + color: widget.offer.isOpen ? Colors.green.shade700 : Colors.red.shade700, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + children: widget.offer.workingHours.map((wh) => Padding( + padding: const EdgeInsets.only(right: 10.0, bottom: 8.0, top: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(wh.day, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), + wh.isOpen + ? Text(wh.shifts.map((s) => '${s.openAt} - ${s.closeAt}').join(' | '), style: const TextStyle(fontSize: 15, color: AppColors.hint)) + : const Text('تعطیل', style: TextStyle(fontSize: 15, color: Colors.red)), + ], + ), + )).toList(), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () => _launchMaps(widget.offer.latitude, widget.offer.longitude, widget.offer.storeName), + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + children: [ + SvgPicture.asset( + Assets.icons.map.path, + width: 25, + height: 25, + colorFilter: const ColorFilter.mode(AppColors.button, BlendMode.srcIn), + ), + const SizedBox(width: 8), + const Text('مسیر فروشگاه روی نقشه', style: TextStyle(fontSize: 17, color: AppColors.button)), + ], + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 60, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: const BoxDecoration( + color: AppColors.selectedImg, + borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(widget.offer.rating.toString(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 11)), + const SizedBox(width: 2), + SvgPicture.asset(Assets.icons.star.path, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn)), + ], + ), + ), + ), + Container( + height: 30, + width: 60, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8)), + ), + child: Center(child: Text('${widget.offer.ratingCount} نفر', style: const TextStyle(color: Colors.black, fontSize: 11, fontWeight: FontWeight.bold))), + ), + ], + ), + ], + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11), + decoration: BoxDecoration( + color: AppColors.singleOfferType, + borderRadius: BorderRadius.circular(20), + ), + child: Text("تخفیف ${widget.offer.discountType}", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('(${widget.offer.discount})', style: const TextStyle(fontSize: 16, color: AppColors.singleOfferType, fontWeight: FontWeight.normal)), + const SizedBox(width: 8), + Text( + widget.offer.originalPrice.toStringAsFixed(0), + style: TextStyle(fontSize: 16, color: Colors.grey.shade600, decoration: TextDecoration.lineThrough), + ), + ], + ), + const SizedBox(height: 1), + Text('${widget.offer.finalPrice.toStringAsFixed(0)} تومان', style: const TextStyle(color: AppColors.singleOfferType, fontSize: 22, fontWeight: FontWeight.bold)), + ], + ), + ], + ), + const SizedBox(height: 24), + _buildInfoRow(icon: Assets.icons.timerPause, text: "مهلت استفاده از تخفیف"), + const SizedBox(height: 10), + if (remainingDuration > Duration.zero) + Column( + children: [ + Localizations.override( + context: context, + locale: const Locale('en'), + child: SlideCountdown( + duration: remainingDuration, + slideDirection: SlideDirection.up, + separator: ':', + style: const TextStyle( + fontSize: 85, + fontWeight: FontWeight.bold, + color: AppColors.countdown, + ), + separatorStyle: const TextStyle( + fontSize: 40, + color: AppColors.countdown, + ), + decoration: const BoxDecoration( + color: Colors.white, + ), + shouldShowDays: (duration) => duration.inDays > 0, + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 60.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("ثانیه", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)), + Text("دقیقه", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)), + Text("ساعت", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)), + ], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildInfoRow({required SvgGenImage icon, required String text}) { + return Row( + children: [ + SvgPicture.asset(icon.path, width: 22, height: 22, colorFilter: ColorFilter.mode(Colors.grey.shade600, BlendMode.srcIn)), + const SizedBox(width: 6), + Expanded(child: Text(text, style: const TextStyle(fontSize: 16, color: AppColors.hint))), + ], + ); + } +} + +class ExpandableInfoRow extends StatefulWidget { + final SvgGenImage icon; + final Widget titleWidget; + final List children; + + const ExpandableInfoRow({ + super.key, + required this.icon, + required this.titleWidget, + this.children = const [], + }); + + @override + State createState() => _ExpandableInfoRowState(); +} + +class _ExpandableInfoRowState extends State { + bool _isExpanded = false; + + void _toggleExpand() { + if (widget.children.isNotEmpty) { + setState(() { + _isExpanded = !_isExpanded; + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + InkWell( + onTap: _toggleExpand, + child: Row( + children: [ + SvgPicture.asset(widget.icon.path, width: 22, height: 22, colorFilter: ColorFilter.mode(Colors.grey.shade600, BlendMode.srcIn)), + const SizedBox(width: 6), + Expanded(child: widget.titleWidget), + if (widget.children.isNotEmpty) + AnimatedRotation( + turns: _isExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: const Icon(Icons.keyboard_arrow_down, color: Colors.black), + ), + ], + ), + ), + AnimatedCrossFade( + firstChild: Container(), + secondChild: Column(children: widget.children), + crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/presentation/pages/user_info_page.dart b/lib/presentation/pages/user_info_page.dart index 4f9426b..1eae480 100644 --- a/lib/presentation/pages/user_info_page.dart +++ b/lib/presentation/pages/user_info_page.dart @@ -5,7 +5,6 @@ import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/pages/notification_preferences_page.dart'; import '../../core/config/app_colors.dart'; import '../../core/gen/assets.gen.dart'; -import 'offers_page.dart'; // صفحه اصلی بعد از ورود class UserInfoPage extends StatefulWidget { const UserInfoPage({super.key}); @@ -16,7 +15,7 @@ class UserInfoPage extends StatefulWidget { class _UserInfoPageState extends State { final _nameController = TextEditingController(); - String _selectedGender = 'مرد'; // مقدار پیش‌فرض + String _selectedGender = 'مرد'; @override void dispose() { @@ -24,7 +23,6 @@ class _UserInfoPageState extends State { super.dispose(); } - // ویجت برای ساخت دکمه‌های رادیویی جنسیت Widget _buildGenderRadio(String title, String value) { return InkWell( onTap: () => setState(() => _selectedGender = value), @@ -34,9 +32,8 @@ class _UserInfoPageState extends State { Radio( value: value, groupValue: _selectedGender, - onChanged: - (newValue) => setState(() => _selectedGender = newValue!), - activeColor: AppColors.active, + onChanged: (newValue) => setState(() => _selectedGender = newValue!), + activeColor: AppColors.primary, ), Text(title, style: const TextStyle(color: Colors.grey)), ], @@ -51,19 +48,17 @@ class _UserInfoPageState extends State { return Scaffold( body: Stack( children: [ - // لایه پس‌زمینه: عکس Positioned.fill( child: Image.asset( - Assets.images.userinfo.path, // یک عکس پس‌زمینه دلخواه + Assets.images.userinfo.path, fit: BoxFit.cover, ), ), - // لایه رویی: باتم شیت ثابت DraggableScrollableSheet( - initialChildSize: 0.50, // ارتفاع اولیه باتم شیت (۶۵٪ صفحه) - minChildSize: 0.50, // حداقل ارتفاع - maxChildSize: 0.50, // حداکثر ارتفاع هنگام اسکرول + initialChildSize: 0.50, + minChildSize: 0.50, + maxChildSize: 0.50, builder: (context, scrollController) { return Container( decoration: const BoxDecoration( @@ -81,15 +76,15 @@ class _UserInfoPageState extends State { width: 50, height: 5, decoration: BoxDecoration( - color: AppColors.grey.withOpacity(0.5), + color:Colors.grey.withOpacity(0.5), borderRadius: BorderRadius.circular(12), ), ), ), - SizedBox(height: 40), - // فیلد نام و نام خانوادگی + const SizedBox(height: 40), TextField( controller: _nameController, + textAlign: TextAlign.right, decoration: const InputDecoration( labelText: "دوست داری با چه اسمی صدات کنیم؟", labelStyle: TextStyle( @@ -97,15 +92,11 @@ class _UserInfoPageState extends State { fontSize: 20, ), hintText: "مثلا نام کوچک شما", - hintStyle: TextStyle( - fontSize: 15, - color: Colors.grey - ) + hintStyle: TextStyle(fontSize: 15, color: Colors.grey), ), ), const SizedBox(height: 24), - // بخش انتخاب جنسیت Text( "جنسیت", style: textTheme.titleMedium?.copyWith( @@ -119,24 +110,22 @@ class _UserInfoPageState extends State { children: [ _buildGenderRadio('مرد', 'مرد'), _buildGenderRadio('زن', 'زن'), - _buildGenderRadio('تمایلی به پاسخ ندارم', 'تمایلی به پاسخ ندارم'), + _buildGenderRadio('تمایلی به پاسخ ندارم', 'نامشخص'), ], ), const SizedBox(height: 70), - // دکمه ادامه BlocConsumer( listener: (context, state) { - if (state is UserInfoUpdateSuccess) { - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const OffersPage()), - (route) => false, - ); - } else if (state is AuthFailure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.message), backgroundColor: Colors.red) - ); - } + if (state is UserInfoSaved) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const NotificationPreferencesPage()), + ); + } else if (state is AuthFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.message), backgroundColor: Colors.red), + ); + } }, builder: (context, state) { if (state is AuthLoading) { @@ -145,25 +134,23 @@ class _UserInfoPageState extends State { return SizedBox( width: double.infinity, child: ElevatedButton( - // ۱. تغییر رنگ دکمه به سبز style: ElevatedButton.styleFrom( - backgroundColor: AppColors.confirm, + backgroundColor: AppColors.confirm, + foregroundColor: Colors.white, ), onPressed: () { - context.read().add(UpdateUserInfoEvent( - name: _nameController.text, - gender: _selectedGender, - )); + context.read().add(SaveUserInfoEvent( + name: _nameController.text, + gender: _selectedGender, + )); }, child: const Text("اعمال"), ), ); }, ), - const SizedBox(height: 9), - - // ۲. افزودن دکمه رد کردن + Center( child: TextButton( onPressed: () { @@ -185,4 +172,4 @@ class _UserInfoPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/presentation/product_detail/bloc/product_detail_bloc.dart b/lib/presentation/product_detail/bloc/product_detail_bloc.dart new file mode 100644 index 0000000..dda68e3 --- /dev/null +++ b/lib/presentation/product_detail/bloc/product_detail_bloc.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:proxibuy/data/models/offer_model.dart'; +import 'package:proxibuy/data/repositories/offer_repository.dart'; +import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart'; +import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart'; + + +class ProductDetailBloc extends Bloc { + final OfferRepository _offerRepository; + + ProductDetailBloc({required OfferRepository offerRepository}) + : _offerRepository = offerRepository, + super(ProductDetailInitial()) { + on(_onFetchRequested); + } + + Future _onFetchRequested( + ProductDetailFetchRequested event, + Emitter emit, + ) async { + emit(ProductDetailLoadInProgress()); + try { + // از ریپازیتوری می‌خواهیم که محصول با این ID را به ما بدهد + final offer = await _offerRepository.fetchOfferById(event.offerId); + if (offer != null) { + emit(ProductDetailLoadSuccess(offer)); + } else { + emit(const ProductDetailLoadFailure('محصول مورد نظر یافت نشد.')); + } + } catch (e) { + emit(ProductDetailLoadFailure(e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/presentation/product_detail/bloc/product_detail_event.dart b/lib/presentation/product_detail/bloc/product_detail_event.dart new file mode 100644 index 0000000..9071dcf --- /dev/null +++ b/lib/presentation/product_detail/bloc/product_detail_event.dart @@ -0,0 +1,19 @@ + +import 'package:equatable/equatable.dart'; + +abstract class ProductDetailEvent extends Equatable { + const ProductDetailEvent(); + + @override + List get props => []; +} + +// این ایونت زمانی فراخوانی می‌شود که بخواهیم جزئیات یک محصول را دریافت کنیم +class ProductDetailFetchRequested extends ProductDetailEvent { + final String offerId; + + const ProductDetailFetchRequested({required this.offerId}); + + @override + List get props => [offerId]; +} \ No newline at end of file diff --git a/lib/presentation/product_detail/bloc/product_detail_state.dart b/lib/presentation/product_detail/bloc/product_detail_state.dart new file mode 100644 index 0000000..23e0b63 --- /dev/null +++ b/lib/presentation/product_detail/bloc/product_detail_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:proxibuy/data/models/offer_model.dart'; + +abstract class ProductDetailState extends Equatable { + const ProductDetailState(); + + @override + List get props => []; +} + +class ProductDetailInitial extends ProductDetailState {} + +class ProductDetailLoadInProgress extends ProductDetailState {} + +class ProductDetailLoadSuccess extends ProductDetailState { + final OfferModel offer; + + const ProductDetailLoadSuccess(this.offer); + + @override + List get props => [offer]; +} + +class ProductDetailLoadFailure extends ProductDetailState { + final String error; + + const ProductDetailLoadFailure(this.error); + + @override + List get props => [error]; +} \ No newline at end of file diff --git a/lib/presentation/widgets/gps_dialog.dart b/lib/presentation/widgets/gps_dialog.dart index 25443ad..320dc47 100644 --- a/lib/presentation/widgets/gps_dialog.dart +++ b/lib/presentation/widgets/gps_dialog.dart @@ -1,4 +1,3 @@ -// lib/presentation/widgets/gps_dialog.dart import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:geolocator/geolocator.dart'; @@ -8,7 +7,7 @@ import 'package:proxibuy/core/gen/assets.gen.dart'; Future showGPSDialog(BuildContext context) async { bool isLocationEnabled = await Geolocator.isLocationServiceEnabled(); if (!isLocationEnabled) { - showDialog( + await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { @@ -42,7 +41,7 @@ Future showGPSDialog(BuildContext context) async { const Text( "برای اینکه بتونیم تخفیف‌های اطرافت رو سریع بهت اطلاع بدیم، اجازه بده به موقعیت مکانیت دسترسی داشته باشیم.", style: TextStyle( - color: AppColors.hint, + color: AppColors.hint, fontSize: 16, ), textAlign: TextAlign.start, diff --git a/lib/presentation/widgets/notification_permission_dialog.dart b/lib/presentation/widgets/notification_permission_dialog.dart new file mode 100644 index 0000000..be952c7 --- /dev/null +++ b/lib/presentation/widgets/notification_permission_dialog.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:proxibuy/core/config/app_colors.dart'; +import 'package:proxibuy/core/gen/assets.gen.dart'; + +Future showNotificationPermissionDialog(BuildContext context) async { + final status = await Permission.notification.status; + if (status.isGranted) { + return; + } + + await 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: 50, + left: 20, + right: 20, + bottom: 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "دسترسی به اعلان‌ها", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 15), + const Text( + "وقتی یه تخفیف جذاب از فروشگاه‌های مورد علاقه‌ات فعال بشه، با یه اعلان فورا خبرت می‌کنیم. فقط کافیه اجازه ارسال اعلان رو بدی.", + style: TextStyle( + color: AppColors.hint, + fontSize: 16, + height: 1.5, + ), + 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 { + final result = + await Permission.notification.request(); + if (result.isGranted) { + Navigator.of(dialogContext).pop(); + } else { + openAppSettings(); + } + }, + 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.volumeHigh.path), + ), + ), + ), + ), + ], + ), + ); + }, + ); +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 10e19fe..bfafdd0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,17 @@ #include "generated_plugin_registrant.h" #include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_localization_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin"); flutter_localization_plugin_register_with_registrar(flutter_localization_registrar); + g_autoptr(FlPluginRegistrar) maps_launcher_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MapsLauncherPlugin"); + maps_launcher_plugin_register_with_registrar(maps_launcher_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2284757..866b317 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,8 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_localization + maps_launcher + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3f1e729..f2712cd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,18 @@ import Foundation import flutter_localization import geolocator_apple +import maps_launcher +import path_provider_foundation import shared_preferences_foundation +import sqflite_darwin +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 53001de..406ebb3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.10.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -154,7 +178,7 @@ packages: source: hosted version: "4.10.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" @@ -278,6 +302,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" flutter_bloc: dependency: "direct main" description: @@ -286,6 +318,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_gen: dependency: "direct main" description: @@ -331,6 +371,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_svg: dependency: "direct main" description: @@ -533,6 +581,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + maps_launcher: + dependency: "direct main" + description: + name: maps_launcher + sha256: dac4c609720211fa6336b5903d917fe45e545c6b5665978efc3db2a3f436b1ae + url: "https://pub.dev" + source: hosted + version: "3.0.0+1" matcher: dependency: transitive description: @@ -573,6 +629,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -597,6 +661,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -621,6 +709,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" + url: "https://pub.dev" + source: hosted + version: "12.0.0+1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -685,8 +829,16 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - shared_preferences: + rxdart: dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" description: name: shared_preferences sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" @@ -762,6 +914,14 @@ packages: description: flutter source: sdk version: "0.0.0" + slide_countdown: + dependency: "direct main" + description: + name: slide_countdown + sha256: "363914f96389502467d4dc9c0f26e88f93df3d8e37de2d5ff05b16d981fe973d" + url: "https://pub.dev" + source: hosted + version: "2.0.2" source_span: dependency: transitive description: @@ -778,6 +938,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" + url: "https://pub.dev" + source: hosted + version: "2.5.5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -810,6 +1010,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" term_glyph: dependency: transitive description: @@ -858,6 +1066,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 983f380..03e2c72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,13 @@ dependencies: flutter_gen: ^5.10.0 country_picker: ^2.0.27 geolocator: ^14.0.1 + permission_handler: ^12.0.0+1 + cached_network_image: ^3.4.1 + collection: ^1.19.1 + shared_preferences: ^2.5.3 + flutter_animate: ^4.5.2 + maps_launcher: ^3.0.0+1 + slide_countdown: ^2.0.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index eb82257..a125364 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,19 @@ #include #include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterLocalizationPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); + MapsLauncherPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MapsLauncherPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4b0ee23..1888886 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_localization geolocator_windows + maps_launcher + permission_handler_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST