diff --git a/lib/config/theme_data.dart b/lib/config/theme_data.dart index d15a530..c326176 100644 --- a/lib/config/theme_data.dart +++ b/lib/config/theme_data.dart @@ -95,6 +95,7 @@ class DarkThemeConfig { color: _colorScheme.text, ), checkboxTheme: CheckboxThemeData( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, fillColor: MaterialStateProperty.all(_colorScheme.primary), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), diff --git a/lib/models/enums.dart b/lib/models/enums.dart index 1859789..1a7fd8d 100644 --- a/lib/models/enums.dart +++ b/lib/models/enums.dart @@ -9,3 +9,9 @@ enum ButtonStyleMode { primary, flat, } + +enum ALertType { + error, + info, + success, +} diff --git a/lib/pages/home/radar/radar.dart b/lib/pages/home/radar/radar.dart index 81060b9..a0e9674 100644 --- a/lib/pages/home/radar/radar.dart +++ b/lib/pages/home/radar/radar.dart @@ -1,12 +1,28 @@ +import 'dart:async'; + import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/radar_category.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/pages/home/radar/radar_state.dart'; import 'package:didvan/pages/home/radar/widgets/categories_gird.dart'; import 'package:didvan/pages/home/radar/widgets/categories_list.dart'; import 'package:didvan/pages/home/radar/widgets/radar_item.dart'; import 'package:didvan/pages/home/radar/widgets/search_field.dart'; import 'package:didvan/pages/home/widgets/logo_app_bar.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/widgets/date_picker_button.dart'; +import 'package:didvan/widgets/didvan/card.dart'; +import 'package:didvan/widgets/didvan/checkbox.dart'; +import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/text.dart'; +import 'package:didvan/widgets/item_title.dart'; +import 'package:didvan/widgets/shimmer_placeholder.dart'; +import 'package:didvan/widgets/sliver_state_handler.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class Radar extends StatefulWidget { const Radar({Key? key}) : super(key: key); @@ -21,64 +37,105 @@ class _RadarState extends State { bool _isColapsed = false; bool _isAnimating = false; + Timer? _timer; + @override void initState() { _scrollController.addListener(() async { _handleAnimations(); }); + Future.delayed(Duration.zero, () { + context.read().getRadarOverviews(page: 1); + }); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( - body: Stack( - children: [ - CustomScrollView( - physics: _isAnimating - ? const NeverScrollableScrollPhysics() - : const ScrollPhysics(), - controller: _scrollController, - slivers: [ - const SliverToBoxAdapter(child: LogoAppBar()), - SliverPadding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - sliver: SliverToBoxAdapter( - child: SearchField( - title: 'رادار', - onChanged: (value) {}, + body: Consumer( + child: SliverPadding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + sliver: SliverToBoxAdapter( + child: Row( + children: [ + Expanded( + child: SearchField(title: 'رادار', onChanged: _onChanged), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: _showFilterBottomSheet, + child: const Icon( + DidvanIcons.filter_regular, + size: 32, ), ), - ), - SliverPadding( - padding: const EdgeInsets.only(top: 284, right: 16, bottom: 20), - sliver: SliverToBoxAdapter( - child: DidvanText( - 'آخرین رصد', - style: Theme.of(context).textTheme.subtitle1, - color: Theme.of(context).colorScheme.title, - ), - ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => const Padding( - padding: EdgeInsets.only(bottom: 20), - child: RadarItem(), - ), - childCount: 10, - ), - ), - ], + ], + ), ), - CategoriesRow1(isColapsed: _isColapsed), - CategoriesRow2(isColapsed: _isColapsed), - CategoriesList(isColapsed: _isColapsed), - ], + ), + builder: (context, state, child) => Stack( + children: [ + CustomScrollView( + physics: _isAnimating || + (state.appState == AppState.busy && state.radars.isEmpty) + ? const NeverScrollableScrollPhysics() + : const ScrollPhysics(), + controller: _scrollController, + slivers: [ + const SliverToBoxAdapter(child: LogoAppBar()), + child!, + if (!state.isFiltering) + const SliverToBoxAdapter( + child: SizedBox(height: 284), + ), + if (state.radars.isNotEmpty && !state.isFiltering) + SliverPadding( + padding: const EdgeInsets.only(right: 16, bottom: 20), + sliver: SliverToBoxAdapter( + child: DidvanText( + 'آخرین رصد', + style: Theme.of(context).textTheme.subtitle1, + color: Theme.of(context).colorScheme.title, + ), + ), + ), + SliverStateHandler( + state: state, + itemPadding: const EdgeInsets.only( + bottom: 20, + left: 16, + right: 16, + ), + placeholder: const _RadarItemPlaceholder(), + builder: (context, state, index) => RadarItem( + radar: state.radars[index], + ), + childCount: state.radars.length, + ), + ], + ), + CategoriesRow1(isColapsed: _isColapsed || state.isFiltering), + CategoriesRow2(isColapsed: _isColapsed || state.isFiltering), + if (!state.isFiltering) CategoriesList(isColapsed: _isColapsed), + ], + ), ), ); } + void _onChanged(String value) { + context.read().resetFilters(); + if (value.length < 4 && value.isNotEmpty) return; + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 2), () { + context.read().getRadarOverviews( + page: 1, + search: value, + ); + }); + } + void _handleAnimations() async { if (_isAnimating) return; final double position = _scrollController.position.pixels; @@ -106,4 +163,144 @@ class _RadarState extends State { setState(() {}); } } + + Future _showFilterBottomSheet() async { + final state = context.read(); + await ActionSheetUtils.showBottomSheet( + data: ActionSheetData( + title: 'فیلتر جستجو', + titleIcon: DidvanIcons.filter_regular, + hasDismissButton: false, + confrimTitle: 'نمایش نتایج', + onConfirmed: () { + Navigator.of(context).pop(); + state.getRadarOverviews(page: 1, filter: true); + }, + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ItemTitle( + title: 'تاریخ رادار', + style: Theme.of(context).textTheme.bodyText2, + icon: DidvanIcons.calendar_range_regular, + ), + const SizedBox(height: 8), + StatefulBuilder( + builder: (context, setState) => Row( + children: [ + DatePickerButton( + initialValue: state.startDate, + emptyText: 'از تاریخ', + onPicked: (date) => setState(() => state.startDate = date), + lastDate: state.endDate, + ), + const SizedBox(width: 8), + DatePickerButton( + initialValue: state.endDate, + emptyText: 'تا تاریخ', + onPicked: (date) => setState(() => state.endDate = date), + firstDate: state.startDate, + ), + ], + ), + ), + const SizedBox(height: 28), + ItemTitle( + title: 'دسته بندی', + icon: DidvanIcons.radar_regular, + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: 12), + Wrap( + children: [ + for (var i = 0; i < state.categories.length; i++) + SizedBox( + width: (MediaQuery.of(context).size.width - 40) / 2, + child: DidvanCheckbox( + title: state.categories[i].title, + value: state.selectedCats.contains(state.categories[i]), + onChanged: (value) { + if (value) { + state.selectedCats.add(state.categories[i]); + return; + } + state.selectedCats.remove(state.categories[i]); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _RadarItemPlaceholder extends StatelessWidget { + const _RadarItemPlaceholder({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder( + width: 200, + height: 16, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder(height: 140), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder( + height: 12, + width: 70, + ), + ShimmerPlaceholder( + height: 12, + width: 70, + ), + ], + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + ), + const DidvanDivider(), + Row( + children: const [ + ShimmerPlaceholder( + height: 32, + width: 32, + ), + Spacer(), + ShimmerPlaceholder( + height: 32, + width: 32, + ), + SizedBox(width: 16), + ShimmerPlaceholder( + height: 32, + width: 32, + ), + ], + ), + ], + ), + ); + } } diff --git a/lib/pages/home/radar/radar_state.dart b/lib/pages/home/radar/radar_state.dart index 1b84299..1c2806b 100644 --- a/lib/pages/home/radar/radar_state.dart +++ b/lib/pages/home/radar/radar_state.dart @@ -1,12 +1,68 @@ import 'package:didvan/constants/assets.dart'; +import 'package:didvan/models/enums.dart'; import 'package:didvan/models/radar_category.dart'; +import 'package:didvan/models/radar_overview/radar_overview.dart'; import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; class RadarState extends CoreProvier { + bool isFiltering = false; + String? _lastSearch; + String? startDate; + String? endDate; + final List selectedCats = []; + + final List radars = []; + + void resetFilters() { + startDate = null; + endDate = null; + selectedCats.clear(); + } + + Future getRadarOverviews({ + required int page, + String? search, + bool filter = false, + }) async { + if (search != null && search.isNotEmpty) { + if (_lastSearch == search && !filter) { + return; + } + isFiltering = true; + _lastSearch = search; + } else { + isFiltering = false; + } + if (filter) { + isFiltering = true; + } + appState = AppState.busy; + final RequestService service = RequestService( + RequestHelper.getRadarOverviews( + page: page, + startDate: startDate?.split(' ').first, + endDate: endDate?.split(' ').first, + search: search, + categories: selectedCats.map((e) => e.id).toList(), + ), + ); + await service.httpGet(); + if (service.isSuccess) { + for (var i = 0; i < service.result['radars'].length; i++) { + radars.add(RadarOverview.fromJson(service.result['radars'][i])); + } + appState = AppState.idle; + return; + } + appState = AppState.failed; + } + final List categories = [ RadarCategory( id: 1, - title: 'افتصادی', + title: 'اقتصادی', asset: Assets.economicCategoryIcon, ), RadarCategory( diff --git a/lib/pages/home/radar/widgets/radar_item.dart b/lib/pages/home/radar/widgets/radar_item.dart index b31a91b..0c6a02c 100644 --- a/lib/pages/home/radar/widgets/radar_item.dart +++ b/lib/pages/home/radar/widgets/radar_item.dart @@ -1,6 +1,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/radar_overview/radar_overview.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/widgets/didvan/card.dart'; import 'package:didvan/widgets/didvan/divider.dart'; @@ -9,79 +10,77 @@ import 'package:didvan/widgets/skeletun_image.dart'; import 'package:flutter/material.dart'; class RadarItem extends StatelessWidget { - const RadarItem({Key? key}) : super(key: key); + final RadarOverview? radar; + const RadarItem({Key? key, required this.radar}) : super(key: key); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: DidvanCard( - onTap: () => Navigator.of(context).pushNamed(Routes.radarDetails), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, - borderRadius: DesignConfig.highBorderRadius, - ), - child: DidvanText( - 'برای مدیران', + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed(Routes.radarDetails), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: DesignConfig.highBorderRadius, + ), + child: DidvanText( + 'برای مدیران', + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.white, + ), + ), + const SizedBox(height: 8), + const DidvanText( + 'نقش مهم فولاد در اقتصاد جهانی', + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 8), + const SkeletonImage( + imageUrl: 'https://wallpapercave.com/wp/wp9373116.jpg', + width: double.infinity, + height: 140, + ), + const SizedBox(height: 8), + Row( + children: [ + DidvanText( + 'رادار کسب و کار', style: Theme.of(context).textTheme.overline, - color: Theme.of(context).colorScheme.white, + color: Theme.of(context).colorScheme.caption, ), - ), - const SizedBox(height: 8), - const DidvanText( - 'نقش مهم فولاد در اقتصاد جهانی', - fontWeight: FontWeight.w600, - ), - const SizedBox(height: 8), - const SkeletonImage( - imageUrl: 'https://wallpapercave.com/wp/wp9373116.jpg', - width: double.infinity, - height: 140, - ), - const SizedBox(height: 8), - Row( - children: [ - DidvanText( - 'رادار کسب و کار', - style: Theme.of(context).textTheme.overline, - color: Theme.of(context).colorScheme.caption, - ), - const Spacer(), - DidvanText( - 'هفته پیش | خواندن 5 دقیقه', - style: Theme.of(context).textTheme.overline, - color: Theme.of(context).colorScheme.caption, - ), - ], - ), - const SizedBox(height: 8), - const DidvanText( - 'صنعت فولاد جوادی مجد سلیمی است پس باید به آن توجه زیادی شود تا بازار به انفجار نرسد. پس جواد مهربانگو باشیم...', - maxLines: 3, - ), - const DidvanDivider(), - Row( - children: const [ - Icon( - DidvanIcons.bookmark_regular, - ), - Spacer(), - DidvanText('2'), - SizedBox(width: 4), - Icon(DidvanIcons.chats_regular), - SizedBox(width: 16), - DidvanText('10'), - SizedBox(width: 4), - Icon(DidvanIcons.evaluation_regular), - ], - ), - ], - ), + const Spacer(), + DidvanText( + 'هفته پیش | خواندن 5 دقیقه', + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + const SizedBox(height: 8), + const DidvanText( + 'صنعت فولاد جوادی مجد سلیمی است پس باید به آن توجه زیادی شود تا بازار به انفجار نرسد. پس جواد مهربانگو باشیم...', + maxLines: 3, + ), + const DidvanDivider(), + Row( + children: const [ + Icon( + DidvanIcons.bookmark_regular, + ), + Spacer(), + DidvanText('2'), + SizedBox(width: 4), + Icon(DidvanIcons.chats_regular), + SizedBox(width: 16), + DidvanText('10'), + SizedBox(width: 4), + Icon(DidvanIcons.evaluation_regular), + ], + ), + ], ), ); } diff --git a/lib/pages/home/radar/widgets/search_field.dart b/lib/pages/home/radar/widgets/search_field.dart index 85602a8..719a3f2 100644 --- a/lib/pages/home/radar/widgets/search_field.dart +++ b/lib/pages/home/radar/widgets/search_field.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; class SearchField extends StatefulWidget { final String title; - final void Function(String? value) onChanged; + final void Function(String value) onChanged; const SearchField({Key? key, required this.title, required this.onChanged}) : super(key: key); @@ -47,8 +47,9 @@ class _SearchFieldState extends State { color: Theme.of(context).colorScheme.primary, ), ), - prefixIcon: const Icon( + prefixIcon: Icon( DidvanIcons.search_regular, + color: Theme.of(context).colorScheme.text, ), prefixIconColor: Theme.of(context).colorScheme.inputText, enabledBorder: OutlineInputBorder( diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index 48be3b6..c0d37fc 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -6,20 +6,31 @@ class RequestHelper { static String getRadarOverviews({ required int page, - int? radarId, + List categories = const [], String? startDate, String? endDate, String? search, - }) => - _baseUrl + - '/radar' + - _urlConcatGenerator([ - MapEntry('page', page.toString()), - MapEntry('start', startDate), - MapEntry('end', endDate), - MapEntry('search', search), - MapEntry('radar', radarId?.toString()), - ]); + }) { + String? cats; + if (categories.isNotEmpty) { + cats = ''; + for (var i = 0; i < categories.length; i++) { + cats = cats! + categories[i].toString(); + if (i != categories.length - 1) { + cats += ','; + } + } + } + return _baseUrl + + '/radar' + + _urlConcatGenerator([ + MapEntry('page', page.toString()), + MapEntry('start', startDate), + MapEntry('end', endDate), + MapEntry('search', search), + MapEntry('categories', cats), + ]); + } static String _urlConcatGenerator(List> additions) { String result = ''; @@ -27,8 +38,8 @@ class RequestHelper { if (additions.isNotEmpty) { result += '?'; for (var i = 0; i < additions.length; i++) { - result += (additions[i].key + additions[i].value!); - if (i != additions.length) { + result += (additions[i].key + '=' + additions[i].value!); + if (i != additions.length - 1) { result += '&'; } } diff --git a/lib/utils/action_sheet.dart b/lib/utils/action_sheet.dart index a44f4a0..a05f319 100644 --- a/lib/utils/action_sheet.dart +++ b/lib/utils/action_sheet.dart @@ -117,7 +117,7 @@ class ActionSheetUtils { Expanded( child: DidvanButton( style: ButtonStyleMode.primary, - onPressed: () {}, + onPressed: data.onConfirmed, title: data.confrimTitle ?? 'تایید', ), ), diff --git a/lib/utils/date_time.dart b/lib/utils/date_time.dart index c603d1a..f49296f 100644 --- a/lib/utils/date_time.dart +++ b/lib/utils/date_time.dart @@ -1,4 +1,6 @@ +import 'package:didvan/config/design_config.dart'; import 'package:flutter/material.dart'; +import 'package:persian_datetime_picker/persian_datetime_picker.dart'; class DateTimeUtils { static TimeOfDay stringToTimeOfDay(String input) => TimeOfDay( @@ -22,4 +24,23 @@ class DateTimeUtils { } return '$minute:$second'; } + + static Future showDatePicker( + {String? initialDate, String? startDate, String? endDate}) async { + final initDate = initialDate == null ? null : DateTime.parse(initialDate); + final initialJalali = Jalali.fromDateTime(initDate ?? DateTime.now()); + final firstDate = Jalali.fromDateTime( + startDate == null ? DateTime(2021) : DateTime.parse(startDate), + ); + final lastDate = Jalali.fromDateTime( + endDate == null ? DateTime.now() : DateTime.parse(endDate), + ); + final Jalali? result = await showPersianDatePicker( + context: DesignConfig.context, + initialDate: initialJalali, + firstDate: firstDate, + lastDate: lastDate, + ); + return result?.toDateTime().toString(); + } } diff --git a/pubspec.lock b/pubspec.lock index 9d580f9..e678844 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -441,6 +441,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.7.0" + persian_datetime_picker: + dependency: "direct main" + description: + name: persian_datetime_picker + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" + persian_number_utility: + dependency: "direct main" + description: + name: persian_number_utility + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 636d094..e588d03 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,8 @@ dependencies: record_web: ^0.2.1 just_waveform: ^0.0.1 another_flushbar: ^1.10.28 + persian_datetime_picker: ^2.4.0 + persian_number_utility: ^1.1.1 dev_dependencies: flutter_test: