Merge pull request #218 from krawieck/feature/more-settings
This commit is contained in:
commit
af8e88702d
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Show avatars setting toggle
|
||||||
|
- Show scores setting toggle
|
||||||
|
- Default listing type for the home tab setting
|
||||||
|
- Import Lemmy settings: long press an account in account settings then choose the import option
|
||||||
- Editing posts
|
- Editing posts
|
||||||
- Editing comments
|
- Editing comments
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,10 @@ class CommunitiesTab extends HookWidget {
|
||||||
onTap: () => goToInstance(context,
|
onTap: () => goToInstance(context,
|
||||||
accountsStore.loggedInInstances.elementAt(i)),
|
accountsStore.loggedInInstances.elementAt(i)),
|
||||||
onLongPress: () => toggleCollapse(i),
|
onLongPress: () => toggleCollapse(i),
|
||||||
leading: Avatar(url: instances[i].icon),
|
leading: Avatar(
|
||||||
|
url: instances[i].icon,
|
||||||
|
alwaysShow: true,
|
||||||
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
instances[i].name,
|
instances[i].name,
|
||||||
style: theme.textTheme.headline6,
|
style: theme.textTheme.headline6,
|
||||||
|
@ -211,6 +214,7 @@ class CommunitiesTab extends HookWidget {
|
||||||
Avatar(
|
Avatar(
|
||||||
radius: 15,
|
radius: 15,
|
||||||
url: comm.community.icon,
|
url: comm.community.icon,
|
||||||
|
alwaysShow: true,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(comm.community.originDisplayName),
|
Text(comm.community.originDisplayName),
|
||||||
|
|
|
@ -257,6 +257,7 @@ class _CommunityOverview extends StatelessWidget {
|
||||||
child: Avatar(
|
child: Avatar(
|
||||||
url: community.community.icon,
|
url: community.community.icon,
|
||||||
radius: 83 / 2,
|
radius: 83 / 2,
|
||||||
|
alwaysShow: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -25,10 +25,13 @@ class HomeTab extends HookWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final accStore = useAccountsStore();
|
final accStore = useAccountsStore();
|
||||||
|
final defaultListingType =
|
||||||
|
useConfigStoreSelect((configStore) => configStore.defaultListingType);
|
||||||
final selectedList = useState(_SelectedList(
|
final selectedList = useState(_SelectedList(
|
||||||
listingType: accStore.hasNoAccount
|
listingType: accStore.hasNoAccount &&
|
||||||
|
defaultListingType == PostListingType.subscribed
|
||||||
? PostListingType.all
|
? PostListingType.all
|
||||||
: PostListingType.subscribed));
|
: defaultListingType));
|
||||||
final isc = useInfiniteScrollController();
|
final isc = useInfiniteScrollController();
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final instancesIcons = useMemoFuture(() async {
|
final instancesIcons = useMemoFuture(() async {
|
||||||
|
@ -54,9 +57,10 @@ class HomeTab extends HookWidget {
|
||||||
selectedList.value.listingType == PostListingType.subscribed ||
|
selectedList.value.listingType == PostListingType.subscribed ||
|
||||||
!accStore.instances.contains(selectedList.value.instanceHost)) {
|
!accStore.instances.contains(selectedList.value.instanceHost)) {
|
||||||
selectedList.value = _SelectedList(
|
selectedList.value = _SelectedList(
|
||||||
listingType: accStore.hasNoAccount
|
listingType: accStore.hasNoAccount &&
|
||||||
|
defaultListingType == PostListingType.subscribed
|
||||||
? PostListingType.all
|
? PostListingType.all
|
||||||
: PostListingType.subscribed,
|
: defaultListingType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,17 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:lemmy_api_client/pictrs.dart';
|
import 'package:lemmy_api_client/pictrs.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/image_picker.dart';
|
import '../hooks/image_picker.dart';
|
||||||
import '../hooks/ref.dart';
|
import '../hooks/ref.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../util/more_icon.dart';
|
||||||
import '../util/pictrs.dart';
|
import '../util/pictrs.dart';
|
||||||
|
import '../widgets/bottom_modal.dart';
|
||||||
import '../widgets/bottom_safe.dart';
|
import '../widgets/bottom_safe.dart';
|
||||||
import '../widgets/radio_picker.dart';
|
|
||||||
|
|
||||||
/// Page for managing things like username, email, avatar etc
|
/// Page for managing things like username, email, avatar etc
|
||||||
/// This page will assume the manage account is logged in and
|
/// This page will assume the manage account is logged in and
|
||||||
|
@ -34,9 +36,39 @@ class ManageAccountPage extends HookWidget {
|
||||||
return site.myUser!;
|
return site.myUser!;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void _openMoreMenu() {
|
||||||
|
showBottomModal(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.open_in_browser),
|
||||||
|
title: const Text('Open in browser'),
|
||||||
|
onTap: () async {
|
||||||
|
final userProfileUrl =
|
||||||
|
await userFuture.then((e) => e.person.actorId);
|
||||||
|
|
||||||
|
if (await ul.canLaunch(userProfileUrl)) {
|
||||||
|
await ul.launch(userProfileUrl);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("can't open in browser")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('$username@$instanceHost'),
|
title: Text('$username@$instanceHost'),
|
||||||
|
actions: [
|
||||||
|
IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: FutureBuilder<LocalUserSettingsView>(
|
body: FutureBuilder<LocalUserSettingsView>(
|
||||||
future: userFuture,
|
future: userFuture,
|
||||||
|
@ -76,12 +108,8 @@ class _ManageAccount extends HookWidget {
|
||||||
useTextEditingController(text: user.person.matrixUserId);
|
useTextEditingController(text: user.person.matrixUserId);
|
||||||
final avatar = useRef(user.person.avatar);
|
final avatar = useRef(user.person.avatar);
|
||||||
final banner = useRef(user.person.banner);
|
final banner = useRef(user.person.banner);
|
||||||
final showAvatars = useState(user.localUser.showAvatars);
|
|
||||||
final showNsfw = useState(user.localUser.showNsfw);
|
|
||||||
final sendNotificationsToEmail =
|
final sendNotificationsToEmail =
|
||||||
useState(user.localUser.sendNotificationsToEmail);
|
useState(user.localUser.sendNotificationsToEmail);
|
||||||
final defaultListingType = useState(user.localUser.defaultListingType);
|
|
||||||
final defaultSortType = useState(user.localUser.defaultSortType);
|
|
||||||
final newPasswordController = useTextEditingController();
|
final newPasswordController = useTextEditingController();
|
||||||
final newPasswordVerifyController = useTextEditingController();
|
final newPasswordVerifyController = useTextEditingController();
|
||||||
final oldPasswordController = useTextEditingController();
|
final oldPasswordController = useTextEditingController();
|
||||||
|
@ -106,12 +134,12 @@ class _ManageAccount extends HookWidget {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await LemmyApiV3(user.instanceHost).run(SaveUserSettings(
|
await LemmyApiV3(user.instanceHost).run(SaveUserSettings(
|
||||||
showNsfw: showNsfw.value,
|
showNsfw: user.localUser.showNsfw,
|
||||||
theme: user.localUser.theme,
|
theme: user.localUser.theme,
|
||||||
defaultSortType: defaultSortType.value,
|
defaultSortType: user.localUser.defaultSortType,
|
||||||
defaultListingType: defaultListingType.value,
|
defaultListingType: user.localUser.defaultListingType,
|
||||||
lang: user.localUser.lang,
|
lang: user.localUser.lang,
|
||||||
showAvatars: showAvatars.value,
|
showAvatars: user.localUser.showAvatars,
|
||||||
sendNotificationsToEmail: sendNotificationsToEmail.value,
|
sendNotificationsToEmail: sendNotificationsToEmail.value,
|
||||||
auth: token.raw,
|
auth: token.raw,
|
||||||
avatar: avatar.current,
|
avatar: avatar.current,
|
||||||
|
@ -290,78 +318,10 @@ class _ManageAccount extends HookWidget {
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
SwitchListTile.adaptive(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(L10n.of(context)!.type),
|
|
||||||
const Text(
|
|
||||||
'This has currently no effect on lemmur',
|
|
||||||
style: TextStyle(fontSize: 10),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
RadioPicker<PostListingType>(
|
|
||||||
values: const [
|
|
||||||
PostListingType.all,
|
|
||||||
PostListingType.local,
|
|
||||||
PostListingType.subscribed,
|
|
||||||
],
|
|
||||||
groupValue: defaultListingType.value,
|
|
||||||
onChanged: (value) => defaultListingType.value = value,
|
|
||||||
mapValueToString: (value) => value.value,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(L10n.of(context)!.sort_type),
|
|
||||||
const Text(
|
|
||||||
'This has currently no effect on lemmur',
|
|
||||||
style: TextStyle(fontSize: 10),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
RadioPicker<SortType>(
|
|
||||||
values: SortType.values,
|
|
||||||
groupValue: defaultSortType.value,
|
|
||||||
onChanged: (value) => defaultSortType.value = value,
|
|
||||||
mapValueToString: (value) => value.value,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
CheckboxListTile(
|
|
||||||
value: showAvatars.value,
|
|
||||||
onChanged: (checked) {
|
|
||||||
if (checked != null) showAvatars.value = checked;
|
|
||||||
},
|
|
||||||
title: Text(L10n.of(context)!.show_avatars),
|
|
||||||
subtitle: const Text('This has currently no effect on lemmur'),
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
CheckboxListTile(
|
|
||||||
value: showNsfw.value,
|
|
||||||
onChanged: (checked) {
|
|
||||||
if (checked != null) showNsfw.value = checked;
|
|
||||||
},
|
|
||||||
title: Text(L10n.of(context)!.show_nsfw),
|
|
||||||
subtitle: const Text('This has currently no effect on lemmur'),
|
|
||||||
dense: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
CheckboxListTile(
|
|
||||||
value: sendNotificationsToEmail.value,
|
value: sendNotificationsToEmail.value,
|
||||||
onChanged: (checked) {
|
onChanged: (checked) {
|
||||||
if (checked != null) sendNotificationsToEmail.value = checked;
|
sendNotificationsToEmail.value = checked;
|
||||||
},
|
},
|
||||||
title: Text(L10n.of(context)!.send_notifications_to_email),
|
title: Text(L10n.of(context)!.send_notifications_to_email),
|
||||||
dense: true,
|
dense: true,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
@ -25,6 +26,13 @@ class SettingsPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: const Text('General'),
|
||||||
|
onTap: () {
|
||||||
|
goTo(context, (_) => const GeneralConfigPage());
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person),
|
leading: const Icon(Icons.person),
|
||||||
title: const Text('Accounts'),
|
title: const Text('Accounts'),
|
||||||
|
@ -54,9 +62,7 @@ class AppearanceConfigPage extends HookWidget {
|
||||||
final configStore = useConfigStore();
|
final configStore = useConfigStore();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: const Text('Appearance')),
|
||||||
title: const Text('Appearance'),
|
|
||||||
),
|
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
const _SectionHeading('Theme'),
|
const _SectionHeading('Theme'),
|
||||||
|
@ -69,7 +75,7 @@ class AppearanceConfigPage extends HookWidget {
|
||||||
if (selected != null) configStore.theme = selected;
|
if (selected != null) configStore.theme = selected;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile.adaptive(
|
||||||
title: const Text('AMOLED dark mode'),
|
title: const Text('AMOLED dark mode'),
|
||||||
value: configStore.amoledDarkMode,
|
value: configStore.amoledDarkMode,
|
||||||
onChanged: (checked) {
|
onChanged: (checked) {
|
||||||
|
@ -77,7 +83,67 @@ class AppearanceConfigPage extends HookWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const _SectionHeading('General'),
|
const _SectionHeading('Other'),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: Text(L10n.of(context)!.show_avatars),
|
||||||
|
value: configStore.showAvatars,
|
||||||
|
onChanged: (checked) {
|
||||||
|
configStore.showAvatars = checked;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: const Text('Show scores'),
|
||||||
|
value: configStore.showScores,
|
||||||
|
onChanged: (checked) {
|
||||||
|
configStore.showScores = checked;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// General settings
|
||||||
|
class GeneralConfigPage extends HookWidget {
|
||||||
|
const GeneralConfigPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final configStore = useConfigStore();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('General')),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10n.of(context)!.sort_type),
|
||||||
|
trailing: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: RadioPicker<SortType>(
|
||||||
|
values: SortType.values,
|
||||||
|
groupValue: configStore.defaultSortType,
|
||||||
|
onChanged: (value) => configStore.defaultSortType = value,
|
||||||
|
mapValueToString: (value) => value.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10n.of(context)!.type),
|
||||||
|
trailing: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: RadioPicker<PostListingType>(
|
||||||
|
values: const [
|
||||||
|
PostListingType.all,
|
||||||
|
PostListingType.local,
|
||||||
|
PostListingType.subscribed,
|
||||||
|
],
|
||||||
|
groupValue: configStore.defaultListingType,
|
||||||
|
onChanged: (value) => configStore.defaultListingType = value,
|
||||||
|
mapValueToString: (value) => value.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10n.of(context)!.language),
|
title: Text(L10n.of(context)!.language),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
|
@ -93,12 +159,110 @@ class AppearanceConfigPage extends HookWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: Text(L10n.of(context)!.show_nsfw),
|
||||||
|
value: configStore.showNsfw,
|
||||||
|
onChanged: (checked) {
|
||||||
|
configStore.showNsfw = checked;
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Popup for an account
|
||||||
|
class _AccountOptions extends HookWidget {
|
||||||
|
final String instanceHost;
|
||||||
|
final String username;
|
||||||
|
|
||||||
|
const _AccountOptions({
|
||||||
|
Key? key,
|
||||||
|
required this.instanceHost,
|
||||||
|
required this.username,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final accountsStore = useAccountsStore();
|
||||||
|
final configStore = useConfigStore();
|
||||||
|
final importLoading = useState(false);
|
||||||
|
|
||||||
|
Future<void> removeUserDialog(String instanceHost, String username) async {
|
||||||
|
if (await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Remove user?'),
|
||||||
|
content: Text(
|
||||||
|
'Are you sure you want to remove $username@$instanceHost?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text(L10n.of(context)!.no),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: Text(L10n.of(context)!.yes),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false) {
|
||||||
|
await accountsStore.removeAccount(instanceHost, username);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (accountsStore.defaultUsernameFor(instanceHost) != username)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.check_circle_outline),
|
||||||
|
title: const Text('Set as default'),
|
||||||
|
onTap: () {
|
||||||
|
accountsStore.setDefaultAccountFor(instanceHost, username);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete),
|
||||||
|
title: const Text('Remove account'),
|
||||||
|
onTap: () => removeUserDialog(instanceHost, username),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: importLoading.value
|
||||||
|
? const SizedBox(
|
||||||
|
height: 25,
|
||||||
|
width: 25,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.cloud_download),
|
||||||
|
title: const Text('Import settings to lemmur'),
|
||||||
|
onTap: () async {
|
||||||
|
importLoading.value = true;
|
||||||
|
try {
|
||||||
|
await configStore.importLemmyUserSettings(
|
||||||
|
accountsStore.userDataFor(instanceHost, username)!.jwt,
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('Import successful'),
|
||||||
|
));
|
||||||
|
} on Exception catch (err) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(err.toString()),
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
importLoading.value = false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Settings for managing accounts
|
/// Settings for managing accounts
|
||||||
class AccountsConfigPage extends HookWidget {
|
class AccountsConfigPage extends HookWidget {
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
|
||||||
|
@ -132,51 +296,12 @@ class AccountsConfigPage extends HookWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeUserDialog(String instanceHost, String username) async {
|
|
||||||
if (await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('Remove user?'),
|
|
||||||
content: Text(
|
|
||||||
'Are you sure you want to remove $username@$instanceHost?'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: Text(L10n.of(context)!.no),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text(L10n.of(context)!.yes),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
) ??
|
|
||||||
false) {
|
|
||||||
await accountsStore.removeAccount(instanceHost, username);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void accountActions(String instanceHost, String username) {
|
void accountActions(String instanceHost, String username) {
|
||||||
showBottomModal(
|
showBottomModal(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Column(
|
builder: (context) => _AccountOptions(
|
||||||
children: [
|
instanceHost: instanceHost,
|
||||||
if (accountsStore.defaultUsernameFor(instanceHost) != username)
|
username: username,
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.check_circle_outline),
|
|
||||||
title: const Text('Set as default'),
|
|
||||||
onTap: () {
|
|
||||||
accountsStore.setDefaultAccountFor(instanceHost, username);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.delete),
|
|
||||||
title: const Text('Remove account'),
|
|
||||||
onTap: () => removeUserDialog(instanceHost, username),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
@ -44,6 +45,90 @@ class ConfigStore extends ChangeNotifier {
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late bool _showAvatars;
|
||||||
|
@JsonKey(defaultValue: true)
|
||||||
|
bool get showAvatars => _showAvatars;
|
||||||
|
set showAvatars(bool showAvatars) {
|
||||||
|
_showAvatars = showAvatars;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
late bool _showNsfw;
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
bool get showNsfw => _showNsfw;
|
||||||
|
set showNsfw(bool showNsfw) {
|
||||||
|
_showNsfw = showNsfw;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
late bool _showScores;
|
||||||
|
@JsonKey(defaultValue: true)
|
||||||
|
bool get showScores => _showScores;
|
||||||
|
set showScores(bool showScores) {
|
||||||
|
_showScores = showScores;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
late SortType _defaultSortType;
|
||||||
|
// default is set in fromJson
|
||||||
|
@JsonKey(fromJson: _sortTypeFromJson)
|
||||||
|
SortType get defaultSortType => _defaultSortType;
|
||||||
|
set defaultSortType(SortType defaultSortType) {
|
||||||
|
_defaultSortType = defaultSortType;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
late PostListingType _defaultListingType;
|
||||||
|
// default is set in fromJson
|
||||||
|
@JsonKey(fromJson: _postListingTypeFromJson)
|
||||||
|
PostListingType get defaultListingType => _defaultListingType;
|
||||||
|
set defaultListingType(PostListingType defaultListingType) {
|
||||||
|
_defaultListingType = defaultListingType;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies over settings from lemmy to [ConfigStore]
|
||||||
|
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
|
||||||
|
// themes from lemmy-ui that are dark mode
|
||||||
|
// const darkModeLemmyUiThemes = {
|
||||||
|
// 'solar',
|
||||||
|
// 'cyborg',
|
||||||
|
// 'darkly',
|
||||||
|
// 'vaporwave-dark',
|
||||||
|
// // TODO: is it dark theme?
|
||||||
|
// 'i386',
|
||||||
|
// };
|
||||||
|
|
||||||
|
_showAvatars = localUserSettings.showAvatars;
|
||||||
|
_showNsfw = localUserSettings.showNsfw;
|
||||||
|
// TODO: should these also be imported? If so, how?
|
||||||
|
// _theme = darkModeLemmyUiThemes.contains(localUserSettings.theme)
|
||||||
|
// ? ThemeMode.dark
|
||||||
|
// : ThemeMode.light;
|
||||||
|
// _locale = L10n.supportedLocales.contains(Locale(localUserSettings.lang))
|
||||||
|
// ? Locale(localUserSettings.lang)
|
||||||
|
// : _locale;
|
||||||
|
// TODO: add when it is released
|
||||||
|
// _showScores = localUserSettings.showScores;
|
||||||
|
_defaultSortType = localUserSettings.defaultSortType;
|
||||||
|
_defaultListingType = localUserSettings.defaultListingType;
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches [LocalUserSettings] and imports them with [.copyLemmyUserSettings]
|
||||||
|
Future<void> importLemmyUserSettings(Jwt token) async {
|
||||||
|
final site =
|
||||||
|
await LemmyApiV3(token.payload.iss).run(GetSite(auth: token.raw));
|
||||||
|
copyLemmyUserSettings(site.myUser!.localUser);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<ConfigStore> load() async {
|
static Future<ConfigStore> load() async {
|
||||||
final prefs = await _prefs;
|
final prefs = await _prefs;
|
||||||
|
|
||||||
|
@ -58,3 +143,8 @@ class ConfigStore extends ChangeNotifier {
|
||||||
await prefs.setString(prefsKey, jsonEncode(_$ConfigStoreToJson(this)));
|
await prefs.setString(prefsKey, jsonEncode(_$ConfigStoreToJson(this)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SortType _sortTypeFromJson(String? json) =>
|
||||||
|
json != null ? SortType.fromJson(json) : SortType.hot;
|
||||||
|
PostListingType _postListingTypeFromJson(String? json) =>
|
||||||
|
json != null ? PostListingType.fromJson(json) : PostListingType.all;
|
||||||
|
|
|
@ -11,7 +11,13 @@ ConfigStore _$ConfigStoreFromJson(Map<String, dynamic> json) {
|
||||||
..theme = _$enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ??
|
..theme = _$enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ??
|
||||||
ThemeMode.system
|
ThemeMode.system
|
||||||
..amoledDarkMode = json['amoledDarkMode'] as bool? ?? false
|
..amoledDarkMode = json['amoledDarkMode'] as bool? ?? false
|
||||||
..locale = LocaleSerde.fromJson(json['locale'] as String?);
|
..locale = LocaleSerde.fromJson(json['locale'] as String?)
|
||||||
|
..showAvatars = json['showAvatars'] as bool? ?? true
|
||||||
|
..showNsfw = json['showNsfw'] as bool? ?? false
|
||||||
|
..showScores = json['showScores'] as bool? ?? true
|
||||||
|
..defaultSortType = _sortTypeFromJson(json['defaultSortType'] as String?)
|
||||||
|
..defaultListingType =
|
||||||
|
_postListingTypeFromJson(json['defaultListingType'] as String?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
|
Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
|
||||||
|
@ -19,6 +25,11 @@ Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
|
||||||
'theme': _$ThemeModeEnumMap[instance.theme],
|
'theme': _$ThemeModeEnumMap[instance.theme],
|
||||||
'amoledDarkMode': instance.amoledDarkMode,
|
'amoledDarkMode': instance.amoledDarkMode,
|
||||||
'locale': LocaleSerde.toJson(instance.locale),
|
'locale': LocaleSerde.toJson(instance.locale),
|
||||||
|
'showAvatars': instance.showAvatars,
|
||||||
|
'showNsfw': instance.showNsfw,
|
||||||
|
'showScores': instance.showScores,
|
||||||
|
'defaultSortType': instance.defaultSortType,
|
||||||
|
'defaultListingType': instance.defaultListingType,
|
||||||
};
|
};
|
||||||
|
|
||||||
K _$enumDecode<K, V>(
|
K _$enumDecode<K, V>(
|
||||||
|
|
|
@ -1,23 +1,34 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
/// User's avatar.
|
import '../hooks/stores.dart';
|
||||||
|
|
||||||
|
/// User's avatar. Respects the `showAvatars` setting from configStore
|
||||||
/// If passed url is null, a blank box is displayed to prevent weird indents
|
/// If passed url is null, a blank box is displayed to prevent weird indents
|
||||||
/// Can be disabled with `noBlank`
|
/// Can be disabled with `noBlank`
|
||||||
class Avatar extends StatelessWidget {
|
class Avatar extends HookWidget {
|
||||||
const Avatar({
|
const Avatar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.url,
|
required this.url,
|
||||||
this.radius = 25,
|
this.radius = 25,
|
||||||
this.noBlank = false,
|
this.noBlank = false,
|
||||||
|
this.alwaysShow = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String? url;
|
final String? url;
|
||||||
final double radius;
|
final double radius;
|
||||||
final bool noBlank;
|
final bool noBlank;
|
||||||
|
|
||||||
|
/// Overrides the `showAvatars` setting
|
||||||
|
final bool alwaysShow;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showAvatars =
|
||||||
|
useConfigStoreSelect((configStore) => configStore.showAvatars) ||
|
||||||
|
alwaysShow;
|
||||||
|
|
||||||
final blankWidget = () {
|
final blankWidget = () {
|
||||||
if (noBlank) return const SizedBox.shrink();
|
if (noBlank) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
@ -29,7 +40,7 @@ class Avatar extends StatelessWidget {
|
||||||
|
|
||||||
final imageUrl = url;
|
final imageUrl = url;
|
||||||
|
|
||||||
if (imageUrl == null) {
|
if (imageUrl == null || !showAvatars) {
|
||||||
return blankWidget;
|
return blankWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,8 @@ class CommentWidget extends HookWidget {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final accStore = useAccountsStore();
|
final accStore = useAccountsStore();
|
||||||
|
final showScores =
|
||||||
|
useConfigStoreSelect((configStore) => configStore.showScores);
|
||||||
|
|
||||||
final isMine = commentTree.comment.comment.creatorId ==
|
final isMine = commentTree.comment.comment.creatorId ==
|
||||||
accStore.defaultUserDataFor(commentTree.comment.instanceHost)?.userId;
|
accStore.defaultUserDataFor(commentTree.comment.instanceHost)?.userId;
|
||||||
|
@ -412,10 +414,13 @@ class CommentWidget extends HookWidget {
|
||||||
SizedBox.fromSize(
|
SizedBox.fromSize(
|
||||||
size: const Size.square(16),
|
size: const Size.square(16),
|
||||||
child: const CircularProgressIndicator())
|
child: const CircularProgressIndicator())
|
||||||
else
|
else if (showScores)
|
||||||
Text(compactNumber(comment.counts.score +
|
Text(compactNumber(comment.counts.score +
|
||||||
(wasVoted ? 0 : myVote.value.value))),
|
(wasVoted ? 0 : myVote.value.value))),
|
||||||
const Text(' · '),
|
if (showScores)
|
||||||
|
const Text(' · ')
|
||||||
|
else
|
||||||
|
const SizedBox(width: 4),
|
||||||
Text(comment.comment.published.fancy),
|
Text(comment.comment.published.fancy),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/logged_in_action.dart';
|
import '../hooks/logged_in_action.dart';
|
||||||
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../pages/create_post.dart';
|
import '../pages/create_post.dart';
|
||||||
import '../pages/full_post.dart';
|
import '../pages/full_post.dart';
|
||||||
|
@ -536,6 +537,8 @@ class _Voting extends HookWidget {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final myVote = useState(post.myVote ?? VoteType.none);
|
final myVote = useState(post.myVote ?? VoteType.none);
|
||||||
final loading = useDelayedLoading();
|
final loading = useDelayedLoading();
|
||||||
|
final showScores =
|
||||||
|
useConfigStoreSelect((configStore) => configStore.showScores);
|
||||||
final loggedInAction = useLoggedInAction(post.instanceHost);
|
final loggedInAction = useLoggedInAction(post.instanceHost);
|
||||||
|
|
||||||
vote(VoteType vote, Jwt token) async {
|
vote(VoteType vote, Jwt token) async {
|
||||||
|
@ -567,11 +570,12 @@ class _Voting extends HookWidget {
|
||||||
myVote.value == VoteType.up ? VoteType.none : VoteType.up,
|
myVote.value == VoteType.up ? VoteType.none : VoteType.up,
|
||||||
token,
|
token,
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
if (loading.loading)
|
if (loading.loading)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 20, height: 20, child: CircularProgressIndicator())
|
width: 20, height: 20, child: CircularProgressIndicator())
|
||||||
else
|
else if (showScores)
|
||||||
Text(NumberFormat.compact()
|
Text(NumberFormat.compact()
|
||||||
.format(post.counts.score + (wasVoted ? 0 : myVote.value.value))),
|
.format(post.counts.score + (wasVoted ? 0 : myVote.value.value))),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
Loading…
Reference in New Issue