From 6e16a3ac1d6a387d269beaa2f2cee55b0323b23f Mon Sep 17 00:00:00 2001 From: John Wesley Date: Fri, 26 Jan 2024 10:42:15 -0500 Subject: [PATCH] Add default feed mode, explore feed sort, and comment sort options --- lib/src/api/comment.dart | 35 ++++ .../{comments.dart => entry_comments.dart} | 5 +- lib/src/api/post_comments.dart | 5 +- lib/src/screens/entries/entry_comment.dart | 8 +- .../screens/entries/entry_comment_screen.dart | 12 +- lib/src/screens/entries/entry_page.dart | 69 +++---- lib/src/screens/feed_screen.dart | 27 +-- lib/src/screens/posts/post_page.dart | 67 +++---- .../screens/settings/settings_controller.dart | 61 +++++++ lib/src/screens/settings/settings_screen.dart | 169 +++++++++++++++--- 10 files changed, 316 insertions(+), 142 deletions(-) create mode 100644 lib/src/api/comment.dart rename lib/src/api/{comments.dart => entry_comments.dart} (97%) diff --git a/lib/src/api/comment.dart b/lib/src/api/comment.dart new file mode 100644 index 0000000..f7cce7f --- /dev/null +++ b/lib/src/api/comment.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:interstellar/src/widgets/selection_menu.dart'; + +enum CommentSort { newest, top, hot, active, oldest } + +const SelectionMenu commentSortSelect = SelectionMenu( + 'Sort Comments', + [ + SelectionMenuItem( + value: CommentSort.hot, + title: 'Hot', + icon: Icons.local_fire_department, + ), + SelectionMenuItem( + value: CommentSort.top, + title: 'Top', + icon: Icons.trending_up, + ), + SelectionMenuItem( + value: CommentSort.newest, + title: 'Newest', + icon: Icons.auto_awesome_rounded, + ), + SelectionMenuItem( + value: CommentSort.active, + title: 'Active', + icon: Icons.rocket_launch, + ), + SelectionMenuItem( + value: CommentSort.oldest, + title: 'Oldest', + icon: Icons.access_time_outlined, + ), + ], +); diff --git a/lib/src/api/comments.dart b/lib/src/api/entry_comments.dart similarity index 97% rename from lib/src/api/comments.dart rename to lib/src/api/entry_comments.dart index 42667d0..3d068f6 100644 --- a/lib/src/api/comments.dart +++ b/lib/src/api/entry_comments.dart @@ -1,17 +1,16 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:interstellar/src/api/comment.dart'; import 'package:interstellar/src/models/entry_comment.dart'; import 'package:interstellar/src/utils/utils.dart'; -enum CommentsSort { newest, top, hot, active, oldest } - Future fetchComments( http.Client client, String instanceHost, int entryId, { int? page, - CommentsSort? sort, + CommentSort? sort, }) async { final response = await client.get(Uri.https( instanceHost, diff --git a/lib/src/api/post_comments.dart b/lib/src/api/post_comments.dart index c153884..271346b 100644 --- a/lib/src/api/post_comments.dart +++ b/lib/src/api/post_comments.dart @@ -1,17 +1,16 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:interstellar/src/api/comment.dart'; import 'package:interstellar/src/models/post_comment.dart'; import 'package:interstellar/src/utils/utils.dart'; -enum CommentsSort { newest, top, hot, active, oldest } - Future fetchComments( http.Client client, String instanceHost, int postId, { int? page, - CommentsSort? sort, + CommentSort? sort, }) async { final response = await client.get(Uri.https( instanceHost, diff --git a/lib/src/screens/entries/entry_comment.dart b/lib/src/screens/entries/entry_comment.dart index fd9ea3d..6590e67 100644 --- a/lib/src/screens/entries/entry_comment.dart +++ b/lib/src/screens/entries/entry_comment.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:interstellar/src/api/comments.dart' as api_comments; +import 'package:interstellar/src/api/entry_comments.dart' as api_comments; import 'package:interstellar/src/models/entry_comment.dart'; import 'package:interstellar/src/screens/entries/entry_comment_screen.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; @@ -134,8 +134,10 @@ class _EntryCommentState extends State { TextButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( - builder: (context) => - EntryCommentScreen(widget.comment.commentId, opUserId: widget.opUserId), + builder: (context) => EntryCommentScreen( + widget.comment.commentId, + opUserId: widget.opUserId, + ), ), ), child: Text( diff --git a/lib/src/screens/entries/entry_comment_screen.dart b/lib/src/screens/entries/entry_comment_screen.dart index 9bd67cf..6365e57 100644 --- a/lib/src/screens/entries/entry_comment_screen.dart +++ b/lib/src/screens/entries/entry_comment_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:interstellar/src/api/comments.dart'; +import 'package:interstellar/src/api/entry_comments.dart'; import 'package:interstellar/src/models/entry_comment.dart'; import 'package:interstellar/src/screens/entries/entry_comment.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; @@ -49,11 +49,11 @@ class _EntryCommentScreenState extends State { vertical: 4, ), child: EntryComment( - _comment!, - (newComment) => setState(() { - _comment = newComment; - }), opUserId: widget.opUserId - ), + _comment!, + (newComment) => setState(() { + _comment = newComment; + }), + opUserId: widget.opUserId), ) ], ) diff --git a/lib/src/screens/entries/entry_page.dart b/lib/src/screens/entries/entry_page.dart index 40b8f10..c3d2823 100644 --- a/lib/src/screens/entries/entry_page.dart +++ b/lib/src/screens/entries/entry_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:interstellar/src/api/comments.dart' as api_comments; +import 'package:interstellar/src/api/comment.dart'; import 'package:interstellar/src/api/entries.dart' as api_entries; +import 'package:interstellar/src/api/entry_comments.dart' as api_comments; import 'package:interstellar/src/models/entry.dart'; import 'package:interstellar/src/models/entry_comment.dart'; import 'package:interstellar/src/screens/entries/entry_comment.dart'; @@ -27,7 +28,7 @@ class EntryPage extends StatefulWidget { class _EntryPageState extends State { EntryModel? _data; - api_comments.CommentsSort commentsSort = api_comments.CommentsSort.hot; + CommentSort commentSort = CommentSort.hot; final PagingController _pagingController = PagingController(firstPageKey: 1); @@ -37,6 +38,7 @@ class _EntryPageState extends State { super.initState(); _data = widget.initData; + commentSort = context.read().defaultCommentSort; _pagingController.addPageRequestListener(_fetchPage); } @@ -55,7 +57,7 @@ class _EntryPageState extends State { context.read().instanceHost, _data!.entryId, page: pageKey, - sort: commentsSort, + sort: commentSort, ); // Check BuildContext @@ -88,6 +90,25 @@ class _EntryPageState extends State { return Scaffold( appBar: AppBar( title: Text(_data?.title ?? ''), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + onPressed: () async { + final newSort = await commentSortSelect.inquireSelection( + context, commentSort); + + if (newSort != null && newSort != commentSort) { + setState(() { + commentSort = newSort; + _pagingController.refresh(); + }); + } + }, + icon: const Icon(Icons.sort), + ), + ), + ], ), body: RefreshIndicator( onRefresh: () => Future.sync( @@ -153,48 +174,6 @@ class _EntryPageState extends State { : null, ), ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - DropdownButton( - value: commentsSort, - onChanged: (newSort) { - if (newSort != null) { - setState(() { - commentsSort = newSort; - _pagingController.refresh(); - }); - } - }, - items: const [ - DropdownMenuItem( - value: api_comments.CommentsSort.hot, - child: Text('Hot'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.top, - child: Text('Top'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.newest, - child: Text('Newest'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.active, - child: Text('Active'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.oldest, - child: Text('Oldest'), - ), - ], - ), - ], - ), - ), - ), PagedSliverList( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( diff --git a/lib/src/screens/feed_screen.dart b/lib/src/screens/feed_screen.dart index a5c27e8..17873ba 100644 --- a/lib/src/screens/feed_screen.dart +++ b/lib/src/screens/feed_screen.dart @@ -35,14 +35,19 @@ class FeedScreen extends StatefulWidget { enum FeedMode { entries, posts } class _FeedScreenState extends State { - FeedMode _feedMode = FeedMode.entries; + FeedMode _mode = FeedMode.entries; FeedSort _sort = FeedSort.hot; @override void initState() { super.initState(); - _sort = context.read().defaultFeedSort; + _mode = (widget.source ?? const FeedSourceAll()).getPostsPath() != null + ? context.read().defaultFeedMode + : FeedMode.entries; + _sort = widget.source == null + ? context.read().defaultFeedSort + : context.read().defaultExploreFeedSort; } @override @@ -76,10 +81,10 @@ class _FeedScreenState extends State { tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity(horizontal: -3, vertical: -3), ), - selected: {_feedMode}, + selected: {_mode}, onSelectionChanged: (Set newSelection) { setState(() { - _feedMode = newSelection.first; + _mode = newSelection.first; }); }, ), @@ -99,7 +104,7 @@ class _FeedScreenState extends State { }, icon: const Icon(Icons.sort), ), - ) + ), ], bottom: widget.source == null ? whenLoggedIn( @@ -129,7 +134,7 @@ class _FeedScreenState extends State { ? FeedScreenBody( source: widget.source!, sort: _sort, - mode: _feedMode, + mode: _mode, details: widget.details, ) : whenLoggedIn( @@ -139,25 +144,25 @@ class _FeedScreenState extends State { FeedScreenBody( source: const FeedSourceSub(), sort: _sort, - mode: _feedMode, + mode: _mode, details: widget.details, ), FeedScreenBody( source: const FeedSourceMod(), sort: _sort, - mode: _feedMode, + mode: _mode, details: widget.details, ), FeedScreenBody( source: const FeedSourceFav(), sort: _sort, - mode: _feedMode, + mode: _mode, details: widget.details, ), FeedScreenBody( source: const FeedSourceAll(), sort: _sort, - mode: _feedMode, + mode: _mode, details: widget.details, ), ], @@ -165,7 +170,7 @@ class _FeedScreenState extends State { otherwise: FeedScreenBody( source: const FeedSourceAll(), sort: _sort, - mode: _feedMode, + mode: _mode, details: widget.details, ), ), diff --git a/lib/src/screens/posts/post_page.dart b/lib/src/screens/posts/post_page.dart index b023fa4..d46c55b 100644 --- a/lib/src/screens/posts/post_page.dart +++ b/lib/src/screens/posts/post_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:interstellar/src/api/comment.dart'; import 'package:interstellar/src/api/post_comments.dart' as api_comments; import 'package:interstellar/src/api/posts.dart' as api_posts; import 'package:interstellar/src/models/post.dart'; @@ -27,7 +28,7 @@ class PostPage extends StatefulWidget { class _PostPageState extends State { PostModel? _data; - api_comments.CommentsSort commentsSort = api_comments.CommentsSort.hot; + CommentSort commentSort = CommentSort.hot; final PagingController _pagingController = PagingController(firstPageKey: 1); @@ -37,6 +38,7 @@ class _PostPageState extends State { super.initState(); _data = widget.initData; + commentSort = context.read().defaultCommentSort; _pagingController.addPageRequestListener(_fetchPage); } @@ -55,7 +57,7 @@ class _PostPageState extends State { context.read().instanceHost, _data!.postId, page: pageKey, - sort: commentsSort, + sort: commentSort, ); // Check BuildContext @@ -86,6 +88,25 @@ class _PostPageState extends State { return Scaffold( appBar: AppBar( title: Text(_data?.user.username ?? ''), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + onPressed: () async { + final newSort = await commentSortSelect.inquireSelection( + context, commentSort); + + if (newSort != null && newSort != commentSort) { + setState(() { + commentSort = newSort; + _pagingController.refresh(); + }); + } + }, + icon: const Icon(Icons.sort), + ), + ), + ], ), body: RefreshIndicator( onRefresh: () => Future.sync( @@ -149,48 +170,6 @@ class _PostPageState extends State { : null, ), ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - DropdownButton( - value: commentsSort, - onChanged: (newSort) { - if (newSort != null) { - setState(() { - commentsSort = newSort; - _pagingController.refresh(); - }); - } - }, - items: const [ - DropdownMenuItem( - value: api_comments.CommentsSort.hot, - child: Text('Hot'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.top, - child: Text('Top'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.newest, - child: Text('Newest'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.active, - child: Text('Active'), - ), - DropdownMenuItem( - value: api_comments.CommentsSort.oldest, - child: Text('Oldest'), - ), - ], - ), - ], - ), - ), - ), PagedSliverList( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( diff --git a/lib/src/screens/settings/settings_controller.dart b/lib/src/screens/settings/settings_controller.dart index b8ea8f8..a0cf3a2 100644 --- a/lib/src/screens/settings/settings_controller.dart +++ b/lib/src/screens/settings/settings_controller.dart @@ -2,16 +2,24 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:interstellar/src/api/comment.dart'; import 'package:interstellar/src/api/feed_source.dart'; import 'package:interstellar/src/api/oauth.dart'; +import 'package:interstellar/src/screens/feed_screen.dart'; import 'package:oauth2/oauth2.dart' as oauth2; import 'package:shared_preferences/shared_preferences.dart'; class SettingsController with ChangeNotifier { late ThemeMode _themeMode; ThemeMode get themeMode => _themeMode; + late FeedMode _defaultFeedMode; + FeedMode get defaultFeedMode => _defaultFeedMode; late FeedSort _defaultFeedSort; FeedSort get defaultFeedSort => _defaultFeedSort; + late FeedSort _defaultExploreFeedSort; + FeedSort get defaultExploreFeedSort => _defaultExploreFeedSort; + late CommentSort _defaultCommentSort; + CommentSort get defaultCommentSort => _defaultCommentSort; late Map _oauthIdentifiers; late Map _oauthCredentials; @@ -31,9 +39,18 @@ class SettingsController with ChangeNotifier { _themeMode = prefs.getString('themeMode') != null ? ThemeMode.values.byName(prefs.getString("themeMode")!) : ThemeMode.system; + _defaultFeedMode = prefs.getString('defaultFeedMode') != null + ? FeedMode.values.byName(prefs.getString("defaultFeedMode")!) + : FeedMode.entries; _defaultFeedSort = prefs.getString('defaultFeedSort') != null ? FeedSort.values.byName(prefs.getString("defaultFeedSort")!) : FeedSort.hot; + _defaultExploreFeedSort = prefs.getString('defaultExploreFeedSort') != null + ? FeedSort.values.byName(prefs.getString("defaultExploreFeedSort")!) + : FeedSort.newest; + _defaultCommentSort = prefs.getString('defaultCommentSort') != null + ? CommentSort.values.byName(prefs.getString("defaultCommentSort")!) + : CommentSort.hot; _oauthIdentifiers = (jsonDecode(prefs.getString('oauthIdentifiers') ?? '{}') as Map) @@ -62,6 +79,19 @@ class SettingsController with ChangeNotifier { await prefs.setString('themeMode', newThemeMode.name); } + Future updateDefaultFeedMode(FeedMode? newDefaultFeedMode) async { + if (newDefaultFeedMode == null) return; + + if (newDefaultFeedMode == _defaultFeedMode) return; + + _defaultFeedMode = newDefaultFeedMode; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('defaultFeedMode', newDefaultFeedMode.name); + } + Future updateDefaultFeedSort(FeedSort? newDefaultFeedSort) async { if (newDefaultFeedSort == null) return; @@ -75,6 +105,37 @@ class SettingsController with ChangeNotifier { await prefs.setString('defaultFeedSort', newDefaultFeedSort.name); } + Future updateDefaultExploreFeedSort( + FeedSort? newDefaultExploreFeedSort, + ) async { + if (newDefaultExploreFeedSort == null) return; + + if (newDefaultExploreFeedSort == _defaultExploreFeedSort) return; + + _defaultExploreFeedSort = newDefaultExploreFeedSort; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString( + 'defaultExploreFeedSort', newDefaultExploreFeedSort.name); + } + + Future updateDefaultCommentSort( + CommentSort? newDefaultCommentSort, + ) async { + if (newDefaultCommentSort == null) return; + + if (newDefaultCommentSort == _defaultCommentSort) return; + + _defaultCommentSort = newDefaultCommentSort; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('defaultCommentSort', newDefaultCommentSort.name); + } + Future getOAuthIdentifier(String instanceHost) async { if (_oauthIdentifiers.containsKey(instanceHost)) { return _oauthIdentifiers[instanceHost]!; diff --git a/lib/src/screens/settings/settings_screen.dart b/lib/src/screens/settings/settings_screen.dart index 962d037..f155938 100644 --- a/lib/src/screens/settings/settings_screen.dart +++ b/lib/src/screens/settings/settings_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:interstellar/src/api/comment.dart'; import 'package:interstellar/src/screens/feed_screen.dart'; import 'package:interstellar/src/screens/settings/login.dart'; +import 'package:interstellar/src/widgets/selection_menu.dart'; import 'settings_controller.dart'; @@ -11,9 +13,21 @@ class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final currentThemeMode = themeModeSelect.options.firstWhere( + (option) => option.value == controller.themeMode, + ); + final currentDefaultFeedMode = feedModeSelect.options.firstWhere( + (option) => option.value == controller.defaultFeedMode, + ); final currentDefaultFeedSort = feedSortSelect.options.firstWhere( (option) => option.value == controller.defaultFeedSort, ); + final currentDefaultExploreFeedSort = feedSortSelect.options.firstWhere( + (option) => option.value == controller.defaultExploreFeedSort, + ); + final currentDefaultCommentSort = commentSortSelect.options.firstWhere( + (option) => option.value == controller.defaultCommentSort, + ); return Scaffold( appBar: AppBar( @@ -27,28 +41,50 @@ class SettingsScreen extends StatelessWidget { child: Text('Theme', style: Theme.of(context).textTheme.titleMedium), ), - DropdownButton( - value: controller.themeMode, - onChanged: controller.updateThemeMode, - items: const [ - DropdownMenuItem( - value: ThemeMode.system, - child: Text('System Theme'), - ), - DropdownMenuItem( - value: ThemeMode.light, - child: Text('Light Theme'), - ), - DropdownMenuItem( - value: ThemeMode.dark, - child: Text('Dark Theme'), - ) - ], + ListTile( + title: const Text('Theme Mode'), + leading: const Icon(Icons.palette), + onTap: () async { + controller.updateThemeMode( + await themeModeSelect.inquireSelection( + context, + currentThemeMode.value, + ), + ); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(currentThemeMode.icon), + const SizedBox(width: 4), + Text(currentThemeMode.title), + ], + ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text('Feed', style: Theme.of(context).textTheme.titleMedium), ), + ListTile( + title: const Text('Default Feed Mode'), + leading: const Icon(Icons.tab), + onTap: () async { + controller.updateDefaultFeedMode( + await feedModeSelect.inquireSelection( + context, + currentDefaultFeedMode.value, + ), + ); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(currentDefaultFeedMode.icon), + const SizedBox(width: 4), + Text(currentDefaultFeedMode.title), + ], + ), + ), ListTile( title: const Text('Default Feed Sort'), leading: const Icon(Icons.sort), @@ -69,6 +105,46 @@ class SettingsScreen extends StatelessWidget { ], ), ), + ListTile( + title: const Text('Default Explore Feed Sort'), + leading: const Icon(Icons.sort), + onTap: () async { + controller.updateDefaultExploreFeedSort( + await feedSortSelect.inquireSelection( + context, + currentDefaultExploreFeedSort.value, + ), + ); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(currentDefaultExploreFeedSort.icon), + const SizedBox(width: 4), + Text(currentDefaultExploreFeedSort.title), + ], + ), + ), + ListTile( + title: const Text('Default Comment Sort'), + leading: const Icon(Icons.comment), + onTap: () async { + controller.updateDefaultCommentSort( + await commentSortSelect.inquireSelection( + context, + currentDefaultCommentSort.value, + ), + ); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(currentDefaultCommentSort.icon), + const SizedBox(width: 4), + Text(currentDefaultCommentSort.title), + ], + ), + ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text('Accounts', @@ -110,19 +186,58 @@ class SettingsScreen extends StatelessWidget { }, icon: const Icon(Icons.delete_outline)), )), - const SizedBox(height: 8), - OutlinedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const LoginScreen(), - ), - ); - }, - child: const Text('Add Account'), + Padding( + padding: const EdgeInsets.all(8.0), + child: OutlinedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const LoginScreen(), + ), + ); + }, + child: const Text('Add Account'), + ), ) ], ), ); } } + +const SelectionMenu themeModeSelect = SelectionMenu( + 'Theme Mode', + [ + SelectionMenuItem( + value: ThemeMode.system, + title: 'System', + icon: Icons.auto_mode, + ), + SelectionMenuItem( + value: ThemeMode.light, + title: 'Light', + icon: Icons.light_mode, + ), + SelectionMenuItem( + value: ThemeMode.dark, + title: 'Dark', + icon: Icons.dark_mode, + ), + ], +); + +const SelectionMenu feedModeSelect = SelectionMenu( + 'Feed Mode', + [ + SelectionMenuItem( + value: FeedMode.entries, + title: 'Threads', + icon: Icons.feed, + ), + SelectionMenuItem( + value: FeedMode.posts, + title: 'Posts', + icon: Icons.chat, + ), + ], +);