import 'dart:async'; import 'dart:math'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/category.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/views/radar/radar_state.dart'; import 'package:didvan/views/widgets/date_picker_button.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/widgets/overview/radar.dart'; import 'package:didvan/views/widgets/didvan/checkbox.dart'; import 'package:didvan/views/widgets/item_title.dart'; import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:didvan/views/widgets/home_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class Radar extends StatefulWidget { const Radar({Key? key}) : super(key: key); @override State createState() => _RadarStateView(); } class _RadarStateView extends State { Timer? _timer; final _focusNode = FocusNode(); @override void initState() { context.read().init(); super.initState(); } String _getCategoryIcon(int categoryId, bool isDark) { switch (categoryId) { case 1: return 'lib/assets/icons/New_Economic.svg'; case 2: return 'lib/assets/icons/New_Siasi.svg'; case 3: return 'lib/assets/icons/New_Fanavari.svg'; case 4: return 'lib/assets/icons/New_Kasbokar.svg'; case 5: return 'lib/assets/icons/New_ZistMohit.svg'; case 6: return 'lib/assets/icons/New_Ejtemaei.svg'; default: return 'lib/assets/icons/Kasb o Kar.svg'; } } @override Widget build(BuildContext context) { final state = context.watch(); final theme = Theme.of(context); final colorScheme = theme.colorScheme; final isDark = theme.brightness == Brightness.dark; final showCategories = !state.filtering && !state.searching; return DidvanScaffold( padding: EdgeInsets.zero, appBarData: null, slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(top: 12.0), child: HomeAppBar( showBackButton: false, showSearchField: state.appState != AppState.failed, onSearchChanged: _onChanged, onFilterPressed: _showFilterBottomSheet, searchFocusNode: _focusNode, isFiltered: state.filtering, searchValue: state.search, ), ), ), SliverAppBar( pinned: true, automaticallyImplyLeading: false, backgroundColor: colorScheme.surface, elevation: 0, centerTitle: false, titleSpacing: 16, title: Text( 'پویش‌ افق', style: theme.textTheme.headlineSmall?.copyWith( color: isDark ? Colors.white : const Color.fromARGB(255, 0, 53, 70), fontWeight: FontWeight.bold, fontSize: 19), ), actions: [ IconButton( onPressed: () => Navigator.of(context).pop(), icon: SvgPicture.asset( 'lib/assets/icons/arrow-left.svg', width: 30, height: 30, colorFilter: ColorFilter.mode( isDark ? Colors.white70 : Theme.of(context).colorScheme.caption, BlendMode.srcIn), ), ), const SizedBox(width: 8), ], ), const SliverToBoxAdapter( child: SizedBox( height: 16, ), ), SliverToBoxAdapter( child: AnimatedSwitcher( duration: const Duration(milliseconds: 500), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeInCubic, transitionBuilder: (Widget child, Animation animation) { return SizeTransition( sizeFactor: animation, axisAlignment: -1.0, child: FadeTransition( opacity: animation, child: child, ), ); }, child: showCategories ? Column( key: const ValueKey('categories_column'), children: [ if (state.categories.length >= 3) Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: state.categories .sublist(0, 3) .map((category) => _buildCoolCategoryItem( category, isDark, state, theme)) .toList(), ), ), if (state.categories.length >= 6) Padding( padding: const EdgeInsets.only( top: 12, left: 35, right: 35), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: state.categories .sublist(3, 6) .map((category) => _buildCoolCategoryItem( category, isDark, state, theme)) .toList(), ), ), const SizedBox(height: 20), ], ) : const SizedBox.shrink(key: ValueKey('empty_categories')), ), ), SliverStateHandler( centerEmptyState: false, onRetry: () => state.getRadars(page: state.page), state: state, childCount: min(state.visibleCount, state.radars.length) + (_hasMoreItems(state) ? 1 : 0), builder: (context, state, index) { final currentDisplayCount = min(state.visibleCount, state.radars.length); if (index == currentDisplayCount && _hasMoreItems(state)) { return _buildLoadMoreButton(context, state); } if (index >= state.radars.length) { return _SlideFadeTransition( index: index, child: RadarOverview.placeholder); } final radar = state.radars[index]; return _SlideFadeTransition( index: index, child: RadarOverview( radar: radar, onMarkChanged: state.changeMark, onLikedChanged: state.changeLike, onCommentsChanged: (id, count) => state.onCommentsChanged(id, count), radarRequestArgs: RadarRequestArgs( page: state.page, categories: List.from(state.selectedCats.map((cat) => cat.id)), endDate: state.endDate, isSingleItem: false, search: state.search, startDate: state.startDate, ), ), ); }, enableEmptyState: state.radars.isEmpty && state.appState != AppState.busy, emptyState: EmptyResult( onNewSearch: () => _focusNode.requestFocus(), ), itemPadding: const EdgeInsets.only( bottom: 20, left: 16, right: 16, ), placeholder: _SlideFadeTransition(index: 0, child: RadarOverview.placeholder), ), ], ); } bool _hasMoreItems(RadarState state) { return state.radars.length > state.visibleCount || state.page < state.lastPage; } Widget _buildLoadMoreButton(BuildContext context, RadarState state) { if (state.appState == AppState.busy && state.radars.length <= state.visibleCount) { return const Padding( padding: EdgeInsets.all(16.0), child: Center(child: CircularProgressIndicator()), ); } return Padding( padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0), child: Center( child: GestureDetector( onTap: () { state.loadMore(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 50.0, vertical: 12.0, ), decoration: BoxDecoration( color: const Color.fromARGB(255, 0, 126, 167), borderRadius: BorderRadius.circular(15), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( 'lib/assets/icons/element-plus.svg', colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), width: 20, ), const SizedBox(width: 8), const Text( 'بارگذاری بیشتر', style: TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold, ), ), ], ), ), ), ), ); } Widget _buildCoolCategoryItem( CategoryData category, bool isDark, RadarState state, ThemeData theme) { final isSelected = state.selectedCats.any((element) => element.id == category.id); category.asset = _getCategoryIcon(category.id, isDark); return Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: GestureDetector( onTap: () => _onCategorySelected(category), child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.fastOutSlowIn, padding: EdgeInsets.zero, decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primary.withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(30), border: Border.all( color: isSelected ? theme.colorScheme.primary : Colors.transparent, width: 2, ), ), child: ClipRRect( borderRadius: BorderRadius.circular(30), child: SvgPicture.asset( category.asset!, width: 100, height: 100, ), ), ), const SizedBox(height: 3), Text( category.label, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, fontWeight: isSelected ? FontWeight.bold : FontWeight.bold, fontSize: 13, ), ), ], ), ), ), ); } void _onChanged(String value) { final state = context.read(); if (value.length < 3 && value.isNotEmpty || state.lastSearch == value) { return; } _timer?.cancel(); _timer = Timer(const Duration(seconds: 1), () { state.search = value; state.getRadars(page: 1); }); } void _onCategorySelected(CategoryData category) { final state = context.read(); final isAlreadySelected = state.selectedCats.any((cat) => cat.id == category.id); state.selectedCats.clear(); if (!isAlreadySelected && category.id != 0) { state.selectedCats.add(category); } state.getRadars(page: 1); } Future _showFilterBottomSheet() async { final state = context.read(); await ActionSheetUtils(context).showBottomSheet( data: ActionSheetData( title: 'فیلتر جستجو', smallDismissButton: true, titleIcon: DidvanIcons.filter_regular, dismissTitle: 'حذف فیلتر', confrimTitle: 'نمایش نتایج', onDismissed: () => state.resetFilters(false), onConfirmed: () => state.getRadars(page: 1), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ItemTitle( title: 'تاریخ ایجاد', style: Theme.of(context).textTheme.bodyMedium, 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.category_regular, style: Theme.of(context).textTheme.bodyMedium, ), 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].label, value: state.selectedCats .any((cat) => cat.id == state.categories[i].id), onChanged: (value) { if (value) { state.selectedCats.add(state.categories[i]); } else { state.selectedCats.removeWhere( (cat) => cat.id == state.categories[i].id); } }, ), ), ], ), ], ), ), ); } @override void dispose() { _focusNode.dispose(); super.dispose(); } } class _SlideFadeTransition extends StatefulWidget { final Widget child; final int index; const _SlideFadeTransition({ Key? key, required this.child, required this.index, }) : super(key: key); @override State<_SlideFadeTransition> createState() => _SlideFadeTransitionState(); } class _SlideFadeTransitionState extends State<_SlideFadeTransition> with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _opacity; late final Animation _offset; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _opacity = CurvedAnimation(parent: _controller, curve: Curves.easeOut); _offset = Tween(begin: const Offset(0, 0.1), end: Offset.zero) .animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); Future.delayed(Duration(milliseconds: 50 * (widget.index % 5)), () { if (mounted) { _controller.forward(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return FadeTransition( opacity: _opacity, child: SlideTransition( position: _offset, child: widget.child, ), ); } }