import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; import '../../../../app/responsive.dart'; import '../../../../backend/backend.dart'; import '../../../../shared/widgets/app_search_bar.dart'; class FeedToolbar extends StatefulWidget { const FeedToolbar({ required this.onSearch, required this.onRefresh, required this.onProviderFilter, required this.onRatingFilter, required this.onClearFilters, required this.providers, required this.selectedTags, required this.selectedProviderIds, required this.topPeriodFilter, required this.tagSuggestions, required this.rating, required this.onQuickProviderToggle, required this.onSearchChanged, required this.onSuggestionTap, required this.onTopPeriodChanged, this.onToggleSelectionMode, this.onRandom, this.providerStatusMessage, this.selectionMode = false, super.key, }); final ValueChanged onSearch; final VoidCallback onRefresh; final VoidCallback onProviderFilter; final VoidCallback onRatingFilter; final VoidCallback onClearFilters; final List providers; final List selectedTags; final List selectedProviderIds; final TopPeriodFilter topPeriodFilter; final List tagSuggestions; final String? rating; final ValueChanged onQuickProviderToggle; final ValueChanged onSearchChanged; final ValueChanged onSuggestionTap; final ValueChanged onTopPeriodChanged; final VoidCallback? onToggleSelectionMode; final VoidCallback? onRandom; final String? providerStatusMessage; final bool selectionMode; @override State createState() => _FeedToolbarState(); } class _FeedToolbarState extends State { late String _query; @override void initState() { super.initState(); _query = widget.selectedTags.join(' '); } @override void didUpdateWidget(covariant FeedToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.selectedTags != widget.selectedTags) { _query = widget.selectedTags.join(' '); } } @override Widget build(BuildContext context) { if (Responsive.isMobile(context)) return _buildMobile(context); return _buildDesktop(context); } Widget _buildDesktop(BuildContext context) { final hasActiveFilters = widget.selectedProviderIds.isNotEmpty || widget.rating != null; return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: Column( children: [ Row( children: [ Expanded( child: TagInputSearchBar( initialValue: _query, suggestions: widget.tagSuggestions, onChanged: (value) { _query = value; widget.onSearchChanged(value); }, onSubmitted: widget.onSearch, onSuggestionApplied: (value) { _query = value; widget.onSuggestionTap(value); }, onTagRemoved: (value) { _query = value; widget.onSearch(value); }, onCleared: () { _query = ''; widget.onSearch(''); }, ), ), const SizedBox(width: 8), IconButton.filledTonal( tooltip: widget.selectionMode ? 'Выйти из выбора (Esc)' : 'Выбрать посты (V)', onPressed: widget.onToggleSelectionMode, icon: Icon(widget.selectionMode ? Icons.check_box_rounded : Icons.check_box_outline_blank_rounded), ), const SizedBox(width: 4), Badge( isLabelVisible: hasActiveFilters, child: IconButton.filledTonal( tooltip: 'Провайдеры', onPressed: widget.onProviderFilter, icon: const Icon(Icons.hub_rounded), ), ), const SizedBox(width: 4), IconButton.filledTonal( tooltip: 'Рейтинг', onPressed: widget.onRatingFilter, icon: const Icon(Icons.tune_rounded), ), const SizedBox(width: 4), IconButton( tooltip: 'Очистить фильтры', onPressed: widget.onClearFilters, icon: const Icon(Icons.filter_alt_off_rounded), ), const SizedBox(width: 4), IconButton( tooltip: 'Обновить (Ctrl+R / F5)', onPressed: widget.onRefresh, icon: const Icon(Icons.refresh_rounded), ), const SizedBox(width: 4), IconButton.filledTonal( tooltip: 'Случайный пост (R)', onPressed: widget.onRandom, icon: const Icon(Icons.casino_rounded), ), ], ), const SizedBox(height: 8), if (widget.providerStatusMessage != null) ...[ Align( alignment: Alignment.centerLeft, child: InputChip( avatar: const Icon(Icons.sync_problem_rounded, size: 16), label: Text(widget.providerStatusMessage!), ), ), const SizedBox(height: 8), ], _HorizontalWheelScroller( height: 38, builder: (controller) => ListView( controller: controller, scrollDirection: Axis.horizontal, primary: false, children: [ _LiquidPeriodTabs( selectedPeriod: widget.topPeriodFilter, onChanged: widget.onTopPeriodChanged, ), const SizedBox(width: 8), const _ToolbarDivider(), const SizedBox(width: 8), if (widget.providers.isNotEmpty) ...[ _LiquidProviderPill( label: 'Все источники', isSelected: widget.selectedProviderIds.isEmpty, icon: Icons.all_inclusive_rounded, onTap: () => widget.onQuickProviderToggle('__all__'), ), const SizedBox(width: 6), for (final provider in widget.providers) ...[ _LiquidProviderPill( label: provider.name, isSelected: widget.selectedProviderIds.contains(provider.id), icon: Icons.hub_rounded, onTap: () => widget.onQuickProviderToggle(provider.id), ), const SizedBox(width: 6), ], ], ], ), ), ], ), ); } Widget _buildMobile(BuildContext context) { final hasActiveFilters = widget.selectedProviderIds.isNotEmpty || widget.rating != null; return Padding( padding: const EdgeInsets.fromLTRB(10, 8, 10, 6), child: Column( children: [ Row( children: [ Expanded( child: TagInputSearchBar( initialValue: _query, suggestions: widget.tagSuggestions, onChanged: (value) { _query = value; widget.onSearchChanged(value); }, onSubmitted: widget.onSearch, onSuggestionApplied: (value) { _query = value; widget.onSuggestionTap(value); }, onTagRemoved: (value) { _query = value; widget.onSearch(value); }, onCleared: () { _query = ''; widget.onSearch(''); }, ), ), const SizedBox(width: 6), Badge( isLabelVisible: hasActiveFilters, child: IconButton.filledTonal( visualDensity: VisualDensity.compact, tooltip: 'Фильтры', onPressed: widget.onProviderFilter, icon: const Icon(Icons.tune_rounded, size: 20), ), ), const SizedBox(width: 4), IconButton.filledTonal( visualDensity: VisualDensity.compact, tooltip: 'Случайный пост', onPressed: widget.onRandom, icon: const Icon(Icons.casino_rounded, size: 20), ), const SizedBox(width: 4), IconButton.filledTonal( visualDensity: VisualDensity.compact, tooltip: widget.selectionMode ? 'Выйти из выбора' : 'Выбрать посты', onPressed: widget.onToggleSelectionMode, icon: Icon( widget.selectionMode ? Icons.check_box_rounded : Icons.check_box_outline_blank_rounded, size: 20, ), ), ], ), if (widget.providerStatusMessage != null) ...[ const SizedBox(height: 6), Align( alignment: Alignment.centerLeft, child: InputChip( avatar: const Icon(Icons.sync_problem_rounded, size: 16), label: Text(widget.providerStatusMessage!), ), ), ], const SizedBox(height: 7), _HorizontalWheelScroller( height: 38, builder: (controller) => ListView( controller: controller, scrollDirection: Axis.horizontal, primary: false, children: [ if (hasActiveFilters || widget.selectedTags.isNotEmpty) ...[ _LiquidProviderPill( label: 'Сбросить', isSelected: false, icon: Icons.filter_alt_off_rounded, onTap: widget.onClearFilters, ), const SizedBox(width: 6), ], _LiquidProviderPill( label: 'Обновить', isSelected: false, icon: Icons.refresh_rounded, onTap: widget.onRefresh, ), const SizedBox(width: 6), const _ToolbarDivider(), const SizedBox(width: 6), _LiquidProviderPill( label: widget.rating ?? 'Рейтинг', isSelected: widget.rating != null, icon: Icons.shield_outlined, onTap: widget.onRatingFilter, ), const SizedBox(width: 6), const _ToolbarDivider(), const SizedBox(width: 6), _LiquidPeriodTabs( selectedPeriod: widget.topPeriodFilter, onChanged: widget.onTopPeriodChanged, ), if (widget.providers.isNotEmpty) ...[ const SizedBox(width: 6), const _ToolbarDivider(), const SizedBox(width: 6), _LiquidProviderPill( label: 'Все', isSelected: widget.selectedProviderIds.isEmpty, icon: Icons.all_inclusive_rounded, onTap: () => widget.onQuickProviderToggle('__all__'), ), const SizedBox(width: 6), for (final provider in widget.providers) ...[ _LiquidProviderPill( label: provider.name, isSelected: widget.selectedProviderIds.contains(provider.id), icon: Icons.hub_rounded, onTap: () => widget.onQuickProviderToggle(provider.id), ), const SizedBox(width: 6), ], ], ], ), ), ], ), ); } } class _ToolbarDivider extends StatelessWidget { const _ToolbarDivider(); @override Widget build(BuildContext context) { return Container( width: 1, height: 20, margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 8), color: Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.4), ); } } class _LiquidPeriodTabs extends StatelessWidget { const _LiquidPeriodTabs({ required this.selectedPeriod, required this.onChanged, }); final TopPeriodFilter selectedPeriod; final ValueChanged onChanged; IconData _iconFor(TopPeriodFilter period) { return switch (period) { TopPeriodFilter.none => Icons.auto_awesome_rounded, TopPeriodFilter.month => Icons.calendar_month_rounded, TopPeriodFilter.year => Icons.calendar_today_rounded, TopPeriodFilter.allTime => Icons.workspace_premium_rounded, }; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final isDark = theme.brightness == Brightness.dark; return Container( padding: const EdgeInsets.all(2.5), decoration: BoxDecoration( color: isDark ? scheme.surfaceContainerHigh.withValues(alpha: 0.55) : scheme.surfaceContainerHighest.withValues(alpha: 0.45), borderRadius: BorderRadius.circular(16), border: Border.all( color: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.06), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ for (final period in TopPeriodFilter.values) ...[ _LiquidPeriodTabItem( period: period, icon: _iconFor(period), isSelected: selectedPeriod == period, onTap: () => onChanged(period), ), if (period != TopPeriodFilter.values.last) const SizedBox(width: 3), ], ], ), ); } } class _LiquidPeriodTabItem extends StatelessWidget { const _LiquidPeriodTabItem({ required this.period, required this.icon, required this.isSelected, required this.onTap, }); final TopPeriodFilter period; final IconData icon; final bool isSelected; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final isDark = theme.brightness == Brightness.dark; return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(13), child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOutCubic, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: isSelected ? (isDark ? scheme.primary.withValues(alpha: 0.28) : scheme.primaryContainer.withValues(alpha: 0.85)) : Colors.transparent, borderRadius: BorderRadius.circular(13), border: isSelected ? Border.all( color: scheme.primary.withValues(alpha: isDark ? 0.45 : 0.3), width: 1, ) : null, boxShadow: isSelected ? [ BoxShadow( color: scheme.primary.withValues(alpha: isDark ? 0.25 : 0.12), blurRadius: 8, offset: const Offset(0, 2), ), ] : null, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 14, color: isSelected ? (isDark ? scheme.primary : scheme.onPrimaryContainer) : scheme.onSurfaceVariant.withValues(alpha: 0.85), ), const SizedBox(width: 6), Text( period.label, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, color: isSelected ? (isDark ? scheme.primary : scheme.onPrimaryContainer) : scheme.onSurfaceVariant, ), ), ], ), ), ), ); } } class _LiquidProviderPill extends StatelessWidget { const _LiquidProviderPill({ required this.label, required this.isSelected, required this.onTap, this.icon, }); final String label; final bool isSelected; final VoidCallback onTap; final IconData? icon; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final isDark = theme.brightness == Brightness.dark; return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(14), child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOutCubic, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: isSelected ? (isDark ? scheme.secondaryContainer.withValues(alpha: 0.35) : scheme.secondaryContainer.withValues(alpha: 0.7)) : (isDark ? scheme.surfaceContainerHigh.withValues(alpha: 0.4) : scheme.surfaceContainerHighest.withValues(alpha: 0.35)), borderRadius: BorderRadius.circular(14), border: Border.all( color: isSelected ? scheme.secondary.withValues(alpha: isDark ? 0.6 : 0.4) : (isDark ? Colors.white.withValues(alpha: 0.08) : Colors.black.withValues(alpha: 0.05)), width: 1, ), boxShadow: isSelected ? [ BoxShadow( color: scheme.secondary.withValues(alpha: isDark ? 0.2 : 0.08), blurRadius: 6, ), ] : null, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon( icon, size: 14, color: isSelected ? (isDark ? scheme.secondary : scheme.onSecondaryContainer) : scheme.onSurfaceVariant.withValues(alpha: 0.8), ), const SizedBox(width: 5), ], Text( label, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, color: isSelected ? (isDark ? scheme.secondary : scheme.onSecondaryContainer) : scheme.onSurfaceVariant, ), ), ], ), ), ), ); } } class _HorizontalWheelScroller extends StatefulWidget { const _HorizontalWheelScroller({ required this.builder, required this.height, }); final Widget Function(ScrollController controller) builder; final double height; @override State<_HorizontalWheelScroller> createState() => _HorizontalWheelScrollerState(); } class _HorizontalWheelScrollerState extends State<_HorizontalWheelScroller> { final _controller = ScrollController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( height: widget.height, child: Listener( onPointerSignal: (event) { if (event is! PointerScrollEvent || !_controller.hasClients) return; final target = (_controller.offset + event.scrollDelta.dy) .clamp(0.0, _controller.position.maxScrollExtent) .toDouble(); _controller.jumpTo(target); }, child: Scrollbar( controller: _controller, thumbVisibility: false, child: widget.builder(_controller), ), ), ); } } Future?> showProviderFilterSheet( BuildContext context, { required List providers, required List selectedIds, }) { final selected = selectedIds.toSet(); return showModalBottomSheet>( context: context, showDragHandle: true, builder: (context) => StatefulBuilder( builder: (context, setState) => ListView( padding: const EdgeInsets.all(16), children: [ Text('Providers', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 8), for (final provider in providers) CheckboxListTile( value: selected.contains(provider.id), title: Text(provider.name), subtitle: Text(provider.baseUrl), onChanged: (value) { setState(() { if (value ?? false) { selected.add(provider.id); } else { selected.remove(provider.id); } }); }, ), const SizedBox(height: 12), FilledButton( onPressed: () => Navigator.pop(context, selected.toList()), child: const Text('Apply'), ), ], ), ), ); } Future showRatingFilterSheet(BuildContext context, String? selected) { return showModalBottomSheet( context: context, showDragHandle: true, builder: (context) => ListView( padding: const EdgeInsets.all(16), children: [ Text('Rating', style: Theme.of(context).textTheme.titleLarge), ListTile( leading: Icon( selected == null ? Icons.radio_button_checked_rounded : Icons.radio_button_off_rounded, ), title: const Text('Any'), onTap: () => Navigator.pop(context, null), ), for (final rating in ['safe', 'questionable', 'explicit']) ListTile( leading: Icon( selected == rating ? Icons.radio_button_checked_rounded : Icons.radio_button_off_rounded, ), title: Text(rating), onTap: () => Navigator.pop(context, rating), ), ], ), ); }