mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-26 08:54:42 +01:00
Refactoring: rewrite acccount settings
This commit is contained in:
parent
a17a0432c4
commit
4bde517813
14 changed files with 941 additions and 403 deletions
|
@ -1,5 +1,6 @@
|
|||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/account_pref_controller.dart';
|
||||
import 'package:nc_photos/controller/collections_controller.dart';
|
||||
import 'package:nc_photos/controller/server_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
|
@ -11,6 +12,8 @@ class AccountController {
|
|||
_collectionsController = null;
|
||||
_serverController?.dispose();
|
||||
_serverController = null;
|
||||
_accountPrefController?.dispose();
|
||||
_accountPrefController = null;
|
||||
}
|
||||
|
||||
Account get account => _account!;
|
||||
|
@ -27,7 +30,13 @@ class AccountController {
|
|||
account: _account!,
|
||||
);
|
||||
|
||||
AccountPrefController get accountPrefController =>
|
||||
_accountPrefController ??= AccountPrefController(
|
||||
account: _account!,
|
||||
);
|
||||
|
||||
Account? _account;
|
||||
CollectionsController? _collectionsController;
|
||||
ServerController? _serverController;
|
||||
AccountPrefController? _accountPrefController;
|
||||
}
|
||||
|
|
79
app/lib/controller/account_pref_controller.dart
Normal file
79
app/lib/controller/account_pref_controller.dart
Normal file
|
@ -0,0 +1,79 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'account_pref_controller.g.dart';
|
||||
|
||||
@npLog
|
||||
class AccountPrefController {
|
||||
AccountPrefController({
|
||||
required this.account,
|
||||
}) : _accountPref = AccountPref.of(account);
|
||||
|
||||
void dispose() {}
|
||||
|
||||
ValueStream<bool> get isEnableFaceRecognitionApp =>
|
||||
_enableFaceRecognitionAppController.stream;
|
||||
|
||||
Future<void> setEnableFaceRecognitionApp(bool value) async {
|
||||
final backup = _enableFaceRecognitionAppController.value;
|
||||
_enableFaceRecognitionAppController.add(value);
|
||||
try {
|
||||
if (!await _accountPref.setEnableFaceRecognitionApp(value)) {
|
||||
throw StateError("Unknown error");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[setEnableFaceRecognitionApp] Failed setting preference", e,
|
||||
stackTrace);
|
||||
_enableFaceRecognitionAppController
|
||||
..addError(e, stackTrace)
|
||||
..add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
ValueStream<String> get shareFolder => _shareFolderController.stream;
|
||||
|
||||
Future<void> setShareFolder(String value) async {
|
||||
final backup = _shareFolderController.value;
|
||||
_shareFolderController.add(value);
|
||||
try {
|
||||
if (!await _accountPref.setShareFolder(value)) {
|
||||
throw StateError("Unknown error");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[setShareFolder] Failed setting preference", e, stackTrace);
|
||||
_shareFolderController
|
||||
..addError(e, stackTrace)
|
||||
..add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
ValueStream<String?> get accountLabel => _accountLabelController.stream;
|
||||
|
||||
Future<void> setAccountLabel(String? value) async {
|
||||
final backup = _accountLabelController.value;
|
||||
_accountLabelController.add(value);
|
||||
try {
|
||||
if (!await _accountPref.setAccountLabel(value)) {
|
||||
throw StateError("Unknown error");
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[setAccountLabel] Failed setting preference", e, stackTrace);
|
||||
_accountLabelController
|
||||
..addError(e, stackTrace)
|
||||
..add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
final Account account;
|
||||
|
||||
final AccountPref _accountPref;
|
||||
late final _enableFaceRecognitionAppController =
|
||||
BehaviorSubject.seeded(_accountPref.isEnableFaceRecognitionAppOr(true));
|
||||
late final _shareFolderController =
|
||||
BehaviorSubject.seeded(_accountPref.getShareFolderOr(""));
|
||||
late final _accountLabelController =
|
||||
BehaviorSubject.seeded(_accountPref.getAccountLabel());
|
||||
}
|
15
app/lib/controller/account_pref_controller.g.dart
Normal file
15
app/lib/controller/account_pref_controller.g.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'account_pref_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$AccountPrefControllerNpLog on AccountPrefController {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("controller.account_pref_controller.AccountPrefController");
|
||||
}
|
|
@ -29,6 +29,7 @@ import 'package:nc_photos/toast.dart';
|
|||
import 'package:nc_photos/url_launcher_util.dart';
|
||||
import 'package:nc_photos/widget/home.dart';
|
||||
import 'package:nc_photos/widget/settings.dart';
|
||||
import 'package:nc_photos/widget/settings/account_settings.dart';
|
||||
import 'package:nc_photos/widget/sign_in.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
@ -354,11 +355,7 @@ class _AccountSettingsView extends StatelessWidget {
|
|||
onTap: () {
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pushNamed(
|
||||
AccountSettingsWidget.routeName,
|
||||
arguments: AccountSettingsWidgetArguments(
|
||||
context.read<_Bloc>().activeAccount),
|
||||
);
|
||||
..pushNamed(AccountSettings.routeName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/controller/account_controller.dart';
|
||||
import 'package:nc_photos/stream_util.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/widget/account_picker_dialog.dart';
|
||||
import 'package:nc_photos/widget/app_bar_circular_progress_indicator.dart';
|
||||
|
@ -21,8 +23,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
|||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
final accountLabel = AccountPref.of(account).getAccountLabel();
|
||||
Widget build(BuildContext context) {
|
||||
return TranslucentSliverAppBar(
|
||||
title: InkWell(
|
||||
onTap: () {
|
||||
|
@ -31,31 +32,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
|||
builder: (_) => const AccountPickerDialog(),
|
||||
);
|
||||
},
|
||||
child: AppBarTitleContainer(
|
||||
title: Row(
|
||||
children: [
|
||||
account.scheme == "http"
|
||||
? Icon(
|
||||
Icons.no_encryption_outlined,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 16,
|
||||
)
|
||||
: Icon(
|
||||
Icons.https,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
accountLabel ?? account.address,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: accountLabel == null ? Text(account.username2) : null,
|
||||
),
|
||||
child: _TitleView(account: account),
|
||||
),
|
||||
scrolledUnderBackgroundColor:
|
||||
Theme.of(context).homeNavigationBarBackgroundColor,
|
||||
|
@ -100,6 +77,47 @@ class HomeSliverAppBar extends StatelessWidget {
|
|||
final bool isShowProgressIcon;
|
||||
}
|
||||
|
||||
class _TitleView extends StatelessWidget {
|
||||
const _TitleView({
|
||||
required this.account,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueStreamBuilder<String?>(
|
||||
stream:
|
||||
context.read<AccountController>().accountPrefController.accountLabel,
|
||||
builder: (context, snapshot) => AppBarTitleContainer(
|
||||
title: Row(
|
||||
children: [
|
||||
account.scheme == "http"
|
||||
? Icon(
|
||||
Icons.no_encryption_outlined,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 16,
|
||||
)
|
||||
: Icon(
|
||||
Icons.https,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
snapshot.data ?? account.address,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: snapshot.data == null ? Text(account.username2) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
class _ProfileIconView extends StatelessWidget {
|
||||
const _ProfileIconView({
|
||||
required this.account,
|
||||
|
|
|
@ -34,6 +34,7 @@ import 'package:nc_photos/widget/places_browser.dart';
|
|||
import 'package:nc_photos/widget/result_viewer.dart';
|
||||
import 'package:nc_photos/widget/root_picker.dart';
|
||||
import 'package:nc_photos/widget/settings.dart';
|
||||
import 'package:nc_photos/widget/settings/account_settings.dart';
|
||||
import 'package:nc_photos/widget/settings/language_settings.dart';
|
||||
import 'package:nc_photos/widget/setup.dart';
|
||||
import 'package:nc_photos/widget/share_folder_picker.dart';
|
||||
|
@ -177,6 +178,7 @@ class _WrappedAppState extends State<_WrappedApp>
|
|||
),
|
||||
CollectionPicker.routeName: CollectionPicker.buildRoute,
|
||||
LanguageSettings.routeName: LanguageSettings.buildRoute,
|
||||
AccountSettings.routeName: AccountSettings.buildRoute,
|
||||
};
|
||||
|
||||
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
||||
|
@ -198,7 +200,6 @@ class _WrappedAppState extends State<_WrappedApp>
|
|||
route ??= _handleSharingBrowserRoute(settings);
|
||||
route ??= _handleSharedFileViewerRoute(settings);
|
||||
route ??= _handleAlbumShareOutlierBrowserRoute(settings);
|
||||
route ??= _handleAccountSettingsRoute(settings);
|
||||
route ??= _handleShareFolderPickerRoute(settings);
|
||||
route ??= _handleEnhancedPhotoBrowserRoute(settings);
|
||||
route ??= _handleLocalFileViewerRoute(settings);
|
||||
|
@ -427,20 +428,6 @@ class _WrappedAppState extends State<_WrappedApp>
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic>? _handleAccountSettingsRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == AccountSettingsWidget.routeName &&
|
||||
settings.arguments != null) {
|
||||
final args = settings.arguments as AccountSettingsWidgetArguments;
|
||||
return AccountSettingsWidget.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe(
|
||||
"[_handleAccountSettingsRoute] Failed while handling route", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic>? _handleShareFolderPickerRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == ShareFolderPicker.routeName &&
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/search_landing.dart';
|
||||
import 'package:nc_photos/controller/account_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/collection/builder.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
|
@ -14,7 +15,6 @@ import 'package:nc_photos/exception.dart';
|
|||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/help_utils.dart' as help_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/url_launcher_util.dart';
|
||||
|
@ -82,7 +82,11 @@ class _SearchLandingState extends State<SearchLanding> {
|
|||
Widget _buildContent(BuildContext context, SearchLandingBlocState state) {
|
||||
return Column(
|
||||
children: [
|
||||
if (AccountPref.of(widget.account).isEnableFaceRecognitionAppOr())
|
||||
if (context
|
||||
.read<AccountController>()
|
||||
.accountPrefController
|
||||
.isEnableFaceRecognitionApp
|
||||
.value)
|
||||
..._buildPeopleSection(context, state),
|
||||
..._buildLocationSection(context, state),
|
||||
ListTile(
|
||||
|
|
|
@ -8,7 +8,6 @@ import 'package:nc_photos/app_localizations.dart';
|
|||
import 'package:nc_photos/controller/pref_controller.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
|
@ -23,15 +22,12 @@ import 'package:nc_photos/stream_util.dart';
|
|||
import 'package:nc_photos/url_launcher_util.dart';
|
||||
import 'package:nc_photos/widget/fancy_option_picker.dart';
|
||||
import 'package:nc_photos/widget/gps_map.dart';
|
||||
import 'package:nc_photos/widget/home.dart';
|
||||
import 'package:nc_photos/widget/list_tile_center_leading.dart';
|
||||
import 'package:nc_photos/widget/root_picker.dart';
|
||||
import 'package:nc_photos/widget/settings/developer_settings.dart';
|
||||
import 'package:nc_photos/widget/settings/expert_settings.dart';
|
||||
import 'package:nc_photos/widget/settings/language_settings.dart';
|
||||
import 'package:nc_photos/widget/settings/settings_list_caption.dart';
|
||||
import 'package:nc_photos/widget/settings/theme_settings.dart';
|
||||
import 'package:nc_photos/widget/share_folder_picker.dart';
|
||||
import 'package:nc_photos/widget/simple_input_dialog.dart';
|
||||
import 'package:nc_photos/widget/stateful_slider.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
@ -199,7 +195,9 @@ class _SettingsState extends State<Settings> {
|
|||
label: "Developer options",
|
||||
builder: () => const DeveloperSettings(),
|
||||
),
|
||||
_buildCaption(context, L10n.global().settingsAboutSectionTitle),
|
||||
SettingsListCaption(
|
||||
label: L10n.global().settingsAboutSectionTitle,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.global().settingsVersionTitle),
|
||||
subtitle: const Text(k.versionStr),
|
||||
|
@ -410,334 +408,6 @@ class _SettingsState extends State<Settings> {
|
|||
static const String _translationUrl = "https://bit.ly/3NwmdSw";
|
||||
}
|
||||
|
||||
class AccountSettingsWidgetArguments {
|
||||
const AccountSettingsWidgetArguments(this.account);
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
class AccountSettingsWidget extends StatefulWidget {
|
||||
static const routeName = "/account-settings";
|
||||
|
||||
static Route buildRoute(AccountSettingsWidgetArguments args) =>
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AccountSettingsWidget.fromArgs(args),
|
||||
);
|
||||
|
||||
const AccountSettingsWidget({
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
AccountSettingsWidget.fromArgs(AccountSettingsWidgetArguments args,
|
||||
{Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
);
|
||||
|
||||
@override
|
||||
createState() => _AccountSettingsState();
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _AccountSettingsState extends State<AccountSettingsWidget> {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_account = widget.account;
|
||||
|
||||
final settings = AccountPref.of(_account);
|
||||
_isEnableFaceRecognitionApp = settings.isEnableFaceRecognitionAppOr();
|
||||
_shareFolder = settings.getShareFolderOr();
|
||||
_label = settings.getAccountLabel();
|
||||
}
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => _buildContent(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => !_shouldReload,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
title: Text(L10n.global().settingsAccountTitle),
|
||||
leading: _shouldReload
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
tooltip: L10n.global().doneButtonTooltip,
|
||||
onPressed: () => _onDonePressed(context),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
ListTile(
|
||||
title: Text(L10n.global().settingsAccountLabelTitle),
|
||||
subtitle: Text(
|
||||
_label ?? L10n.global().settingsAccountLabelDescription),
|
||||
onTap: () => _onLabelPressed(context),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.global().settingsIncludedFoldersTitle),
|
||||
subtitle: Text(_account.roots.map((e) => "/$e").join("; ")),
|
||||
onTap: _onIncludedFoldersPressed,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.global().settingsShareFolderTitle),
|
||||
subtitle: Text("/$_shareFolder"),
|
||||
onTap: () => _onShareFolderPressed(context),
|
||||
),
|
||||
_buildCaption(
|
||||
context, L10n.global().settingsServerAppSectionTitle),
|
||||
SwitchListTile(
|
||||
title: const Text("Face Recognition"),
|
||||
value: _isEnableFaceRecognitionApp,
|
||||
onChanged: _onEnableFaceRecognitionAppChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDonePressed(BuildContext context) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
Home.routeName,
|
||||
(route) => false,
|
||||
arguments: HomeArguments(_account),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLabelPressed(BuildContext context) async {
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => SimpleInputDialog(
|
||||
titleText: L10n.global().settingsAccountLabelTitle,
|
||||
buttonText: MaterialLocalizations.of(context).okButtonLabel,
|
||||
initialText: _label ?? "",
|
||||
));
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
if (result.isEmpty) {
|
||||
return _setLabel(null);
|
||||
} else {
|
||||
return _setLabel(result);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onIncludedFoldersPressed() async {
|
||||
try {
|
||||
final result = await Navigator.of(context).pushNamed<Account>(
|
||||
RootPicker.routeName,
|
||||
arguments: RootPickerArguments(_account));
|
||||
if (result == null) {
|
||||
// user canceled
|
||||
return;
|
||||
}
|
||||
// we've got a good account
|
||||
if (result == _account) {
|
||||
// no changes, do nothing
|
||||
_log.fine("[_onIncludedFoldersPressed] No changes");
|
||||
return;
|
||||
}
|
||||
final accounts = Pref().getAccounts3()!;
|
||||
if (accounts.contains(result)) {
|
||||
// conflict with another account. This normally won't happen because
|
||||
// the app passwords are unique to each entry, but just in case
|
||||
Navigator.of(context).pop();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().editAccountConflictFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final index = accounts.indexOf(_account);
|
||||
if (index < 0) {
|
||||
_log.shout("[_onIncludedFoldersPressed] Account not found: $_account");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
accounts[index] = result;
|
||||
if (!await Pref().setAccounts3(accounts)) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_account = result;
|
||||
_shouldReload = true;
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_onIncludedFoldersPressed] Exception", e, stackTrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(exception_util.toUserString(e)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onShareFolderPressed(BuildContext context) async {
|
||||
final path = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => _ShareFolderDialog(
|
||||
account: widget.account,
|
||||
initialValue: _shareFolder,
|
||||
),
|
||||
);
|
||||
if (path == null || path == _shareFolder) {
|
||||
return;
|
||||
}
|
||||
return _setShareFolder(path);
|
||||
}
|
||||
|
||||
Future<void> _onEnableFaceRecognitionAppChanged(bool value) async {
|
||||
_log.info("[_onEnableFaceRecognitionAppChanged] New value: $value");
|
||||
final oldValue = _isEnableFaceRecognitionApp;
|
||||
setState(() {
|
||||
_isEnableFaceRecognitionApp = value;
|
||||
});
|
||||
if (!await AccountPref.of(_account).setEnableFaceRecognitionApp(value)) {
|
||||
_log.severe("[_onEnableFaceRecognitionAppChanged] Failed writing pref");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
setState(() {
|
||||
_isEnableFaceRecognitionApp = oldValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setLabel(String? value) async {
|
||||
_log.info("[_setLabel] New value: $value");
|
||||
final oldValue = _label;
|
||||
setState(() {
|
||||
_label = value;
|
||||
});
|
||||
if (!await AccountPref.of(_account).setAccountLabel(value)) {
|
||||
_log.severe("[_setLabel] Failed writing pref");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
setState(() {
|
||||
_label = oldValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setShareFolder(String value) async {
|
||||
_log.info("[_setShareFolder] New value: $value");
|
||||
final oldValue = _shareFolder;
|
||||
setState(() {
|
||||
_shareFolder = value;
|
||||
});
|
||||
if (!await AccountPref.of(_account).setShareFolder(value)) {
|
||||
_log.severe("[_setShareFolder] Failed writing pref");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
setState(() {
|
||||
_shareFolder = oldValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldReload = false;
|
||||
late Account _account;
|
||||
late bool _isEnableFaceRecognitionApp;
|
||||
late String _shareFolder;
|
||||
late String? _label;
|
||||
}
|
||||
|
||||
class _ShareFolderDialog extends StatefulWidget {
|
||||
const _ShareFolderDialog({
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.initialValue,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
createState() => _ShareFolderDialogState();
|
||||
|
||||
final Account account;
|
||||
final String initialValue;
|
||||
}
|
||||
|
||||
class _ShareFolderDialogState extends State<_ShareFolderDialog> {
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(L10n.global().settingsShareFolderDialogTitle),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(L10n.global().settingsShareFolderDialogDescription),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: _onTextFieldPressed,
|
||||
child: TextFormField(
|
||||
enabled: false,
|
||||
controller: _controller,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _onOkPressed,
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTextFieldPressed() async {
|
||||
final pick = await Navigator.of(context).pushNamed<String>(
|
||||
ShareFolderPicker.routeName,
|
||||
arguments: ShareFolderPickerArguments(widget.account, _path));
|
||||
if (pick != null) {
|
||||
_path = pick;
|
||||
_controller.text = "/$pick";
|
||||
}
|
||||
}
|
||||
|
||||
void _onOkPressed() {
|
||||
Navigator.of(context).pop(_path);
|
||||
}
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final _controller =
|
||||
TextEditingController(text: "/${widget.initialValue}");
|
||||
late String _path = widget.initialValue;
|
||||
}
|
||||
|
||||
class _PhotosSettings extends StatefulWidget {
|
||||
const _PhotosSettings({
|
||||
Key? key,
|
||||
|
@ -1527,17 +1197,5 @@ class _MiscSettingsState extends State<_MiscSettings> {
|
|||
late bool _isDoubleTapExit;
|
||||
}
|
||||
|
||||
Widget _buildCaption(BuildContext context, String label) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// final _enabledExperiments = [
|
||||
// ];
|
||||
|
|
|
@ -13,13 +13,6 @@ extension _$_SettingsStateNpLog on _SettingsState {
|
|||
static final log = Logger("widget.settings._SettingsState");
|
||||
}
|
||||
|
||||
extension _$_AccountSettingsStateNpLog on _AccountSettingsState {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.settings._AccountSettingsState");
|
||||
}
|
||||
|
||||
extension _$_PhotosSettingsStateNpLog on _PhotosSettingsState {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
|
158
app/lib/widget/settings/account/bloc.dart
Normal file
158
app/lib/widget/settings/account/bloc.dart
Normal file
|
@ -0,0 +1,158 @@
|
|||
part of '../account_settings.dart';
|
||||
|
||||
@npLog
|
||||
class _Bloc extends Bloc<_Event, _State> {
|
||||
_Bloc({
|
||||
required DiContainer container,
|
||||
required Account account,
|
||||
required this.accountPrefController,
|
||||
}) : _c = container,
|
||||
super(_State.init(
|
||||
account: account,
|
||||
label: accountPrefController.accountLabel.value,
|
||||
shareFolder: accountPrefController.shareFolder.value,
|
||||
isEnableFaceRecognitionApp:
|
||||
accountPrefController.isEnableFaceRecognitionApp.value,
|
||||
)) {
|
||||
on<_SetLabel>(_onSetLabel);
|
||||
on<_OnUpdateLabel>(_onOnUpdateLabel);
|
||||
_subscriptions.add(accountPrefController.accountLabel.listen(
|
||||
(event) {
|
||||
add(_OnUpdateLabel(event));
|
||||
},
|
||||
onError: (e, stackTrace) {
|
||||
add(_SetError(_WritePrefError(e, stackTrace)));
|
||||
},
|
||||
));
|
||||
|
||||
on<_SetAccount>(_onSetAccount);
|
||||
on<_OnUpdateAccount>(_onOnUpdateAccount);
|
||||
|
||||
on<_SetShareFolder>(_onSetShareFolder);
|
||||
on<_OnUpdateShareFolder>(_onOnUpdateShareFolder);
|
||||
_subscriptions.add(accountPrefController.shareFolder.listen(
|
||||
(event) {
|
||||
add(_OnUpdateShareFolder(event));
|
||||
},
|
||||
onError: (e, stackTrace) {
|
||||
add(_SetError(_WritePrefError(e, stackTrace)));
|
||||
},
|
||||
));
|
||||
|
||||
on<_SetEnableFaceRecognitionApp>(_onSetEnableFaceRecognitionApp);
|
||||
on<_OnUpdateEnableFaceRecognitionApp>(_onOnUpdateEnableFaceRecognitionApp);
|
||||
_subscriptions.add(accountPrefController.isEnableFaceRecognitionApp.listen(
|
||||
(event) {
|
||||
add(_OnUpdateEnableFaceRecognitionApp(event));
|
||||
},
|
||||
onError: (e, stackTrace) {
|
||||
add(_SetError(_WritePrefError(e, stackTrace)));
|
||||
},
|
||||
));
|
||||
|
||||
on<_SetError>(_onSetError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
for (final s in _subscriptions) {
|
||||
unawaited(s.cancel());
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(Object error, StackTrace stackTrace) {
|
||||
// we need this to prevent onError being triggered recursively
|
||||
if (!isClosed && !_isHandlingError) {
|
||||
_isHandlingError = true;
|
||||
try {
|
||||
add(_SetError(error, stackTrace));
|
||||
} catch (_) {}
|
||||
_isHandlingError = false;
|
||||
}
|
||||
super.onError(error, stackTrace);
|
||||
}
|
||||
|
||||
void _onSetLabel(_SetLabel ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
accountPrefController.setAccountLabel(ev.label);
|
||||
}
|
||||
|
||||
void _onOnUpdateLabel(_OnUpdateLabel ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(label: ev.label));
|
||||
}
|
||||
|
||||
Future<void> _onSetAccount(_SetAccount ev, Emitter<_State> emit) async {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(
|
||||
account: ev.account,
|
||||
shouldReload: true,
|
||||
));
|
||||
final revert = state.account;
|
||||
try {
|
||||
final accounts = _c.pref.getAccounts3()!;
|
||||
if (accounts.contains(ev.account)) {
|
||||
// conflict with another account. This normally won't happen because
|
||||
// the app passwords are unique to each entry, but just in case
|
||||
throw const _AccountConflictError();
|
||||
}
|
||||
|
||||
final index = accounts.indexWhere((a) => a.id == ev.account.id);
|
||||
if (index < 0) {
|
||||
_log.shout("[_onSetAccount] Account not found: ${ev.account}");
|
||||
throw const _WritePrefError();
|
||||
}
|
||||
|
||||
accounts[index] = ev.account;
|
||||
if (!await _c.pref.setAccounts3(accounts)) {
|
||||
_log.severe("[_onSetAccount] Failed while setAccounts3: ${ev.account}");
|
||||
throw const _WritePrefError();
|
||||
}
|
||||
} catch (_) {
|
||||
emit(state.copyWith(account: revert));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _onOnUpdateAccount(_OnUpdateAccount ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
}
|
||||
|
||||
void _onSetShareFolder(_SetShareFolder ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
accountPrefController.setShareFolder(ev.shareFolder);
|
||||
emit(state.copyWith(shouldReload: true));
|
||||
}
|
||||
|
||||
void _onOnUpdateShareFolder(_OnUpdateShareFolder ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(shareFolder: ev.shareFolder));
|
||||
}
|
||||
|
||||
void _onSetEnableFaceRecognitionApp(
|
||||
_SetEnableFaceRecognitionApp ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
accountPrefController
|
||||
.setEnableFaceRecognitionApp(ev.isEnableFaceRecognitionApp);
|
||||
}
|
||||
|
||||
void _onOnUpdateEnableFaceRecognitionApp(
|
||||
_OnUpdateEnableFaceRecognitionApp ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(
|
||||
isEnableFaceRecognitionApp: ev.isEnableFaceRecognitionApp));
|
||||
}
|
||||
|
||||
void _onSetError(_SetError ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final AccountPrefController accountPrefController;
|
||||
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
var _isHandlingError = false;
|
||||
}
|
148
app/lib/widget/settings/account/state_event.dart
Normal file
148
app/lib/widget/settings/account/state_event.dart
Normal file
|
@ -0,0 +1,148 @@
|
|||
part of '../account_settings.dart';
|
||||
|
||||
@genCopyWith
|
||||
@toString
|
||||
class _State {
|
||||
const _State({
|
||||
required this.account,
|
||||
required this.shouldReload,
|
||||
required this.label,
|
||||
required this.shareFolder,
|
||||
required this.isEnableFaceRecognitionApp,
|
||||
this.error,
|
||||
});
|
||||
|
||||
factory _State.init({
|
||||
required Account account,
|
||||
required String? label,
|
||||
required String shareFolder,
|
||||
required bool isEnableFaceRecognitionApp,
|
||||
}) {
|
||||
return _State(
|
||||
shouldReload: false,
|
||||
account: account,
|
||||
label: label,
|
||||
shareFolder: shareFolder,
|
||||
isEnableFaceRecognitionApp: isEnableFaceRecognitionApp,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final bool shouldReload;
|
||||
final Account account;
|
||||
final String? label;
|
||||
final String shareFolder;
|
||||
final bool isEnableFaceRecognitionApp;
|
||||
|
||||
final ExceptionEvent? error;
|
||||
}
|
||||
|
||||
class _AccountConflictError implements Exception {
|
||||
const _AccountConflictError();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _WritePrefError implements Exception {
|
||||
const _WritePrefError([this.error, this.stackTrace]);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Error? error;
|
||||
final StackTrace? stackTrace;
|
||||
}
|
||||
|
||||
abstract class _Event {}
|
||||
|
||||
@toString
|
||||
class _SetLabel implements _Event {
|
||||
const _SetLabel(this.label);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final String? label;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _OnUpdateLabel implements _Event {
|
||||
const _OnUpdateLabel(this.label);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final String? label;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetAccount implements _Event {
|
||||
const _SetAccount(this.account);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _OnUpdateAccount implements _Event {
|
||||
const _OnUpdateAccount(this.account);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetShareFolder implements _Event {
|
||||
const _SetShareFolder(this.shareFolder);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final String shareFolder;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _OnUpdateShareFolder implements _Event {
|
||||
const _OnUpdateShareFolder(this.shareFolder);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final String shareFolder;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetEnableFaceRecognitionApp implements _Event {
|
||||
const _SetEnableFaceRecognitionApp(this.isEnableFaceRecognitionApp);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final bool isEnableFaceRecognitionApp;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _OnUpdateEnableFaceRecognitionApp implements _Event {
|
||||
const _OnUpdateEnableFaceRecognitionApp(this.isEnableFaceRecognitionApp);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final bool isEnableFaceRecognitionApp;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetError implements _Event {
|
||||
const _SetError(this.error, [this.stackTrace]);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object error;
|
||||
final StackTrace? stackTrace;
|
||||
}
|
294
app/lib/widget/settings/account_settings.dart
Normal file
294
app/lib/widget/settings/account_settings.dart
Normal file
|
@ -0,0 +1,294 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/controller/account_controller.dart';
|
||||
import 'package:nc_photos/controller/account_pref_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/widget/home.dart';
|
||||
import 'package:nc_photos/widget/page_visibility_mixin.dart';
|
||||
import 'package:nc_photos/widget/root_picker.dart';
|
||||
import 'package:nc_photos/widget/settings/settings_list_caption.dart';
|
||||
import 'package:nc_photos/widget/share_folder_picker.dart';
|
||||
import 'package:nc_photos/widget/simple_input_dialog.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'account/bloc.dart';
|
||||
part 'account/state_event.dart';
|
||||
part 'account_settings.g.dart';
|
||||
|
||||
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||
typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||
|
||||
class AccountSettings extends StatelessWidget {
|
||||
static const routeName = "/account-settings";
|
||||
|
||||
static Route buildRoute() => MaterialPageRoute(
|
||||
builder: (_) => const AccountSettings(),
|
||||
);
|
||||
|
||||
const AccountSettings({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accountController = context.read<AccountController>();
|
||||
return BlocProvider(
|
||||
create: (_) => _Bloc(
|
||||
container: KiwiContainer().resolve(),
|
||||
account: accountController.account,
|
||||
accountPrefController: accountController.accountPrefController,
|
||||
),
|
||||
child: const _WrappedAccountSettings(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WrappedAccountSettings extends StatefulWidget {
|
||||
const _WrappedAccountSettings();
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _WrappedDeveloperSettingsState();
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _WrappedDeveloperSettingsState extends State<_WrappedAccountSettings>
|
||||
with RouteAware, PageVisibilityMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: MultiBlocListener(
|
||||
listeners: [
|
||||
_BlocListener(
|
||||
listenWhen: (previous, current) => previous.error != current.error,
|
||||
listener: (context, state) {
|
||||
if (state.error != null && isPageVisible()) {
|
||||
final String errorMsg;
|
||||
if (state.error is _AccountConflictError) {
|
||||
errorMsg =
|
||||
L10n.global().editAccountConflictFailureNotification;
|
||||
} else if (state.error is _WritePrefError) {
|
||||
errorMsg = L10n.global().writePreferenceFailureNotification;
|
||||
} else {
|
||||
errorMsg = exception_util.toUserString(state.error!.error);
|
||||
}
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(errorMsg),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: WillPopScope(
|
||||
onWillPop: () async => !context.read<_Bloc>().state.shouldReload,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
title: Text(L10n.global().settingsAccountTitle),
|
||||
leading: _BlocSelector<bool>(
|
||||
selector: (state) => state.shouldReload,
|
||||
builder: (_, state) =>
|
||||
state ? const _DoneButton() : const BackButton(),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
_BlocSelector<String?>(
|
||||
selector: (state) => state.label,
|
||||
builder: (context, state) => ListTile(
|
||||
title: Text(L10n.global().settingsAccountLabelTitle),
|
||||
subtitle: Text(state ??
|
||||
L10n.global().settingsAccountLabelDescription),
|
||||
onTap: () => _onLabelPressed(context),
|
||||
),
|
||||
),
|
||||
_BlocSelector<Account>(
|
||||
selector: (state) => state.account,
|
||||
builder: (context, state) => ListTile(
|
||||
title: Text(L10n.global().settingsIncludedFoldersTitle),
|
||||
subtitle:
|
||||
Text(state.roots.map((e) => "/$e").join("; ")),
|
||||
onTap: () => _onIncludedFoldersPressed(context),
|
||||
),
|
||||
),
|
||||
_BlocSelector<String>(
|
||||
selector: (state) => state.shareFolder,
|
||||
builder: (context, state) => ListTile(
|
||||
title: Text(L10n.global().settingsShareFolderTitle),
|
||||
subtitle: Text("/$state"),
|
||||
onTap: () => _onShareFolderPressed(context),
|
||||
),
|
||||
),
|
||||
SettingsListCaption(
|
||||
label: L10n.global().settingsServerAppSectionTitle,
|
||||
),
|
||||
_BlocSelector<bool>(
|
||||
selector: (state) => state.isEnableFaceRecognitionApp,
|
||||
builder: (context, state) => SwitchListTile(
|
||||
title: const Text("Face Recognition"),
|
||||
value: state,
|
||||
onChanged: (value) =>
|
||||
_onEnableFaceRecognitionAppChanged(context, value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLabelPressed(BuildContext context) async {
|
||||
final bloc = context.read<_Bloc>();
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => SimpleInputDialog(
|
||||
titleText: L10n.global().settingsAccountLabelTitle,
|
||||
buttonText: MaterialLocalizations.of(context).okButtonLabel,
|
||||
initialText: bloc.state.label ?? "",
|
||||
),
|
||||
);
|
||||
if (!context.mounted || result == null) {
|
||||
return;
|
||||
}
|
||||
context.read<_Bloc>().add(_SetLabel(result.isEmpty ? null : result));
|
||||
}
|
||||
|
||||
Future<void> _onIncludedFoldersPressed(BuildContext context) async {
|
||||
final bloc = context.read<_Bloc>();
|
||||
final result = await Navigator.of(context).pushNamed<Account>(
|
||||
RootPicker.routeName,
|
||||
arguments: RootPickerArguments(bloc.state.account),
|
||||
);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
if (result == bloc.state.account) {
|
||||
// no changes, do nothing
|
||||
_log.fine("[_onIncludedFoldersPressed] No changes");
|
||||
return;
|
||||
}
|
||||
context.read<_Bloc>().add(_SetAccount(result));
|
||||
}
|
||||
|
||||
Future<void> _onShareFolderPressed(BuildContext context) async {
|
||||
final bloc = context.read<_Bloc>();
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => _ShareFolderDialog(
|
||||
account: bloc.state.account,
|
||||
initialValue: bloc.state.shareFolder,
|
||||
),
|
||||
);
|
||||
if (!context.mounted || result == null) {
|
||||
return;
|
||||
}
|
||||
context.read<_Bloc>().add(_SetShareFolder(result));
|
||||
}
|
||||
|
||||
void _onEnableFaceRecognitionAppChanged(BuildContext context, bool value) {
|
||||
context.read<_Bloc>().add(_SetEnableFaceRecognitionApp(value));
|
||||
}
|
||||
}
|
||||
|
||||
class _DoneButton extends StatelessWidget {
|
||||
const _DoneButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
tooltip: L10n.global().doneButtonTooltip,
|
||||
onPressed: () {
|
||||
final newAccount = context.read<_Bloc>().state.account;
|
||||
context.read<AccountController>().setCurrentAccount(newAccount);
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
Home.routeName,
|
||||
(_) => false,
|
||||
arguments: HomeArguments(newAccount),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareFolderDialog extends StatefulWidget {
|
||||
const _ShareFolderDialog({
|
||||
required this.account,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ShareFolderDialogState();
|
||||
|
||||
final Account account;
|
||||
final String initialValue;
|
||||
}
|
||||
|
||||
class _ShareFolderDialogState extends State<_ShareFolderDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(L10n.global().settingsShareFolderDialogTitle),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(L10n.global().settingsShareFolderDialogDescription),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: _onTextFieldPressed,
|
||||
child: TextFormField(
|
||||
enabled: false,
|
||||
controller: _controller,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _onOkPressed,
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTextFieldPressed() async {
|
||||
final pick = await Navigator.of(context).pushNamed<String>(
|
||||
ShareFolderPicker.routeName,
|
||||
arguments: ShareFolderPickerArguments(widget.account, _path),
|
||||
);
|
||||
if (pick != null) {
|
||||
_path = pick;
|
||||
_controller.text = "/$pick";
|
||||
}
|
||||
}
|
||||
|
||||
void _onOkPressed() {
|
||||
Navigator.of(context).pop(_path);
|
||||
}
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final _controller =
|
||||
TextEditingController(text: "/${widget.initialValue}");
|
||||
late String _path = widget.initialValue;
|
||||
}
|
155
app/lib/widget/settings/account_settings.g.dart
Normal file
155
app/lib/widget/settings/account_settings.g.dart
Normal file
|
@ -0,0 +1,155 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'account_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $_StateCopyWithWorker {
|
||||
_State call(
|
||||
{bool? shouldReload,
|
||||
Account? account,
|
||||
String? label,
|
||||
String? shareFolder,
|
||||
bool? isEnableFaceRecognitionApp,
|
||||
ExceptionEvent? error});
|
||||
}
|
||||
|
||||
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||
_$_StateCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
_State call(
|
||||
{dynamic shouldReload,
|
||||
dynamic account,
|
||||
dynamic label = copyWithNull,
|
||||
dynamic shareFolder,
|
||||
dynamic isEnableFaceRecognitionApp,
|
||||
dynamic error = copyWithNull}) {
|
||||
return _State(
|
||||
shouldReload: shouldReload as bool? ?? that.shouldReload,
|
||||
account: account as Account? ?? that.account,
|
||||
label: label == copyWithNull ? that.label : label as String?,
|
||||
shareFolder: shareFolder as String? ?? that.shareFolder,
|
||||
isEnableFaceRecognitionApp: isEnableFaceRecognitionApp as bool? ??
|
||||
that.isEnableFaceRecognitionApp,
|
||||
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
|
||||
}
|
||||
|
||||
final _State that;
|
||||
}
|
||||
|
||||
extension $_StateCopyWith on _State {
|
||||
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_WrappedDeveloperSettingsStateNpLog
|
||||
on _WrappedDeveloperSettingsState {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log =
|
||||
Logger("widget.settings.account_settings._WrappedDeveloperSettingsState");
|
||||
}
|
||||
|
||||
extension _$_BlocNpLog on _Bloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.settings.account_settings._Bloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_StateToString on _State {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_State {shouldReload: $shouldReload, account: $account, label: $label, shareFolder: $shareFolder, isEnableFaceRecognitionApp: $isEnableFaceRecognitionApp, error: $error}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_WritePrefErrorToString on _WritePrefError {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_WritePrefError {error: $error, stackTrace: $stackTrace}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetLabelToString on _SetLabel {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetLabel {label: $label}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_OnUpdateLabelToString on _OnUpdateLabel {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_OnUpdateLabel {label: $label}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetAccountToString on _SetAccount {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetAccount {account: $account}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_OnUpdateAccountToString on _OnUpdateAccount {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_OnUpdateAccount {account: $account}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetShareFolderToString on _SetShareFolder {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetShareFolder {shareFolder: $shareFolder}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_OnUpdateShareFolderToString on _OnUpdateShareFolder {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_OnUpdateShareFolder {shareFolder: $shareFolder}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetEnableFaceRecognitionAppToString
|
||||
on _SetEnableFaceRecognitionApp {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetEnableFaceRecognitionApp {isEnableFaceRecognitionApp: $isEnableFaceRecognitionApp}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_OnUpdateEnableFaceRecognitionAppToString
|
||||
on _OnUpdateEnableFaceRecognitionApp {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_OnUpdateEnableFaceRecognitionApp {isEnableFaceRecognitionApp: $isEnableFaceRecognitionApp}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetErrorToString on _SetError {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetError {error: $error, stackTrace: $stackTrace}";
|
||||
}
|
||||
}
|
23
app/lib/widget/settings/settings_list_caption.dart
Normal file
23
app/lib/widget/settings/settings_list_caption.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsListCaption extends StatelessWidget {
|
||||
const SettingsListCaption({
|
||||
super.key,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String label;
|
||||
}
|
Loading…
Add table
Reference in a new issue