mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-23 09:16:19 +01:00
435 lines
14 KiB
Dart
435 lines
14 KiB
Dart
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/bloc_util.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/entity/person.dart';
|
|
import 'package:nc_photos/entity/pref.dart';
|
|
import 'package:nc_photos/exception_event.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/snack_bar_manager.dart';
|
|
import 'package:nc_photos/url_launcher_util.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:np_ui/np_ui.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>;
|
|
|
|
enum AccountSettingsOption {
|
|
personProvider,
|
|
}
|
|
|
|
class AccountSettingsArguments {
|
|
const AccountSettingsArguments({
|
|
this.highlight,
|
|
});
|
|
|
|
final AccountSettingsOption? highlight;
|
|
}
|
|
|
|
class AccountSettings extends StatelessWidget {
|
|
static const routeName = "/settings/account";
|
|
|
|
static Route buildRoute(AccountSettingsArguments? args) => MaterialPageRoute(
|
|
builder: (_) => AccountSettings.fromArgs(args),
|
|
);
|
|
|
|
const AccountSettings({
|
|
super.key,
|
|
this.highlight,
|
|
});
|
|
|
|
AccountSettings.fromArgs(AccountSettingsArguments? args, {Key? key})
|
|
: this(
|
|
key: key,
|
|
highlight: args?.highlight,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final accountController = context.read<AccountController>();
|
|
return BlocProvider(
|
|
create: (_) => _Bloc(
|
|
container: KiwiContainer().resolve(),
|
|
account: accountController.account,
|
|
accountPrefController: accountController.accountPrefController,
|
|
highlight: highlight,
|
|
),
|
|
child: const _WrappedAccountSettings(),
|
|
);
|
|
}
|
|
|
|
final AccountSettingsOption? highlight;
|
|
}
|
|
|
|
class _WrappedAccountSettings extends StatefulWidget {
|
|
const _WrappedAccountSettings();
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _WrappedAccountSettingsState();
|
|
}
|
|
|
|
@npLog
|
|
class _WrappedAccountSettingsState extends State<_WrappedAccountSettings>
|
|
with RouteAware, PageVisibilityMixin, TickerProviderStateMixin {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_accountController = context.read<AccountController>();
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_animationController.repeat(reverse: true);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (_bloc.state.shouldResync &&
|
|
_bloc.state.personProvider != PersonProvider.none) {
|
|
_log.fine("[dispose] Requesting to resync account");
|
|
_accountController.syncController.requestResync(
|
|
account: _bloc.state.account,
|
|
filesController: context.read(),
|
|
personsController: context.read(),
|
|
personProvider: _bloc.state.personProvider,
|
|
);
|
|
}
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@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 => !_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<PersonProvider>(
|
|
selector: (state) => state.personProvider,
|
|
builder: (context, state) {
|
|
if (_bloc.highlight ==
|
|
AccountSettingsOption.personProvider) {
|
|
return AnimatedBuilder(
|
|
animation: _highlightAnimation,
|
|
builder: (context, child) => ListTile(
|
|
title: Text(
|
|
L10n.global().settingsPersonProviderTitle),
|
|
subtitle: Text(state.toUserString()),
|
|
onTap: () => _onPersonProviderPressed(context),
|
|
tileColor: _highlightAnimation.value,
|
|
),
|
|
);
|
|
} else {
|
|
return ListTile(
|
|
title:
|
|
Text(L10n.global().settingsPersonProviderTitle),
|
|
subtitle: Text(state.toUserString()),
|
|
onTap: () => _onPersonProviderPressed(context),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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: _bloc.state.label ?? "",
|
|
),
|
|
);
|
|
if (!context.mounted || result == null) {
|
|
return;
|
|
}
|
|
_bloc.add(_SetLabel(result.isEmpty ? null : result));
|
|
}
|
|
|
|
Future<void> _onIncludedFoldersPressed(BuildContext context) async {
|
|
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;
|
|
}
|
|
_bloc.add(_SetAccount(result));
|
|
}
|
|
|
|
Future<void> _onShareFolderPressed(BuildContext context) async {
|
|
final result = await showDialog<String>(
|
|
context: context,
|
|
builder: (_) => _ShareFolderDialog(
|
|
account: _bloc.state.account,
|
|
initialValue: _bloc.state.shareFolder,
|
|
),
|
|
);
|
|
if (!context.mounted || result == null) {
|
|
return;
|
|
}
|
|
_bloc.add(_SetShareFolder(result));
|
|
}
|
|
|
|
Future<void> _onPersonProviderPressed(BuildContext context) async {
|
|
final result = await showDialog<PersonProvider>(
|
|
context: context,
|
|
builder: (_) => _PersonProviderDialog(
|
|
initialValue: _bloc.state.personProvider,
|
|
),
|
|
);
|
|
if (!context.mounted || result == null) {
|
|
return;
|
|
}
|
|
_bloc.add(_SetPersonProvider(result));
|
|
}
|
|
|
|
late final _bloc = context.read<_Bloc>();
|
|
late final AccountController _accountController;
|
|
|
|
late final _animationController = AnimationController(
|
|
vsync: this,
|
|
duration: k.settingsHighlightDuration,
|
|
);
|
|
late final _highlightAnimation = ColorTween(
|
|
end: Theme.of(context).colorScheme.primaryContainer,
|
|
).animate(_animationController);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
@npLog
|
|
class _PersonProviderDialog extends StatelessWidget {
|
|
const _PersonProviderDialog({
|
|
required this.initialValue,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FancyOptionPicker(
|
|
title: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(L10n.global().settingsPersonProviderTitle),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
launch(help_util.peopleUrl);
|
|
},
|
|
tooltip: L10n.global().helpTooltip,
|
|
icon: const Icon(Icons.help_outline),
|
|
),
|
|
],
|
|
),
|
|
items: PersonProvider.values
|
|
.map((provider) => FancyOptionPickerItem(
|
|
label: provider.toUserString(),
|
|
isSelected: provider == initialValue,
|
|
onSelect: () {
|
|
_log.info("[build] Set provider: ${provider.toUserString()}");
|
|
Navigator.of(context).pop(provider);
|
|
},
|
|
))
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
final PersonProvider initialValue;
|
|
}
|
|
|
|
extension on PersonProvider {
|
|
String toUserString() {
|
|
switch (this) {
|
|
case PersonProvider.none:
|
|
return "n/a";
|
|
case PersonProvider.faceRecognition:
|
|
return "Face Recognition";
|
|
case PersonProvider.recognize:
|
|
return "Recognize";
|
|
}
|
|
}
|
|
}
|