Implement subscriptions/stars panel

This commit is contained in:
John Wesley 2024-05-06 19:33:48 -04:00
parent 8e0811796c
commit a2d00dadbd
13 changed files with 640 additions and 260 deletions

View File

@ -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<DomainsScreen> {
void initState() {
super.initState();
if (widget.onlySubbed) {
filter = KbinAPIDomainsFilter.subscribed;
}
_pagingController.addPageRequestListener(_fetchPage);
}
@ -61,62 +68,63 @@ class _DomainsScreenState extends State<DomainsScreen> {
),
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<KbinAPIDomainsFilter>(
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<KbinAPIDomainsFilter>(
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<DomainModel>(

View File

@ -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<MagazineScreen> {
@override
Widget build(BuildContext context) {
final globalName = _data == null
? null
: _data!.name.contains('@')
? '!${_data!.name}'
: '!${_data!.name}@${context.watch<SettingsController>().instanceHost}';
return FeedScreen(
source: FeedSource.magazine,
sourceId: widget.magazineId,
@ -87,11 +94,7 @@ class _MagazineScreenState extends State<MagazineScreen> {
),
);
},
child: Text(
_data!.name.contains('@')
? '!${_data!.name}'
: '!${_data!.name}@${context.read<SettingsController>().instanceHost}',
),
child: Text(globalName!),
)
],
),
@ -114,6 +117,7 @@ class _MagazineScreenState extends State<MagazineScreen> {
}
}),
),
StarButton(globalName),
if (whenLoggedIn(context, true) == true)
IconButton(
onPressed: () async {

View File

@ -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<MagazinesScreen> {
void initState() {
super.initState();
if (widget.onlySubbed) {
filter = APIMagazinesFilter.subscribed;
}
_pagingController.addPageRequestListener(_fetchPage);
}
@ -65,108 +72,109 @@ class _MagazinesScreenState extends State<MagazinesScreen> {
),
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: DropdownButton<APIMagazinesFilter>(
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<SettingsController>()
.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<APIMagazinesFilter>(
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<SettingsController>()
.serverSoftware !=
ServerSoftware.lemmy)
const DropdownMenuItem(
value: APIMagazinesFilter.blocked,
child: Text('Blocked'),
),
]) ??
[])
],
),
),
),
...(context.watch<SettingsController>().serverSoftware ==
ServerSoftware.lemmy ||
filter == APIMagazinesFilter.all ||
filter == APIMagazinesFilter.local
? [
Padding(
padding: const EdgeInsets.only(right: 12),
child: DropdownButton<APIMagazinesSort>(
value: sort,
onChanged: (newSort) {
if (newSort != null) {
...(context.watch<SettingsController>().serverSoftware ==
ServerSoftware.lemmy ||
filter == APIMagazinesFilter.all ||
filter == APIMagazinesFilter.local
? [
Padding(
padding: const EdgeInsets.only(right: 12),
child: DropdownButton<APIMagazinesSort>(
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<DetailedMagazineModel>(

View File

@ -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<UserScreen> {
final user = _data!;
final currentFeedSortOption = feedSortSelect.getOption(_sort);
final globalName = user.name.contains('@')
? '@${user.name}'
: '@${user.name}@${context.watch<SettingsController>().instanceHost}';
return Scaffold(
appBar: AppBar(
title: Row(
@ -155,22 +160,23 @@ class _UserScreenState extends State<UserScreen> {
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<UserScreen> {
),
);
},
child: Text(
user.name.contains('@')
? '@${user.name}'
: '@${user.name}@${context.read<SettingsController>().instanceHost}',
),
child: Text(globalName),
)
],
),
@ -236,6 +238,7 @@ class _UserScreenState extends State<UserScreen> {
}
}),
),
StarButton(globalName),
if (whenLoggedIn(context, true) == true)
IconButton(
onPressed: () async {
@ -332,10 +335,9 @@ class _UserScreenState extends State<UserScreen> {
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<UserScreenBody> {
@override
void didUpdateWidget(covariant oldWidget) {
super.didUpdateWidget(oldWidget);
_pagingController.refresh();
if (widget.mode != oldWidget.mode || widget.sort != oldWidget.sort) {
_pagingController.refresh();
}
}
@override

View File

@ -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<UsersScreen> {
void initState() {
super.initState();
if (widget.onlySubbed) {
filter = api_users.UsersFilter.followed;
}
_pagingController.addPageRequestListener(_fetchPage);
}
@ -60,50 +67,51 @@ class _UsersScreenState extends State<UsersScreen> {
),
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<api_users.UsersFilter>(
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<api_users.UsersFilter>(
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<DetailedUserModel>(

View File

@ -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<FeedScreen> {
)
.toList(),
),
drawer: widget.sourceId != null ? null : const NavDrawer(),
),
);
}

View File

@ -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<NavDrawer> createState() => _NavDrawerState();
}
class _NavDrawerState extends State<NavDrawer> {
List<DetailedMagazineModel>? subbedMagazines;
List<DetailedUserModel>? subbedUsers;
List<DomainModel>? subbedDomains;
@override
void initState() {
super.initState();
if (context.read<SettingsController>().isLoggedIn) {
context
.read<SettingsController>()
.api
.magazines
.list(filter: APIMagazinesFilter.subscribed)
.then((value) => setState(() {
if (value.items.isNotEmpty) {
subbedMagazines = value.items;
}
}));
if (context.read<SettingsController>().serverSoftware !=
ServerSoftware.lemmy) {
context
.read<SettingsController>()
.api
.users
.list(filter: UsersFilter.followed)
.then((value) => setState(() {
if (value.items.isNotEmpty) {
subbedUsers = value.items;
}
}));
context
.read<SettingsController>()
.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<SettingsController>().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<SettingsController>().stars.toList()..sort()).map(
(star) => ListTile(
title: Text(star),
onTap: () async {
String name = star.substring(1);
if (name.endsWith(
context.read<SettingsController>().instanceHost)) {
name = name.split('@').first;
}
switch (star[0]) {
case '@':
final user = await context
.read<SettingsController>()
.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<SettingsController>()
.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<SettingsController>().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<SettingsController>().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<SettingsController>().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<SettingsController>().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<SettingsController>().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'),
),
),
],
],
),
);
}
}

View File

@ -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),

View File

@ -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),

View File

@ -110,6 +110,9 @@ class SettingsController with ChangeNotifier {
late String _defaultCreateLang;
String get defaultCreateLang => _defaultCreateLang;
late Set<String> _stars;
Set<String> get stars => _stars;
late Map<String, Server> _servers;
late Map<String, Account> _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<String, dynamic>)
.map((key, value) => MapEntry(key, Server.fromJson(value)));
@ -469,6 +474,34 @@ class SettingsController with ChangeNotifier {
await prefs.setString('defaultCreateLang', newValue);
}
Future<void> 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<void> 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<void> saveServer(ServerSoftware software, String server) async {
if (_servers.containsKey(server) &&
_servers[server]!.software == software) {

View File

@ -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('@');

View File

@ -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))),
);
}
}

View File

@ -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<SettingsController>().stars.contains(name);
return IconButton(
onPressed: isStarred
? () => context.read<SettingsController>().removeStar(name)
: () => context.read<SettingsController>().addStar(name),
icon: context.read<SettingsController>().stars.contains(name)
? const Icon(Icons.star)
: const Icon(Icons.star_border),
color: isStarred ? Colors.yellow : null,
);
}
}