From 060a97f5ec2c75a137d84d011221833b24850292 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 8 Mar 2025 18:19:57 +0800 Subject: [PATCH] :recycle: Refactored explore screen --- lib/screens/explore.dart | 442 ++++++++++++++------------------------- 1 file changed, 157 insertions(+), 285 deletions(-) diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 84655cd..99c60cd 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -1,4 +1,3 @@ -import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; @@ -12,13 +11,15 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_realm.dart'; import 'package:surface/types/post.dart'; import 'package:surface/types/realm.dart'; -import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; +const kPostChannels = ['Global', 'Friends', 'Following']; +const kPostChannelIcons = [Symbols.globe, Symbols.group, Symbols.subscriptions]; + const Map kCategoryIcons = { 'technology': Symbols.tools_wrench, 'gaming': Symbols.gamepad, @@ -39,17 +40,14 @@ class ExploreScreen extends StatefulWidget { State createState() => _ExploreScreenState(); } -// You know what? I'm not going to make this a global variable. -// Cuz the global key make the selected category not update to child widget when the category is changed. -SnPostCategory? _selectedCategory; - class _ExploreScreenState extends State - with SingleTickerProviderStateMixin { - late final TabController _tabController = - TabController(length: 4, vsync: this); + with TickerProviderStateMixin { + late TabController _tabController = TabController(length: 3, vsync: this); final _fabKey = GlobalKey(); - final _listKeys = List.generate(4, (_) => GlobalKey<_PostListWidgetState>()); + final _listKeys = GlobalKey<_PostListWidgetState>(); + + bool _showCategories = false; final List _categories = List.empty(growable: true); @@ -69,14 +67,65 @@ class _ExploreScreenState extends State } } - void _clearFilter() { - _selectedCategory = null; + final List _realms = List.empty(growable: true); + + Future _fetchRealms() async { + try { + final rels = context.read(); + final out = await rels.listAvailableRealms(); + setState(() { + _realms.addAll(out); + }); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + rethrow; + } + } + + void _toggleShowCategories() { + _showCategories = !_showCategories; + if (_showCategories) { + _tabController = TabController(length: _categories.length, vsync: this); + } else { + _tabController = TabController(length: 4, vsync: this); + } + _tabListen(); + setState(() {}); + } + + void _tabListen() { + _tabController.addListener(() { + if (_tabController.indexIsChanging) { + if (_showCategories) { + _listKeys.currentState + ?.setCategory(_categories[_tabController.index]); + _listKeys.currentState?.refreshPosts(); + return; + } + switch (_tabController.index) { + case 0: + case 3: + _listKeys.currentState?.setChannel(null); + break; + case 1: + _listKeys.currentState?.setChannel('friends'); + break; + case 2: + _listKeys.currentState?.setChannel('following'); + break; + } + _listKeys.currentState?.refreshPosts(); + } + }); } @override void initState() { - _fetchCategories(); super.initState(); + _tabListen(); + _fetchCategories(); + _fetchRealms(); } @override @@ -86,7 +135,7 @@ class _ExploreScreenState extends State } Future refreshPosts() async { - await _listKeys[_tabController.index].currentState?.refreshPosts(); + await _listKeys.currentState?.refreshPosts(); } @override @@ -155,19 +204,18 @@ class _ExploreScreenState extends State actions: [ IconButton( icon: const Icon(Symbols.category), + style: _showCategories + ? ButtonStyle( + foregroundColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.primary, + ), + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.secondaryContainer, + ), + ) + : null, onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => _PostCategoryPickerPopup( - categories: _categories, - selected: _selectedCategory, - ), - ).then((value) { - if (value != null && context.mounted) { - _selectedCategory = value == false ? null : value; - refreshPosts(); - } - }); + _toggleShowCategories(); }, ), IconButton( @@ -179,122 +227,74 @@ class _ExploreScreenState extends State const Gap(8), ], bottom: TabBar( + isScrollable: _showCategories, controller: _tabController, - tabs: [ - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Symbols.globe, - size: 20, - color: Theme.of(context) - .appBarTheme - .foregroundColor), - const Gap(8), - Flexible( - child: Text( - 'postChannelGlobal', - maxLines: 1, - ).tr().textColor( - Theme.of(context).appBarTheme.foregroundColor), - ), + tabs: _showCategories + ? [ + for (final category in _categories) + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + kCategoryIcons[category.alias] ?? + Symbols.question_mark, + color: Theme.of(context) + .appBarTheme + .foregroundColor!), + const Gap(8), + Flexible( + child: Text( + 'postCategory${category.alias.capitalize()}' + .trExists() + ? 'postCategory${category.alias.capitalize()}' + .tr() + : category.name, + maxLines: 1, + ).textColor(Theme.of(context) + .appBarTheme + .foregroundColor!), + ), + ], + ), + ), + ] + : [ + for (final channel in kPostChannels) + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + kPostChannelIcons[ + kPostChannels.indexOf(channel)], + size: 20, + color: Theme.of(context) + .appBarTheme + .foregroundColor, + ), + const Gap(8), + Flexible( + child: Text( + 'postChannel$channel', + maxLines: 1, + ).tr().textColor(Theme.of(context) + .appBarTheme + .foregroundColor), + ), + ], + ), + ), ], - ), - ), - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Symbols.group, - size: 20, - color: Theme.of(context) - .appBarTheme - .foregroundColor), - const Gap(8), - Flexible( - child: Text( - 'postChannelFriends', - maxLines: 1, - textAlign: TextAlign.center, - ).tr().textColor( - Theme.of(context).appBarTheme.foregroundColor), - ), - ], - ), - ), - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Symbols.subscriptions, - size: 20, - color: Theme.of(context) - .appBarTheme - .foregroundColor), - const Gap(8), - Flexible( - child: Text( - 'postChannelFollowing', - maxLines: 1, - ).tr().textColor( - Theme.of(context).appBarTheme.foregroundColor), - ), - ], - ), - ), - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Symbols.workspaces, - size: 20, - color: Theme.of(context) - .appBarTheme - .foregroundColor), - const Gap(8), - Flexible( - child: Text( - 'postChannelRealm', - maxLines: 1, - ).tr().textColor( - Theme.of(context).appBarTheme.foregroundColor), - ), - ], - ), - ), - ], ), ), ), ]; }, - body: TabBarView( - controller: _tabController, - children: [ - _PostListWidget( - key: _listKeys[0], - onClearFilter: _clearFilter, - ), - _PostListWidget( - key: _listKeys[1], - channel: 'friends', - onClearFilter: _clearFilter, - ), - _PostListWidget( - key: _listKeys[2], - channel: 'following', - onClearFilter: _clearFilter, - ), - _PostListWidget( - key: _listKeys[3], - withRealm: true, - onClearFilter: _clearFilter, - ), - ], + body: _PostListWidget( + key: _listKeys, ), ), ); @@ -302,15 +302,7 @@ class _ExploreScreenState extends State } class _PostListWidget extends StatefulWidget { - final String? channel; - final bool withRealm; - final Function onClearFilter; - - const _PostListWidget( - {super.key, - this.channel, - this.withRealm = false, - required this.onClearFilter}); + const _PostListWidget({super.key}); @override State<_PostListWidget> createState() => _PostListWidgetState(); @@ -320,25 +312,11 @@ class _PostListWidgetState extends State<_PostListWidget> { bool _isBusy = false; final List _posts = List.empty(growable: true); - final List _realms = List.empty(growable: true); SnRealm? _selectedRealm; + String? _selectedChannel; + SnPostCategory? _selectedCategory; int? _postCount; - Future _fetchRealms() async { - try { - final rels = context.read(); - final out = await rels.listAvailableRealms(); - setState(() { - _realms.addAll(out); - _selectedRealm = out.firstOrNull; - }); - } catch (err) { - if (!mounted) return; - context.showErrorDialog(err); - rethrow; - } - } - Future _fetchPosts() async { if (_postCount != null && _posts.length >= _postCount!) return; @@ -349,7 +327,7 @@ class _PostListWidgetState extends State<_PostListWidget> { take: 10, offset: _posts.length, categories: _selectedCategory != null ? [_selectedCategory!.alias] : null, - channel: widget.channel, + channel: _selectedChannel, realm: _selectedRealm?.alias, ); final out = result.$1; @@ -362,6 +340,21 @@ class _PostListWidgetState extends State<_PostListWidget> { if (mounted) setState(() => _isBusy = false); } + void setChannel(String? channel) { + _selectedChannel = channel; + setState(() {}); + } + + void setRealm(SnRealm? realm) { + _selectedRealm = realm; + setState(() {}); + } + + void setCategory(SnPostCategory? category) { + _selectedCategory = category; + setState(() {}); + } + Future refreshPosts() { _postCount = null; _posts.clear(); @@ -371,13 +364,7 @@ class _PostListWidgetState extends State<_PostListWidget> { @override void initState() { super.initState(); - if (widget.withRealm) { - _fetchRealms().then((_) { - _fetchPosts(); - }); - } else { - _fetchPosts(); - } + _fetchPosts(); } @override @@ -400,52 +387,13 @@ class _PostListWidgetState extends State<_PostListWidget> { IconButton( icon: const Icon(Symbols.clear), onPressed: () { - widget.onClearFilter.call(); + setState(() => _selectedCategory = null); refreshPosts(); }, ), ], padding: const EdgeInsets.only(left: 20, right: 4), ), - if (widget.withRealm) - DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - items: _realms - .map( - (ele) => DropdownMenuItem( - value: ele, - child: Row( - children: [ - AccountImage( - content: ele.avatar, - fallbackWidget: const Icon(Symbols.group, size: 16), - radius: 14, - ), - const Gap(8), - Text( - ele.name, - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - ), - ) - .toList(), - value: _selectedRealm, - onChanged: (SnRealm? value) { - setState(() => _selectedRealm = value); - refreshPosts(); - }, - buttonStyleData: const ButtonStyleData( - padding: EdgeInsets.only(left: 4, right: 12), - ), - menuItemStyleData: const MenuItemStyleData( - height: 48, - ), - ), - ), - if (widget.withRealm) const Divider(height: 1), Expanded( child: MediaQuery.removePadding( context: context, @@ -454,6 +402,7 @@ class _PostListWidgetState extends State<_PostListWidget> { displacement: 40 + MediaQuery.of(context).padding.top, onRefresh: () => refreshPosts(), child: InfiniteList( + padding: EdgeInsets.only(top: 8), itemCount: _posts.length, isLoading: _isBusy, centerLoading: true, @@ -475,83 +424,6 @@ class _PostListWidgetState extends State<_PostListWidget> { separatorBuilder: (_, __) => const Gap(8), ), ), - ).padding(top: 8), - ), - ], - ); - } -} - -class _PostCategoryPickerPopup extends StatelessWidget { - final List categories; - final SnPostCategory? selected; - - const _PostCategoryPickerPopup({required this.categories, this.selected}); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Symbols.category, size: 24), - const Gap(16), - Text('postCategory') - .tr() - .textStyle(Theme.of(context).textTheme.titleLarge!), - ], - ).padding(horizontal: 20, top: 16, bottom: 12), - ListTile( - leading: const Icon(Symbols.clear), - title: Text('postFilterReset').tr(), - subtitle: Text('postFilterResetDescription').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - onTap: () { - Navigator.pop(context, false); - }, - ), - const Divider(height: 1), - Expanded( - child: GridView.count( - crossAxisCount: 4, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - childAspectRatio: 1, - children: categories - .map( - (ele) => InkWell( - onTap: () { - _selectedCategory = ele; - Navigator.pop(context, ele); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - kCategoryIcons[ele.alias] ?? Symbols.question_mark, - color: selected == ele - ? Theme.of(context).colorScheme.primary - : null, - ), - const Gap(4), - Text( - 'postCategory${ele.alias.capitalize()}'.trExists() - ? 'postCategory${ele.alias.capitalize()}'.tr() - : ele.name, - ) - .textStyle(Theme.of(context).textTheme.titleMedium!) - .textColor(selected == ele - ? Theme.of(context).colorScheme.primary - : null), - ], - ), - ), - ) - .toList(), ), ), ],