From 7138f9b76c8ddda897ab658f6974024551a482c0 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 20:15:48 +0200 Subject: [PATCH 01/15] Add settings --- lib/stores/config_store.dart | 59 ++++++++++++++++++++++++++++++++++ lib/stores/config_store.g.dart | 8 ++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 33b4a0fc..366905f7 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:lemmy_api_client/v3.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../l10n/l10n.dart'; @@ -44,6 +45,64 @@ class ConfigStore extends ChangeNotifier { 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(); + } + + void importLemmyUserSettings(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; + + // TODO: should these even be supported? Or we should use our own per-community setting + // SortType defaultSortType + // PostListingType defaultListingType + + notifyListeners(); + save(); + } + static Future load() async { final prefs = await _prefs; diff --git a/lib/stores/config_store.g.dart b/lib/stores/config_store.g.dart index af41b4ac..e75ecadb 100644 --- a/lib/stores/config_store.g.dart +++ b/lib/stores/config_store.g.dart @@ -11,7 +11,10 @@ ConfigStore _$ConfigStoreFromJson(Map json) { ..theme = _$enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ?? ThemeMode.system ..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; } Map _$ConfigStoreToJson(ConfigStore instance) => @@ -19,6 +22,9 @@ Map _$ConfigStoreToJson(ConfigStore instance) => 'theme': _$ThemeModeEnumMap[instance.theme], 'amoledDarkMode': instance.amoledDarkMode, 'locale': LocaleSerde.toJson(instance.locale), + 'showAvatars': instance.showAvatars, + 'showNsfw': instance.showNsfw, + 'showScores': instance.showScores, }; K _$enumDecode( From ef7ba71bccf2922df23a84be2f11e9a410a3d436 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 20:32:20 +0200 Subject: [PATCH 02/15] Implement show scores --- lib/widgets/comment.dart | 9 +++++++-- lib/widgets/post.dart | 24 ++++++++++++++---------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/widgets/comment.dart b/lib/widgets/comment.dart index cbe5b588..2650fe7e 100644 --- a/lib/widgets/comment.dart +++ b/lib/widgets/comment.dart @@ -93,6 +93,8 @@ class CommentWidget extends HookWidget { final theme = Theme.of(context); final accStore = useAccountsStore(); + final showScores = + useConfigStoreSelect((configStore) => configStore.showScores); final isMine = commentTree.comment.comment.creatorId == accStore.defaultUserDataFor(commentTree.comment.instanceHost)?.userId; @@ -387,10 +389,13 @@ class CommentWidget extends HookWidget { SizedBox.fromSize( size: const Size.square(16), child: const CircularProgressIndicator()) - else + else if (showScores) Text(compactNumber(comment.counts.score + (wasVoted ? 0 : myVote.value.value))), - const Text(' · '), + if (showScores) + const Text(' · ') + else + const SizedBox(width: 4), Text(comment.comment.published.fancy), ], ), diff --git a/lib/widgets/post.dart b/lib/widgets/post.dart index daa6f861..d35b7f24 100644 --- a/lib/widgets/post.dart +++ b/lib/widgets/post.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart' as ul; import '../hooks/delayed_loading.dart'; import '../hooks/logged_in_action.dart'; +import '../hooks/stores.dart'; import '../l10n/l10n.dart'; import '../pages/full_post.dart'; import '../url_launcher.dart'; @@ -496,6 +497,8 @@ class _Voting extends HookWidget { final theme = Theme.of(context); final myVote = useState(post.myVote ?? VoteType.none); final loading = useDelayedLoading(); + final showScores = + useConfigStoreSelect((configStore) => configStore.showScores); final loggedInAction = useLoggedInAction(post.instanceHost); vote(VoteType vote, Jwt token) async { @@ -518,20 +521,21 @@ class _Voting extends HookWidget { return Row( children: [ IconButton( - icon: Icon( - Icons.arrow_upward, - color: myVote.value == VoteType.up ? theme.accentColor : null, + icon: Icon( + Icons.arrow_upward, + color: myVote.value == VoteType.up ? theme.accentColor : null, + ), + onPressed: loggedInAction( + (token) => vote( + myVote.value == VoteType.up ? VoteType.none : VoteType.up, + token, ), - onPressed: loggedInAction( - (token) => vote( - myVote.value == VoteType.up ? VoteType.none : VoteType.up, - token, - ), - )), + ), + ), if (loading.loading) const SizedBox( width: 20, height: 20, child: CircularProgressIndicator()) - else + else if (showScores) Text(NumberFormat.compact() .format(post.counts.score + (wasVoted ? 0 : myVote.value.value))), IconButton( From 600d52211e7a29eea02d88b24a7c400b4e75f7dd Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 20:32:25 +0200 Subject: [PATCH 03/15] Add settings --- lib/pages/settings.dart | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index fc557573..9ae92605 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -69,7 +69,7 @@ class AppearanceConfigPage extends HookWidget { if (selected != null) configStore.theme = selected; }, ), - SwitchListTile( + SwitchListTile.adaptive( title: const Text('AMOLED dark mode'), value: configStore.amoledDarkMode, onChanged: (checked) { @@ -78,6 +78,27 @@ class AppearanceConfigPage extends HookWidget { ), const SizedBox(height: 12), const _SectionHeading('General'), + SwitchListTile.adaptive( + title: Text(L10n.of(context)!.show_nsfw), + value: configStore.showNsfw, + onChanged: (checked) { + configStore.showNsfw = checked; + }, + ), + 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; + }, + ), ListTile( title: Text(L10n.of(context)!.language), trailing: SizedBox( From 01e9dae0ccfc1d1f150a07e2d22ff5cf393cf7ba Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 20:41:33 +0200 Subject: [PATCH 04/15] Implement showAvatars --- lib/pages/communities_tab.dart | 6 +++++- lib/pages/community.dart | 1 + lib/widgets/avatar.dart | 17 ++++++++++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/pages/communities_tab.dart b/lib/pages/communities_tab.dart index 12db2611..cc210bce 100644 --- a/lib/pages/communities_tab.dart +++ b/lib/pages/communities_tab.dart @@ -180,7 +180,10 @@ class CommunitiesTab extends HookWidget { onTap: () => goToInstance(context, accountsStore.loggedInInstances.elementAt(i)), onLongPress: () => toggleCollapse(i), - leading: Avatar(url: instances[i].icon), + leading: Avatar( + url: instances[i].icon, + alwaysShow: true, + ), title: Text( instances[i].name, style: theme.textTheme.headline6, @@ -211,6 +214,7 @@ class CommunitiesTab extends HookWidget { Avatar( radius: 15, url: comm.community.icon, + alwaysShow: true, ), const SizedBox(width: 10), Text(comm.community.originDisplayName), diff --git a/lib/pages/community.dart b/lib/pages/community.dart index bb24dbac..b5958693 100644 --- a/lib/pages/community.dart +++ b/lib/pages/community.dart @@ -257,6 +257,7 @@ class _CommunityOverview extends StatelessWidget { child: Avatar( url: community.community.icon, radius: 83 / 2, + alwaysShow: true, ), ), ], diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 62d477cb..e25ff82b 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -1,23 +1,34 @@ import 'package:cached_network_image/cached_network_image.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 /// Can be disabled with `noBlank` -class Avatar extends StatelessWidget { +class Avatar extends HookWidget { const Avatar({ Key? key, required this.url, this.radius = 25, this.noBlank = false, + this.alwaysShow = false, }) : super(key: key); final String? url; final double radius; final bool noBlank; + /// Overrides the `showAvatars` setting + final bool alwaysShow; + @override Widget build(BuildContext context) { + final showAvatars = + useConfigStoreSelect((configStore) => configStore.showAvatars) || + alwaysShow; + final blankWidget = () { if (noBlank) return const SizedBox.shrink(); @@ -29,7 +40,7 @@ class Avatar extends StatelessWidget { final imageUrl = url; - if (imageUrl == null) { + if (imageUrl == null || !showAvatars) { return blankWidget; } From c371c3f4fb4bad244728d16e9a251eaf3b2bfdf4 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 21:19:59 +0200 Subject: [PATCH 05/15] Add settings import --- lib/pages/settings.dart | 113 +++++++++++++++++++++++++---------- lib/stores/config_store.dart | 10 +++- 2 files changed, 92 insertions(+), 31 deletions(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 9ae92605..d3ebae6a 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -120,21 +122,31 @@ class AppearanceConfigPage extends HookWidget { } } -/// Settings for managing accounts -class AccountsConfigPage extends HookWidget { - final GlobalKey _scaffoldKey = GlobalKey(); +/// 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 theme = Theme.of(context); final accountsStore = useAccountsStore(); + final configStore = useConfigStore(); + final loading = useState(false); + final error = useState(false); - removeInstanceDialog(String instanceHost) async { + Future removeUserDialog(String instanceHost, String username) async { if (await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Remove instance?'), - content: Text('Are you sure you want to remove $instanceHost?'), + title: const Text('Remove user?'), + content: Text( + 'Are you sure you want to remove $username@$instanceHost?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), @@ -148,18 +160,73 @@ class AccountsConfigPage extends HookWidget { ), ) ?? false) { - await accountsStore.removeInstance(instanceHost); + await accountsStore.removeAccount(instanceHost, username); Navigator.of(context).pop(); } } - Future removeUserDialog(String instanceHost, String username) async { + 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: loading.value + ? const SizedBox( + height: 25, + width: 25, + child: CircularProgressIndicator(), + ) + : error.value + ? Icon( + Icons.error, + color: Theme.of(context).errorColor, + ) + : const Icon(Icons.cloud_download), + title: const Text('Import settings to lemmur'), + onTap: () async { + loading.value = true; + error.value = false; + try { + await configStore.importLemmyUserSettings( + accountsStore.userDataFor(instanceHost, username)!.jwt, + ); + } on SocketException { + error.value = true; + } + loading.value = false; + }), + ], + ); + } +} + +/// Settings for managing accounts +class AccountsConfigPage extends HookWidget { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final accountsStore = useAccountsStore(); + + removeInstanceDialog(String instanceHost) async { if (await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Remove user?'), - content: Text( - 'Are you sure you want to remove $username@$instanceHost?'), + title: const Text('Remove instance?'), + content: Text('Are you sure you want to remove $instanceHost?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), @@ -173,7 +240,7 @@ class AccountsConfigPage extends HookWidget { ), ) ?? false) { - await accountsStore.removeAccount(instanceHost, username); + await accountsStore.removeInstance(instanceHost); Navigator.of(context).pop(); } } @@ -181,23 +248,9 @@ class AccountsConfigPage extends HookWidget { void accountActions(String instanceHost, String username) { showBottomModal( context: context, - builder: (context) => 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), - ), - ], + builder: (context) => _AccountOptions( + instanceHost: instanceHost, + username: username, ), ); } diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 366905f7..8f9f98a4 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -72,7 +72,8 @@ class ConfigStore extends ChangeNotifier { save(); } - void importLemmyUserSettings(LocalUserSettings localUserSettings) { + /// Copies over settings from lemmy to [ConfigStore] + void copyLemmyUserSettings(LocalUserSettings localUserSettings) { // themes from lemmy-ui that are dark mode // const darkModeLemmyUiThemes = { // 'solar', @@ -103,6 +104,13 @@ class ConfigStore extends ChangeNotifier { save(); } + /// Fetches [LocalUserSettings] and imports them with [.copyLemmyUserSettings] + Future importLemmyUserSettings(Jwt token) async { + final site = + await LemmyApiV3(token.payload.iss).run(GetSite(auth: token.raw)); + copyLemmyUserSettings(site.myUser!.localUser); + } + static Future load() async { final prefs = await _prefs; From e357332a0799bb7673b7bd342905fb088936aa22 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 21:43:02 +0200 Subject: [PATCH 06/15] Add default sortings --- lib/stores/config_store.dart | 30 ++++++++++++++++++++++++++---- lib/stores/config_store.g.dart | 7 ++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 8f9f98a4..4dd121e4 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -72,6 +72,24 @@ class ConfigStore extends ChangeNotifier { save(); } + late SortType _defaultSortType; + @JsonKey(fromJson: _sortTypeFromJson) + SortType get defaultSortType => _defaultSortType; + set defaultSortType(SortType defaultSortType) { + _defaultSortType = defaultSortType; + notifyListeners(); + save(); + } + + late PostListingType _defaultListingType; + @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 @@ -95,10 +113,8 @@ class ConfigStore extends ChangeNotifier { // : _locale; // TODO: add when it is released // _showScores = localUserSettings.showScores; - - // TODO: should these even be supported? Or we should use our own per-community setting - // SortType defaultSortType - // PostListingType defaultListingType + _defaultSortType = localUserSettings.defaultSortType; + _defaultListingType = localUserSettings.defaultListingType; notifyListeners(); save(); @@ -125,3 +141,9 @@ class ConfigStore extends ChangeNotifier { await prefs.setString(prefsKey, jsonEncode(_$ConfigStoreToJson(this))); } } + +SortType _sortTypeFromJson(String? json) => + json != null ? SortType.fromJson(json) : SortType.hot; +// String _sortType +PostListingType _postListingTypeFromJson(String? json) => + json != null ? PostListingType.fromJson(json) : PostListingType.all; diff --git a/lib/stores/config_store.g.dart b/lib/stores/config_store.g.dart index e75ecadb..206d90a5 100644 --- a/lib/stores/config_store.g.dart +++ b/lib/stores/config_store.g.dart @@ -14,7 +14,10 @@ ConfigStore _$ConfigStoreFromJson(Map json) { ..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; + ..showScores = json['showScores'] as bool? ?? true + ..defaultSortType = _sortTypeFromJson(json['defaultSortType'] as String?) + ..defaultListingType = + _postListingTypeFromJson(json['defaultListingType'] as String?); } Map _$ConfigStoreToJson(ConfigStore instance) => @@ -25,6 +28,8 @@ Map _$ConfigStoreToJson(ConfigStore instance) => 'showAvatars': instance.showAvatars, 'showNsfw': instance.showNsfw, 'showScores': instance.showScores, + 'defaultSortType': instance.defaultSortType, + 'defaultListingType': instance.defaultListingType, }; K _$enumDecode( From 270702192ef4ef4c5bf3f2c0fcd0032302ea310d Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 21:50:23 +0200 Subject: [PATCH 07/15] Add sort type settings --- lib/pages/settings.dart | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index d3ebae6a..1f0f725f 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; +import 'package:lemmy_api_client/v3.dart'; import '../hooks/stores.dart'; import '../l10n/l10n.dart'; @@ -116,6 +117,34 @@ class AppearanceConfigPage extends HookWidget { ), ), ), + ListTile( + title: Text(L10n.of(context)!.type), + trailing: SizedBox( + width: 120, + child: RadioPicker( + values: const [ + PostListingType.all, + PostListingType.local, + PostListingType.subscribed, + ], + groupValue: configStore.defaultListingType, + onChanged: (value) => configStore.defaultListingType = value, + mapValueToString: (value) => value.value, + ), + ), + ), + ListTile( + title: Text(L10n.of(context)!.sort_type), + trailing: SizedBox( + width: 120, + child: RadioPicker( + values: SortType.values, + groupValue: configStore.defaultSortType, + onChanged: (value) => configStore.defaultSortType = value, + mapValueToString: (value) => value.value, + ), + ), + ), ], ), ); From fa7effcb4b6b3c212ca8c455f3b1b2c546b60a52 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 16 Apr 2021 21:59:51 +0200 Subject: [PATCH 08/15] Integrate defaultListingType --- lib/pages/home_tab.dart | 12 ++++++++---- lib/stores/config_store.dart | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pages/home_tab.dart b/lib/pages/home_tab.dart index b23c4198..b74ac8d3 100644 --- a/lib/pages/home_tab.dart +++ b/lib/pages/home_tab.dart @@ -25,10 +25,13 @@ class HomeTab extends HookWidget { @override Widget build(BuildContext context) { final accStore = useAccountsStore(); + final defaultListingType = + useConfigStoreSelect((configStore) => configStore.defaultListingType); final selectedList = useState(_SelectedList( - listingType: accStore.hasNoAccount + listingType: accStore.hasNoAccount && + defaultListingType == PostListingType.subscribed ? PostListingType.all - : PostListingType.subscribed)); + : defaultListingType)); final isc = useInfiniteScrollController(); final theme = Theme.of(context); final instancesIcons = useMemoFuture(() async { @@ -54,9 +57,10 @@ class HomeTab extends HookWidget { selectedList.value.listingType == PostListingType.subscribed || !accStore.instances.contains(selectedList.value.instanceHost)) { selectedList.value = _SelectedList( - listingType: accStore.hasNoAccount + listingType: accStore.hasNoAccount && + defaultListingType == PostListingType.subscribed ? PostListingType.all - : PostListingType.subscribed, + : defaultListingType, ); } diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 4dd121e4..471dc235 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -144,6 +144,5 @@ class ConfigStore extends ChangeNotifier { SortType _sortTypeFromJson(String? json) => json != null ? SortType.fromJson(json) : SortType.hot; -// String _sortType PostListingType _postListingTypeFromJson(String? json) => json != null ? PostListingType.fromJson(json) : PostListingType.all; From 12cb62f87ac6b3003e97fc760fe747fb64b46ad2 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sat, 17 Apr 2021 12:22:15 +0200 Subject: [PATCH 09/15] Add changelog --- CHANGELOG.md | 9 +++++++++ lib/stores/config_store.dart | 2 ++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d7a879..d852b535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased + +### 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 + ## v0.4.2 - 2021-04-12 ### Changed diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 471dc235..b47e5eb2 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -73,6 +73,7 @@ class ConfigStore extends ChangeNotifier { } late SortType _defaultSortType; + // default is set in fromJson @JsonKey(fromJson: _sortTypeFromJson) SortType get defaultSortType => _defaultSortType; set defaultSortType(SortType defaultSortType) { @@ -82,6 +83,7 @@ class ConfigStore extends ChangeNotifier { } late PostListingType _defaultListingType; + // default is set in fromJson @JsonKey(fromJson: _postListingTypeFromJson) PostListingType get defaultListingType => _defaultListingType; set defaultListingType(PostListingType defaultListingType) { From c19496e2cf00cb2a393897ec8703049cc3faecd2 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sun, 18 Apr 2021 16:32:35 +0200 Subject: [PATCH 10/15] Restructure settings --- lib/pages/manage_account.dart | 36 ++++++++--------------------------- lib/pages/settings.dart | 32 +++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/pages/manage_account.dart b/lib/pages/manage_account.dart index a139e09c..0fa48151 100644 --- a/lib/pages/manage_account.dart +++ b/lib/pages/manage_account.dart @@ -293,16 +293,7 @@ class _ManageAccount extends HookWidget { Row( 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), - ) - ], - ), + Text(L10n.of(context)!.type), RadioPicker( values: const [ PostListingType.all, @@ -319,16 +310,7 @@ class _ManageAccount extends HookWidget { 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), - ) - ], - ), + Text(L10n.of(context)!.sort_type), RadioPicker( values: SortType.values, groupValue: defaultSortType.value, @@ -338,30 +320,28 @@ class _ManageAccount extends HookWidget { ], ), const SizedBox(height: 8), - CheckboxListTile( + SwitchListTile.adaptive( value: showAvatars.value, onChanged: (checked) { - if (checked != null) showAvatars.value = checked; + 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( + SwitchListTile.adaptive( value: showNsfw.value, onChanged: (checked) { - if (checked != null) showNsfw.value = checked; + 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( + SwitchListTile.adaptive( value: sendNotificationsToEmail.value, onChanged: (checked) { - if (checked != null) sendNotificationsToEmail.value = checked; + sendNotificationsToEmail.value = checked; }, title: Text(L10n.of(context)!.send_notifications_to_email), dense: true, diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 1f0f725f..8143b73b 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -42,6 +42,13 @@ class SettingsPage extends StatelessWidget { goTo(context, (_) => const AppearanceConfigPage()); }, ), + ListTile( + leading: const Icon(Icons.settings), + title: const Text('General'), + onTap: () { + goTo(context, (_) => const GeneralConfigPage()); + }, + ), const AboutTile() ], ), @@ -57,12 +64,9 @@ class AppearanceConfigPage extends HookWidget { final configStore = useConfigStore(); return Scaffold( - appBar: AppBar( - title: const Text('Appearance'), - ), + appBar: AppBar(title: const Text('Appearance')), body: ListView( children: [ - const _SectionHeading('Theme'), for (final theme in ThemeMode.values) RadioListTile( value: theme, @@ -79,8 +83,24 @@ class AppearanceConfigPage extends HookWidget { configStore.amoledDarkMode = checked; }, ), - const SizedBox(height: 12), - const _SectionHeading('General'), + ], + ), + ); + } +} + +/// 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: [ SwitchListTile.adaptive( title: Text(L10n.of(context)!.show_nsfw), value: configStore.showNsfw, From bb0e1baa0b95ea67e8ed55aefc3148e5708f8e98 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sun, 18 Apr 2021 16:38:02 +0200 Subject: [PATCH 11/15] Move show avatar/scores to appearance --- lib/pages/settings.dart | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 8143b73b..b9c9e47f 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -67,6 +67,7 @@ class AppearanceConfigPage extends HookWidget { appBar: AppBar(title: const Text('Appearance')), body: ListView( children: [ + const _SectionHeading('Theme'), for (final theme in ThemeMode.values) RadioListTile( value: theme, @@ -83,6 +84,22 @@ class AppearanceConfigPage extends HookWidget { configStore.amoledDarkMode = checked; }, ), + const SizedBox(height: 12), + 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; + }, + ), ], ), ); @@ -108,20 +125,6 @@ class GeneralConfigPage extends HookWidget { configStore.showNsfw = checked; }, ), - 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; - }, - ), ListTile( title: Text(L10n.of(context)!.language), trailing: SizedBox( From d1dcdda099ab86f4c8ef94c1eb81224b39b69e0f Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sun, 18 Apr 2021 16:48:38 +0200 Subject: [PATCH 12/15] Remove lemmy-ui specific options --- lib/pages/manage_account.dart | 61 +++-------------------------------- 1 file changed, 4 insertions(+), 57 deletions(-) diff --git a/lib/pages/manage_account.dart b/lib/pages/manage_account.dart index 0fa48151..208aae5d 100644 --- a/lib/pages/manage_account.dart +++ b/lib/pages/manage_account.dart @@ -12,7 +12,6 @@ import '../hooks/stores.dart'; import '../l10n/l10n.dart'; import '../util/pictrs.dart'; import '../widgets/bottom_safe.dart'; -import '../widgets/radio_picker.dart'; /// Page for managing things like username, email, avatar etc /// This page will assume the manage account is logged in and @@ -76,12 +75,8 @@ class _ManageAccount extends HookWidget { useTextEditingController(text: user.person.matrixUserId); final avatar = useRef(user.person.avatar); final banner = useRef(user.person.banner); - final showAvatars = useState(user.localUser.showAvatars); - final showNsfw = useState(user.localUser.showNsfw); final sendNotificationsToEmail = useState(user.localUser.sendNotificationsToEmail); - final defaultListingType = useState(user.localUser.defaultListingType); - final defaultSortType = useState(user.localUser.defaultSortType); final newPasswordController = useTextEditingController(); final newPasswordVerifyController = useTextEditingController(); final oldPasswordController = useTextEditingController(); @@ -106,12 +101,12 @@ class _ManageAccount extends HookWidget { try { await LemmyApiV3(user.instanceHost).run(SaveUserSettings( - showNsfw: showNsfw.value, + showNsfw: user.localUser.showNsfw, theme: user.localUser.theme, - defaultSortType: defaultSortType.value, - defaultListingType: defaultListingType.value, + defaultSortType: user.localUser.defaultSortType, + defaultListingType: user.localUser.defaultListingType, lang: user.localUser.lang, - showAvatars: showAvatars.value, + showAvatars: user.localUser.showAvatars, sendNotificationsToEmail: sendNotificationsToEmail.value, auth: token.raw, avatar: avatar.current, @@ -290,54 +285,6 @@ class _ManageAccount extends HookWidget { obscureText: true, ), const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(L10n.of(context)!.type), - RadioPicker( - 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: [ - Text(L10n.of(context)!.sort_type), - RadioPicker( - values: SortType.values, - groupValue: defaultSortType.value, - onChanged: (value) => defaultSortType.value = value, - mapValueToString: (value) => value.value, - ), - ], - ), - const SizedBox(height: 8), - SwitchListTile.adaptive( - value: showAvatars.value, - onChanged: (checked) { - showAvatars.value = checked; - }, - title: Text(L10n.of(context)!.show_avatars), - dense: true, - ), - const SizedBox(height: 8), - SwitchListTile.adaptive( - value: showNsfw.value, - onChanged: (checked) { - showNsfw.value = checked; - }, - title: Text(L10n.of(context)!.show_nsfw), - dense: true, - ), - const SizedBox(height: 8), SwitchListTile.adaptive( value: sendNotificationsToEmail.value, onChanged: (checked) { From 0d440062dd5e5356afbad25f79d3aff1a1ef4b8a Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 19 Apr 2021 16:57:43 +0200 Subject: [PATCH 13/15] Add link to profile --- lib/pages/manage_account.dart | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/pages/manage_account.dart b/lib/pages/manage_account.dart index 208aae5d..8bc6172b 100644 --- a/lib/pages/manage_account.dart +++ b/lib/pages/manage_account.dart @@ -4,13 +4,16 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lemmy_api_client/pictrs.dart'; import 'package:lemmy_api_client/v3.dart'; +import 'package:url_launcher/url_launcher.dart' as ul; import '../hooks/delayed_loading.dart'; import '../hooks/image_picker.dart'; import '../hooks/ref.dart'; import '../hooks/stores.dart'; import '../l10n/l10n.dart'; +import '../util/more_icon.dart'; import '../util/pictrs.dart'; +import '../widgets/bottom_modal.dart'; import '../widgets/bottom_safe.dart'; /// Page for managing things like username, email, avatar etc @@ -33,9 +36,38 @@ class ManageAccountPage extends HookWidget { 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); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("can't open in browser")), + ); + } + }, + ), + ], + ), + ); + } + return Scaffold( appBar: AppBar( title: Text('$username@$instanceHost'), + actions: [ + IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu), + ], ), body: FutureBuilder( future: userFuture, From 666b2cb8dd757013a7c6042717308a4f5b0ee79a Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 21 Apr 2021 16:12:18 +0200 Subject: [PATCH 14/15] CR suggestions --- lib/pages/manage_account.dart | 1 + lib/pages/settings.dart | 59 ++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/pages/manage_account.dart b/lib/pages/manage_account.dart index 8bc6172b..8f656814 100644 --- a/lib/pages/manage_account.dart +++ b/lib/pages/manage_account.dart @@ -50,6 +50,7 @@ class ManageAccountPage extends HookWidget { 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")), diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index b9c9e47f..20470ab6 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -28,6 +28,13 @@ class SettingsPage extends StatelessWidget { ), body: ListView( children: [ + ListTile( + leading: const Icon(Icons.settings), + title: const Text('General'), + onTap: () { + goTo(context, (_) => const GeneralConfigPage()); + }, + ), ListTile( leading: const Icon(Icons.person), title: const Text('Accounts'), @@ -42,13 +49,6 @@ class SettingsPage extends StatelessWidget { goTo(context, (_) => const AppearanceConfigPage()); }, ), - ListTile( - leading: const Icon(Icons.settings), - title: const Text('General'), - onTap: () { - goTo(context, (_) => const GeneralConfigPage()); - }, - ), const AboutTile() ], ), @@ -118,25 +118,15 @@ class GeneralConfigPage extends HookWidget { appBar: AppBar(title: const Text('General')), body: ListView( children: [ - SwitchListTile.adaptive( - title: Text(L10n.of(context)!.show_nsfw), - value: configStore.showNsfw, - onChanged: (checked) { - configStore.showNsfw = checked; - }, - ), ListTile( - title: Text(L10n.of(context)!.language), + title: Text(L10n.of(context)!.sort_type), trailing: SizedBox( width: 120, - child: RadioPicker( - title: 'Choose language', - groupValue: configStore.locale, - values: L10n.supportedLocales, - mapValueToString: (locale) => locale.languageName, - onChanged: (selected) { - configStore.locale = selected; - }, + child: RadioPicker( + values: SortType.values, + groupValue: configStore.defaultSortType, + onChanged: (value) => configStore.defaultSortType = value, + mapValueToString: (value) => value.value, ), ), ), @@ -157,17 +147,27 @@ class GeneralConfigPage extends HookWidget { ), ), ListTile( - title: Text(L10n.of(context)!.sort_type), + title: Text(L10n.of(context)!.language), trailing: SizedBox( width: 120, - child: RadioPicker( - values: SortType.values, - groupValue: configStore.defaultSortType, - onChanged: (value) => configStore.defaultSortType = value, - mapValueToString: (value) => value.value, + child: RadioPicker( + title: 'Choose language', + groupValue: configStore.locale, + values: L10n.supportedLocales, + mapValueToString: (locale) => locale.languageName, + onChanged: (selected) { + configStore.locale = selected; + }, ), ), ), + SwitchListTile.adaptive( + title: Text(L10n.of(context)!.show_nsfw), + value: configStore.showNsfw, + onChanged: (checked) { + configStore.showNsfw = checked; + }, + ), ], ), ); @@ -254,6 +254,7 @@ class _AccountOptions extends HookWidget { await configStore.importLemmyUserSettings( accountsStore.userDataFor(instanceHost, username)!.jwt, ); + Navigator.of(context).pop(); } on SocketException { error.value = true; } From 1e0653331965a4cde488a932be080d9c759684db Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 21 Apr 2021 21:11:48 +0200 Subject: [PATCH 15/15] Add snackbar --- lib/pages/settings.dart | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 20470ab6..c8eb2a5e 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -189,8 +187,7 @@ class _AccountOptions extends HookWidget { Widget build(BuildContext context) { final accountsStore = useAccountsStore(); final configStore = useConfigStore(); - final loading = useState(false); - final error = useState(false); + final importLoading = useState(false); Future removeUserDialog(String instanceHost, String username) async { if (await showDialog( @@ -234,31 +231,32 @@ class _AccountOptions extends HookWidget { onTap: () => removeUserDialog(instanceHost, username), ), ListTile( - leading: loading.value + leading: importLoading.value ? const SizedBox( height: 25, width: 25, child: CircularProgressIndicator(), ) - : error.value - ? Icon( - Icons.error, - color: Theme.of(context).errorColor, - ) - : const Icon(Icons.cloud_download), + : const Icon(Icons.cloud_download), title: const Text('Import settings to lemmur'), onTap: () async { - loading.value = true; - error.value = false; + 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(); - } on SocketException { - error.value = true; + importLoading.value = false; } - loading.value = false; }), ], );