Refactoring: rewrite acccount settings

This commit is contained in:
Ming Ming 2023-06-10 18:44:02 +08:00
parent a17a0432c4
commit 4bde517813
14 changed files with 941 additions and 403 deletions

View file

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

View 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());
}

View 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");
}

View file

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

View file

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

View file

@ -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 &&

View file

@ -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(

View file

@ -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 = [
// ];

View file

@ -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;

View 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;
}

View 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;
}

View 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;
}

View 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}";
}
}

View 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;
}