diff --git a/lib/src/screens/explore/domains_screen.dart b/lib/src/screens/explore/domains_screen.dart index 016693d..034509c 100644 --- a/lib/src/screens/explore/domains_screen.dart +++ b/lib/src/screens/explore/domains_screen.dart @@ -9,8 +9,11 @@ import 'package:interstellar/src/widgets/subscription_button.dart'; import 'package:provider/provider.dart'; class DomainsScreen extends StatefulWidget { + final bool onlySubbed; + const DomainsScreen({ super.key, + this.onlySubbed = false, }); @override @@ -28,6 +31,10 @@ class _DomainsScreenState extends State { void initState() { super.initState(); + if (widget.onlySubbed) { + filter = KbinAPIDomainsFilter.subscribed; + } + _pagingController.addPageRequestListener(_fetchPage); } @@ -61,62 +68,63 @@ class _DomainsScreenState extends State { ), child: CustomScrollView( slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - ...whenLoggedIn(context, [ - Padding( - padding: const EdgeInsets.only(right: 12), - child: DropdownButton( - value: filter, - onChanged: (newFilter) { - if (newFilter != null) { - setState(() { - filter = newFilter; - _pagingController.refresh(); - }); - } - }, - items: const [ - DropdownMenuItem( - value: KbinAPIDomainsFilter.all, - child: Text('All'), - ), - DropdownMenuItem( - value: KbinAPIDomainsFilter.subscribed, - child: Text('Subscribed'), - ), - DropdownMenuItem( - value: KbinAPIDomainsFilter.blocked, - child: Text('Blocked'), - ), - ], - ), - ) - ]) ?? - [], - if (filter == KbinAPIDomainsFilter.all) - SizedBox( - width: 128, - child: TextFormField( - initialValue: search, - onChanged: (newSearch) { - setState(() { - search = newSearch; - _pagingController.refresh(); - }); - }, - decoration: const InputDecoration( - border: OutlineInputBorder(), - label: Text('Search')), - ), - ) - ], + if (!widget.onlySubbed) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + ...whenLoggedIn(context, [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: DropdownButton( + value: filter, + onChanged: (newFilter) { + if (newFilter != null) { + setState(() { + filter = newFilter; + _pagingController.refresh(); + }); + } + }, + items: const [ + DropdownMenuItem( + value: KbinAPIDomainsFilter.all, + child: Text('All'), + ), + DropdownMenuItem( + value: KbinAPIDomainsFilter.subscribed, + child: Text('Subscribed'), + ), + DropdownMenuItem( + value: KbinAPIDomainsFilter.blocked, + child: Text('Blocked'), + ), + ], + ), + ) + ]) ?? + [], + if (filter == KbinAPIDomainsFilter.all) + SizedBox( + width: 128, + child: TextFormField( + initialValue: search, + onChanged: (newSearch) { + setState(() { + search = newSearch; + _pagingController.refresh(); + }); + }, + decoration: const InputDecoration( + border: OutlineInputBorder(), + label: Text('Search')), + ), + ) + ], + ), ), ), - ), PagedSliverList( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( diff --git a/lib/src/screens/explore/magazine_screen.dart b/lib/src/screens/explore/magazine_screen.dart index 3798947..17ce305 100644 --- a/lib/src/screens/explore/magazine_screen.dart +++ b/lib/src/screens/explore/magazine_screen.dart @@ -7,6 +7,7 @@ import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/widgets/avatar.dart'; import 'package:interstellar/src/widgets/markdown.dart'; +import 'package:interstellar/src/widgets/star_button.dart'; import 'package:interstellar/src/widgets/subscription_button.dart'; import 'package:provider/provider.dart'; @@ -45,6 +46,12 @@ class _MagazineScreenState extends State { @override Widget build(BuildContext context) { + final globalName = _data == null + ? null + : _data!.name.contains('@') + ? '!${_data!.name}' + : '!${_data!.name}@${context.watch().instanceHost}'; + return FeedScreen( source: FeedSource.magazine, sourceId: widget.magazineId, @@ -87,11 +94,7 @@ class _MagazineScreenState extends State { ), ); }, - child: Text( - _data!.name.contains('@') - ? '!${_data!.name}' - : '!${_data!.name}@${context.read().instanceHost}', - ), + child: Text(globalName!), ) ], ), @@ -114,6 +117,7 @@ class _MagazineScreenState extends State { } }), ), + StarButton(globalName), if (whenLoggedIn(context, true) == true) IconButton( onPressed: () async { diff --git a/lib/src/screens/explore/magazines_screen.dart b/lib/src/screens/explore/magazines_screen.dart index 9bda356..6e0f633 100644 --- a/lib/src/screens/explore/magazines_screen.dart +++ b/lib/src/screens/explore/magazines_screen.dart @@ -10,8 +10,11 @@ import 'package:interstellar/src/widgets/subscription_button.dart'; import 'package:provider/provider.dart'; class MagazinesScreen extends StatefulWidget { + final bool onlySubbed; + const MagazinesScreen({ super.key, + this.onlySubbed = false, }); @override @@ -30,6 +33,10 @@ class _MagazinesScreenState extends State { void initState() { super.initState(); + if (widget.onlySubbed) { + filter = APIMagazinesFilter.subscribed; + } + _pagingController.addPageRequestListener(_fetchPage); } @@ -65,108 +72,109 @@ class _MagazinesScreenState extends State { ), child: CustomScrollView( slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 12), - child: DropdownButton( - value: filter, - onChanged: (newFilter) { - if (newFilter != null) { - setState(() { - filter = newFilter; - _pagingController.refresh(); - }); - } - }, - items: [ - const DropdownMenuItem( - value: APIMagazinesFilter.all, - child: Text('All'), - ), - const DropdownMenuItem( - value: APIMagazinesFilter.local, - child: Text('Local'), - ), - ...(whenLoggedIn(context, [ - const DropdownMenuItem( - value: APIMagazinesFilter.subscribed, - child: Text('Subscribed'), - ), - const DropdownMenuItem( - value: APIMagazinesFilter.moderated, - child: Text('Moderated'), - ), - if (context - .read() - .serverSoftware != - ServerSoftware.lemmy) + if (!widget.onlySubbed) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: DropdownButton( + value: filter, + onChanged: (newFilter) { + if (newFilter != null) { + setState(() { + filter = newFilter; + _pagingController.refresh(); + }); + } + }, + items: [ + const DropdownMenuItem( + value: APIMagazinesFilter.all, + child: Text('All'), + ), + const DropdownMenuItem( + value: APIMagazinesFilter.local, + child: Text('Local'), + ), + ...(whenLoggedIn(context, [ const DropdownMenuItem( - value: APIMagazinesFilter.blocked, - child: Text('Blocked'), + value: APIMagazinesFilter.subscribed, + child: Text('Subscribed'), ), - ]) ?? - []) - ], + const DropdownMenuItem( + value: APIMagazinesFilter.moderated, + child: Text('Moderated'), + ), + if (context + .read() + .serverSoftware != + ServerSoftware.lemmy) + const DropdownMenuItem( + value: APIMagazinesFilter.blocked, + child: Text('Blocked'), + ), + ]) ?? + []) + ], + ), ), - ), - ...(context.watch().serverSoftware == - ServerSoftware.lemmy || - filter == APIMagazinesFilter.all || - filter == APIMagazinesFilter.local - ? [ - Padding( - padding: const EdgeInsets.only(right: 12), - child: DropdownButton( - value: sort, - onChanged: (newSort) { - if (newSort != null) { + ...(context.watch().serverSoftware == + ServerSoftware.lemmy || + filter == APIMagazinesFilter.all || + filter == APIMagazinesFilter.local + ? [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: DropdownButton( + value: sort, + onChanged: (newSort) { + if (newSort != null) { + setState(() { + sort = newSort; + _pagingController.refresh(); + }); + } + }, + items: const [ + DropdownMenuItem( + value: APIMagazinesSort.hot, + child: Text('Top'), + ), + DropdownMenuItem( + value: APIMagazinesSort.active, + child: Text('Active'), + ), + DropdownMenuItem( + value: APIMagazinesSort.newest, + child: Text('Newest'), + ), + ], + ), + ), + SizedBox( + width: 128, + child: TextFormField( + initialValue: search, + onChanged: (newSearch) { setState(() { - sort = newSort; + search = newSearch; _pagingController.refresh(); }); - } - }, - items: const [ - DropdownMenuItem( - value: APIMagazinesSort.hot, - child: Text('Top'), - ), - DropdownMenuItem( - value: APIMagazinesSort.active, - child: Text('Active'), - ), - DropdownMenuItem( - value: APIMagazinesSort.newest, - child: Text('Newest'), - ), - ], + }, + decoration: const InputDecoration( + border: OutlineInputBorder(), + label: Text('Search')), + ), ), - ), - SizedBox( - width: 128, - child: TextFormField( - initialValue: search, - onChanged: (newSearch) { - setState(() { - search = newSearch; - _pagingController.refresh(); - }); - }, - decoration: const InputDecoration( - border: OutlineInputBorder(), - label: Text('Search')), - ), - ), - ] - : []), - ], + ] + : []), + ], + ), ), ), - ), PagedSliverList( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( diff --git a/lib/src/screens/explore/user_screen.dart b/lib/src/screens/explore/user_screen.dart index d7ca174..0e01fd0 100644 --- a/lib/src/screens/explore/user_screen.dart +++ b/lib/src/screens/explore/user_screen.dart @@ -18,6 +18,7 @@ import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/widgets/avatar.dart'; import 'package:interstellar/src/widgets/loading_template.dart'; import 'package:interstellar/src/widgets/markdown.dart'; +import 'package:interstellar/src/widgets/star_button.dart'; import 'package:interstellar/src/widgets/subscription_button.dart'; import 'package:interstellar/src/widgets/text_editor.dart'; import 'package:interstellar/src/widgets/wrapper.dart'; @@ -71,6 +72,10 @@ class _UserScreenState extends State { final user = _data!; final currentFeedSortOption = feedSortSelect.getOption(_sort); + final globalName = user.name.contains('@') + ? '@${user.name}' + : '@${user.name}@${context.watch().instanceHost}'; + return Scaffold( appBar: AppBar( title: Row( @@ -155,22 +160,23 @@ class _UserScreenState extends State { matchesUsername: user.name) != null) Positioned( - right: 0, - top: 0, - child: Padding( - padding: const EdgeInsets.all(12), - child: TextButton( - onPressed: () => Navigator.of(context).push( - MaterialPageRoute(builder: (context) { - return ProfileEditScreen(_data!, (DetailedUserModel? user) { - setState(() { - _data = user; - }); - }); - }) - ), - child: const Text("Edit")) - ) + right: 0, + top: 0, + child: Padding( + padding: const EdgeInsets.all(12), + child: TextButton( + onPressed: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return ProfileEditScreen(_data!, + (DetailedUserModel? user) { + setState(() { + _data = user; + }); + }); + })), + child: const Text("Edit"), + ), + ), ) ], ), @@ -209,11 +215,7 @@ class _UserScreenState extends State { ), ); }, - child: Text( - user.name.contains('@') - ? '@${user.name}' - : '@${user.name}@${context.read().instanceHost}', - ), + child: Text(globalName), ) ], ), @@ -236,6 +238,7 @@ class _UserScreenState extends State { } }), ), + StarButton(globalName), if (whenLoggedIn(context, true) == true) IconButton( onPressed: () async { @@ -332,10 +335,9 @@ class _UserScreenState extends State { Padding( padding: const EdgeInsets.only(top: 12), child: Markdown( - user.about!, - getNameHost(context, user.name), - ) - ), + user.about!, + getNameHost(context, user.name), + )), ], ), ), @@ -452,7 +454,9 @@ class _UserScreenBodyState extends State { @override void didUpdateWidget(covariant oldWidget) { super.didUpdateWidget(oldWidget); - _pagingController.refresh(); + if (widget.mode != oldWidget.mode || widget.sort != oldWidget.sort) { + _pagingController.refresh(); + } } @override diff --git a/lib/src/screens/explore/users_screen.dart b/lib/src/screens/explore/users_screen.dart index b019139..468d406 100644 --- a/lib/src/screens/explore/users_screen.dart +++ b/lib/src/screens/explore/users_screen.dart @@ -10,8 +10,11 @@ import 'package:interstellar/src/widgets/subscription_button.dart'; import 'package:provider/provider.dart'; class UsersScreen extends StatefulWidget { + final bool onlySubbed; + const UsersScreen({ super.key, + this.onlySubbed = false, }); @override @@ -28,6 +31,10 @@ class _UsersScreenState extends State { void initState() { super.initState(); + if (widget.onlySubbed) { + filter = api_users.UsersFilter.followed; + } + _pagingController.addPageRequestListener(_fetchPage); } @@ -60,50 +67,51 @@ class _UsersScreenState extends State { ), child: CustomScrollView( slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - ...whenLoggedIn(context, [ - Padding( - padding: const EdgeInsets.only(right: 12), - child: DropdownButton( - value: filter, - onChanged: (newFilter) { - if (newFilter != null) { - setState(() { - filter = newFilter; - _pagingController.refresh(); - }); - } - }, - items: const [ - DropdownMenuItem( - value: api_users.UsersFilter.all, - child: Text('All'), - ), - DropdownMenuItem( - value: api_users.UsersFilter.followed, - child: Text('Followed'), - ), - DropdownMenuItem( - value: api_users.UsersFilter.followers, - child: Text('Followers'), - ), - DropdownMenuItem( - value: api_users.UsersFilter.blocked, - child: Text('Blocked'), - ), - ], - ), - ) - ]) ?? - [], - ], + if (!widget.onlySubbed) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + ...whenLoggedIn(context, [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: DropdownButton( + value: filter, + onChanged: (newFilter) { + if (newFilter != null) { + setState(() { + filter = newFilter; + _pagingController.refresh(); + }); + } + }, + items: const [ + DropdownMenuItem( + value: api_users.UsersFilter.all, + child: Text('All'), + ), + DropdownMenuItem( + value: api_users.UsersFilter.followed, + child: Text('Followed'), + ), + DropdownMenuItem( + value: api_users.UsersFilter.followers, + child: Text('Followers'), + ), + DropdownMenuItem( + value: api_users.UsersFilter.blocked, + child: Text('Blocked'), + ), + ], + ), + ) + ]) ?? + [], + ], + ), ), ), - ), PagedSliverList( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( diff --git a/lib/src/screens/feed/feed_screen.dart b/lib/src/screens/feed/feed_screen.dart index ed4d9c4..7e01c03 100644 --- a/lib/src/screens/feed/feed_screen.dart +++ b/lib/src/screens/feed/feed_screen.dart @@ -4,6 +4,7 @@ import 'package:interstellar/src/api/feed_source.dart'; import 'package:interstellar/src/models/magazine.dart'; import 'package:interstellar/src/models/post.dart'; import 'package:interstellar/src/screens/create_screen.dart'; +import 'package:interstellar/src/screens/feed/nav_drawer.dart'; import 'package:interstellar/src/screens/feed/post_item.dart'; import 'package:interstellar/src/screens/feed/post_page.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; @@ -361,6 +362,7 @@ class _FeedScreenState extends State { ) .toList(), ), + drawer: widget.sourceId != null ? null : const NavDrawer(), ), ); } diff --git a/lib/src/screens/feed/nav_drawer.dart b/lib/src/screens/feed/nav_drawer.dart new file mode 100644 index 0000000..3347f70 --- /dev/null +++ b/lib/src/screens/feed/nav_drawer.dart @@ -0,0 +1,296 @@ +import 'package:flutter/material.dart'; +import 'package:interstellar/src/api/domains.dart'; +import 'package:interstellar/src/api/magazines.dart'; +import 'package:interstellar/src/api/users.dart'; +import 'package:interstellar/src/models/domain.dart'; +import 'package:interstellar/src/models/magazine.dart'; +import 'package:interstellar/src/models/user.dart'; +import 'package:interstellar/src/screens/explore/domain_screen.dart'; +import 'package:interstellar/src/screens/explore/domains_screen.dart'; +import 'package:interstellar/src/screens/explore/magazine_screen.dart'; +import 'package:interstellar/src/screens/explore/magazines_screen.dart'; +import 'package:interstellar/src/screens/explore/user_screen.dart'; +import 'package:interstellar/src/screens/explore/users_screen.dart'; +import 'package:interstellar/src/screens/settings/settings_controller.dart'; +import 'package:interstellar/src/widgets/avatar.dart'; +import 'package:interstellar/src/widgets/settings_header.dart'; +import 'package:interstellar/src/widgets/star_button.dart'; +import 'package:provider/provider.dart'; + +class NavDrawer extends StatefulWidget { + const NavDrawer({super.key}); + + @override + State createState() => _NavDrawerState(); +} + +class _NavDrawerState extends State { + List? subbedMagazines; + List? subbedUsers; + List? subbedDomains; + + @override + void initState() { + super.initState(); + + if (context.read().isLoggedIn) { + context + .read() + .api + .magazines + .list(filter: APIMagazinesFilter.subscribed) + .then((value) => setState(() { + if (value.items.isNotEmpty) { + subbedMagazines = value.items; + } + })); + if (context.read().serverSoftware != + ServerSoftware.lemmy) { + context + .read() + .api + .users + .list(filter: UsersFilter.followed) + .then((value) => setState(() { + if (value.items.isNotEmpty) { + subbedUsers = value.items; + } + })); + context + .read() + .api + .domains + .list(filter: KbinAPIDomainsFilter.subscribed) + .then((value) => setState(() { + if (value.items.isNotEmpty) { + subbedDomains = value.items; + } + })); + } + } + } + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: SettingsHeader('Stars'), + ), + if (context.watch().stars.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'This feels empty; star a magazine or user to appear here.', + style: TextStyle(fontWeight: FontWeight.w300), + ), + ), + ...(context.watch().stars.toList()..sort()).map( + (star) => ListTile( + title: Text(star), + onTap: () async { + String name = star.substring(1); + if (name.endsWith( + context.read().instanceHost)) { + name = name.split('@').first; + } + + switch (star[0]) { + case '@': + final user = await context + .read() + .api + .users + .getByName(name); + + if (!mounted) return; + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + UserScreen(user.id, initData: user), + ), + ); + break; + + case '!': + final magazine = await context + .read() + .api + .magazines + .getByName(name); + + if (!mounted) return; + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + MagazineScreen(magazine.id, initData: magazine), + ), + ); + break; + } + }, + trailing: StarButton(star), + ), + ), + if (context.watch().isLoggedIn && + (subbedMagazines != null || + subbedUsers != null || + subbedDomains != null)) ...[ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: SettingsHeader('Subscriptions'), + ), + if (subbedMagazines != null) ...[ + ...subbedMagazines! + .asMap() + .map( + (index, magazine) => MapEntry( + index, + ListTile( + title: Text(magazine.name), + leading: magazine.icon == null + ? null + : Avatar(magazine.icon, radius: 16), + trailing: StarButton(magazine.name.contains('@') + ? '!${magazine.name}' + : '!${magazine.name}@${context.watch().instanceHost}'), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MagazineScreen( + magazine.id, + initData: magazine, + onUpdate: (newValue) { + setState(() { + subbedMagazines![index] = newValue; + }); + }, + ), + ), + ); + }, + ), + ), + ) + .values, + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Scaffold( + appBar: + AppBar(title: const Text('Magazine Subscriptions')), + body: const MagazinesScreen(onlySubbed: true), + ), + ), + ), + child: const Text('All Magazine Subs'), + ), + ), + ], + ], + if (context.read().serverSoftware != + ServerSoftware.lemmy && + subbedUsers != null) ...[ + ...subbedUsers! + .asMap() + .map( + (index, user) => MapEntry( + index, + ListTile( + title: Text(user.name), + leading: user.avatar == null + ? null + : Avatar(user.avatar, radius: 16), + trailing: StarButton(user.name.contains('@') + ? '@${user.name}' + : '@${user.name}@${context.watch().instanceHost}'), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => UserScreen( + user.id, + initData: user, + onUpdate: (newValue) { + setState(() { + subbedUsers![index] = newValue; + }); + }, + ), + ), + ); + }, + ), + ), + ) + .values, + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Scaffold( + appBar: AppBar(title: const Text('User Follows')), + body: const UsersScreen(onlySubbed: true), + ), + ), + ), + child: const Text('All User Follows'), + ), + ), + ], + if (context.read().serverSoftware != + ServerSoftware.lemmy && + subbedDomains != null) ...[ + ...subbedDomains! + .asMap() + .map( + (index, domain) => MapEntry( + index, + ListTile( + title: Text(domain.name), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DomainScreen( + domain.id, + initData: domain, + onUpdate: (newValue) { + setState(() { + subbedDomains![index] = domain; + }); + }, + ), + ), + ); + }, + ), + ), + ) + .values, + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Scaffold( + appBar: AppBar(title: const Text('Domain Subscriptions')), + body: const DomainsScreen(onlySubbed: true), + ), + ), + ), + child: const Text('All Domain Subs'), + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/src/screens/settings/action_settings.dart b/lib/src/screens/settings/action_settings.dart index f7d434b..3b19411 100644 --- a/lib/src/screens/settings/action_settings.dart +++ b/lib/src/screens/settings/action_settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/api/comments.dart'; import 'package:interstellar/src/screens/feed/feed_screen.dart'; import 'package:interstellar/src/widgets/actions.dart'; +import 'package:interstellar/src/widgets/settings_header.dart'; import 'settings_controller.dart'; @@ -32,11 +33,7 @@ class ActionSettings extends StatelessWidget { body: ListView( padding: const EdgeInsets.symmetric(horizontal: 16), children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text('Feed Actions', - style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Feed Actions'), ActionSettingsItem( metadata: feedActionExpandFab, location: controller.feedActionExpandFab, @@ -72,11 +69,7 @@ class ActionSettings extends StatelessWidget { location: controller.feedActionSetType, setLocation: controller.updateFeedActionSetType, ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text('Defaults', - style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Defaults'), ListTile( title: const Text('Feed Type'), leading: const Icon(Icons.tab), diff --git a/lib/src/screens/settings/general_settings.dart b/lib/src/screens/settings/general_settings.dart index 40d75db..420893e 100644 --- a/lib/src/screens/settings/general_settings.dart +++ b/lib/src/screens/settings/general_settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/utils/language_codes.dart'; import 'package:interstellar/src/utils/themes.dart'; import 'package:interstellar/src/widgets/selection_menu.dart'; +import 'package:interstellar/src/widgets/settings_header.dart'; import 'settings_controller.dart'; @@ -30,11 +31,7 @@ class GeneralScreen extends StatelessWidget { body: ListView( padding: const EdgeInsets.symmetric(horizontal: 16), children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: - Text('Theme', style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Theme'), ListTile( title: const Text('Theme Mode'), leading: const Icon(Icons.brightness_medium), @@ -87,11 +84,7 @@ class GeneralScreen extends StatelessWidget { ), enabled: !controller.useDynamicColor, ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text('Post Appearance', - style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Post Appearance'), ListTile( title: const Text('Image Position'), leading: const Icon(Icons.image), @@ -148,11 +141,7 @@ class GeneralScreen extends StatelessWidget { onChanged: controller.updatePostUseCardPreview, ), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text('Language', - style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Language'), SwitchListTile( title: const Text('Use Account Language Filter'), subtitle: const Text( @@ -222,11 +211,7 @@ class GeneralScreen extends StatelessWidget { children: [Text(getLangName(controller.defaultCreateLang))], ), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: - Text('Other', style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Other'), ListTile( title: const Text('Always Show Instance'), leading: const Icon(Icons.public), diff --git a/lib/src/screens/settings/settings_controller.dart b/lib/src/screens/settings/settings_controller.dart index 368d9f2..deb9e4c 100644 --- a/lib/src/screens/settings/settings_controller.dart +++ b/lib/src/screens/settings/settings_controller.dart @@ -110,6 +110,9 @@ class SettingsController with ChangeNotifier { late String _defaultCreateLang; String get defaultCreateLang => _defaultCreateLang; + late Set _stars; + Set get stars => _stars; + late Map _servers; late Map _accounts; late String _selectedAccount; @@ -211,6 +214,8 @@ class SettingsController with ChangeNotifier { _langFilter = prefs.getStringList("langFilter")?.toSet() ?? {}; _defaultCreateLang = prefs.getString("defaultCreateLang") ?? 'en'; + _stars = prefs.getStringList("stars")?.toSet() ?? {}; + _servers = (jsonDecode(prefs.getString('servers') ?? '{"kbin.earth":{"software":"mbin"}}') as Map) .map((key, value) => MapEntry(key, Server.fromJson(value))); @@ -469,6 +474,34 @@ class SettingsController with ChangeNotifier { await prefs.setString('defaultCreateLang', newValue); } + Future addStar( + String? newStar, + ) async { + if (newStar == null) return; + if (_stars.contains(newStar)) return; + + _stars.add(newStar); + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setStringList('stars', _stars.toList()); + } + + Future removeStar( + String? oldStar, + ) async { + if (oldStar == null) return; + if (!_stars.contains(oldStar)) return; + + _stars.remove(oldStar); + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setStringList('stars', _stars.toList()); + } + Future saveServer(ServerSoftware software, String server) async { if (_servers.containsKey(server) && _servers[server]!.software == software) { diff --git a/lib/src/screens/settings/settings_screen.dart b/lib/src/screens/settings/settings_screen.dart index 51b334c..98c2f65 100644 --- a/lib/src/screens/settings/settings_screen.dart +++ b/lib/src/screens/settings/settings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/screens/settings/action_settings.dart'; import 'package:interstellar/src/screens/settings/general_settings.dart'; import 'package:interstellar/src/screens/settings/login_select.dart'; +import 'package:interstellar/src/widgets/settings_header.dart'; import 'package:provider/provider.dart'; import 'settings_controller.dart'; @@ -44,11 +45,7 @@ class SettingsScreen extends StatelessWidget { ); }, ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: - Text('Presets', style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Presets'), ListTile( title: const Text('Classic Layout'), onTap: () async { @@ -69,11 +66,7 @@ class SettingsScreen extends StatelessWidget { )); }, ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text('Accounts', - style: Theme.of(context).textTheme.titleMedium), - ), + const SettingsHeader('Accounts'), ...(controller.accounts.keys.toList() ..sort((a, b) { final [aLocal, aHost] = a.split('@'); diff --git a/lib/src/widgets/settings_header.dart b/lib/src/widgets/settings_header.dart new file mode 100644 index 0000000..fdc5e16 --- /dev/null +++ b/lib/src/widgets/settings_header.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class SettingsHeader extends StatelessWidget { + final String text; + + const SettingsHeader(this.text, {super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text(text, + style: Theme.of(context) + .textTheme + .titleMedium! + .merge(const TextStyle(fontWeight: FontWeight.w600))), + ); + } +} diff --git a/lib/src/widgets/star_button.dart b/lib/src/widgets/star_button.dart new file mode 100644 index 0000000..0b5ca56 --- /dev/null +++ b/lib/src/widgets/star_button.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:interstellar/src/screens/settings/settings_controller.dart'; +import 'package:provider/provider.dart'; + +class StarButton extends StatelessWidget { + final String name; + + const StarButton( + this.name, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final isStarred = context.watch().stars.contains(name); + + return IconButton( + onPressed: isStarred + ? () => context.read().removeStar(name) + : () => context.read().addStar(name), + icon: context.read().stars.contains(name) + ? const Icon(Icons.star) + : const Icon(Icons.star_border), + color: isStarred ? Colors.yellow : null, + ); + } +}