Redesign account picker dialog

This commit is contained in:
Ming Ming 2023-06-05 01:15:29 +08:00
parent 712ed850fe
commit bde05103e0
8 changed files with 660 additions and 223 deletions

View file

@ -1403,6 +1403,7 @@
}, },
"createCollectionDialogNextcloudAlbumDescription": "Server-side album, require Nextcloud 25 or above", "createCollectionDialogNextcloudAlbumDescription": "Server-side album, require Nextcloud 25 or above",
"removeCollectionsFailedNotification": "Failed to remove some collections", "removeCollectionsFailedNotification": "Failed to remove some collections",
"accountSettingsTooltip": "Account settings",
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": { "@errorUnauthenticated": {

View file

@ -10,7 +10,8 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
], ],
"de": [ "de": [
@ -212,6 +213,7 @@
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification", "removeCollectionsFailedNotification",
"accountSettingsTooltip",
"errorAlbumDowngrade" "errorAlbumDowngrade"
], ],
@ -316,7 +318,16 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
],
"es": [
"accountSettingsTooltip"
],
"fi": [
"accountSettingsTooltip"
], ],
"fr": [ "fr": [
@ -440,7 +451,8 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
], ],
"it": [ "it": [
@ -741,6 +753,7 @@
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification", "removeCollectionsFailedNotification",
"accountSettingsTooltip",
"errorUnauthenticated", "errorUnauthenticated",
"errorDisconnected", "errorDisconnected",
"errorLocked", "errorLocked",
@ -1087,6 +1100,7 @@
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification", "removeCollectionsFailedNotification",
"accountSettingsTooltip",
"errorUnauthenticated", "errorUnauthenticated",
"errorDisconnected", "errorDisconnected",
"errorLocked", "errorLocked",
@ -1232,7 +1246,8 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
], ],
"pt": [ "pt": [
@ -1246,7 +1261,8 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
], ],
"ru": [ "ru": [
@ -1366,7 +1382,8 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
], ],
"zh": [ "zh": [
@ -1486,7 +1503,8 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
], ],
"zh_Hant": [ "zh_Hant": [
@ -1606,6 +1624,7 @@
"exportCollectionDialogTitle", "exportCollectionDialogTitle",
"createCollectionDialogNextcloudAlbumLabel", "createCollectionDialogNextcloudAlbumLabel",
"createCollectionDialogNextcloudAlbumDescription", "createCollectionDialogNextcloudAlbumDescription",
"removeCollectionsFailedNotification" "removeCollectionsFailedNotification",
"accountSettingsTooltip"
] ]
} }

View file

@ -1,176 +1,386 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:clock/clock.dart';
import 'package:copy_with/copy_with.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart'; import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart';
import 'package:nc_photos/account.dart'; 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/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/server_status.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql; import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; 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/k.dart' as k;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart'; 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/home.dart';
import 'package:nc_photos/widget/settings.dart'; import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/sign_in.dart'; import 'package:nc_photos/widget/sign_in.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
part 'account_picker_dialog.g.dart'; part 'account_picker_dialog.g.dart';
part 'account_picker_dialog/bloc.dart';
part 'account_picker_dialog/state_event.dart';
/// A dialog that allows the user to switch between accounts typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
class AccountPickerDialog extends StatefulWidget { typedef _BlocListener = BlocListener<_Bloc, _State>;
const AccountPickerDialog({
Key? key, class AccountPickerDialog extends StatelessWidget {
required this.account, const AccountPickerDialog({super.key});
}) : super(key: key);
@override @override
createState() => _AccountPickerDialogState(); Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _Bloc(
container: KiwiContainer().resolve(),
accountController: context.read(),
),
child: const _WrappedAccountPickerDialog(),
);
}
}
class _WrappedAccountPickerDialog extends StatelessWidget {
const _WrappedAccountPickerDialog();
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListener(
listenWhen: (previous, current) =>
previous.newSelectAccount != current.newSelectAccount,
listener: (context, state) {
if (state.newSelectAccount != null) {
Navigator.of(context).pushNamedAndRemoveUntil(
Home.routeName,
(_) => false,
arguments: HomeArguments(state.newSelectAccount!),
);
}
},
),
_BlocListener(
listenWhen: (previous, current) => previous.error != current.error,
listener: (context, state) {
if (state.error != null) {
AppToast.showToast(
context,
msg: exception_util.toUserString(state.error!.error),
duration: k.snackBarDurationNormal,
);
}
},
),
],
child: Dialog(
child: Padding(
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Container(
color: Theme.of(context).colorScheme.background,
child: Material(
type: MaterialType.transparency,
child: _BlocBuilder(
buildWhen: (previous, current) =>
previous.isOpenDropdown != current.isOpenDropdown ||
previous.accounts != current.accounts,
builder: (context, state) {
final bloc = context.read<_Bloc>();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const _AccountDropdown(),
if (state.isOpenDropdown) ...[
...state.accounts
.where((a) => a.id != bloc.activeAccount.id)
.map((a) => _AccountView(account: a)),
const _NewAccountView(),
] else
const _AccountSettingsView(),
],
);
},
),
),
),
),
_IconTile(
icon: const Icon(Icons.settings_outlined),
title: Text(L10n.global().settingsMenuLabel),
onTap: () {
Navigator.of(context)
..pop()
..pushNamed(
Settings.routeName,
arguments: SettingsArguments(
context.read<_Bloc>().activeAccount),
);
},
),
_IconTile(
icon: const Icon(Icons.help_outline),
title: Text(L10n.global().helpTooltip),
onTap: () {
Navigator.of(context).pop();
launch(help_util.mainUrl);
},
),
const _AboutChin(),
],
),
),
),
),
);
}
}
class _AccountDropdown extends StatelessWidget {
const _AccountDropdown();
@override
Widget build(BuildContext context) {
return _AccountTile(
account: context.read<_Bloc>().activeAccount,
trailing: _BlocBuilder(
builder: (_, state) {
return AnimatedRotation(
turns: state.isOpenDropdown ? .5 : 0,
duration: k.animationDurationShort,
child: const Icon(Icons.arrow_drop_down_outlined),
);
},
),
onTap: () {
context.read<_Bloc>().add(const _ToggleDropdown());
},
);
}
}
class _AccountTile extends StatelessWidget {
const _AccountTile({
required this.account,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
final accountLabel = AccountPref.of(account).getAccountLabel();
return ListTile(
leading: SizedBox.square(
dimension: 48,
child: Center(child: _AccountIcon(account)),
),
title: accountLabel != null
? SizedBox(
height: 64,
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
accountLabel,
maxLines: 1,
overflow: TextOverflow.clip,
),
),
)
: Text(
account.address,
maxLines: 1,
overflow: TextOverflow.clip,
),
subtitle: accountLabel == null ? Text(account.username2) : null,
trailing: trailing,
onTap: onTap,
);
}
final Account account;
final Widget? trailing;
final VoidCallback? onTap;
}
class _AccountIcon extends StatelessWidget {
const _AccountIcon(this.account);
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(24),
child: CachedNetworkImage(
imageUrl: api_util.getAccountAvatarUrl(account, 64),
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
),
);
}
final Account account; final Account account;
} }
@npLog class _IconTile extends StatelessWidget {
class _AccountPickerDialogState extends State<AccountPickerDialog> { const _IconTile({
@override required this.icon,
initState() { required this.title,
super.initState(); this.onTap,
_accounts = Pref().getAccounts3Or([]); });
}
@override @override
build(BuildContext context) { Widget build(BuildContext context) {
final otherAccountOptions = return ListTile(
_accounts.where((a) => a != widget.account).map((a) { leading: SizedBox.square(
final label = AccountPref.of(a).getAccountLabel(); dimension: 48,
return SimpleDialogOption( child: Center(child: icon),
padding: const EdgeInsets.symmetric(horizontal: 8), ),
onPressed: () => _onItemPressed(a), title: title,
child: ListTile( onTap: onTap,
dense: true, );
title: Text(label ?? a.url), }
subtitle: label == null ? Text(a.username2) : null,
final Widget icon;
final Widget title;
final VoidCallback? onTap;
}
class _AccountView extends StatelessWidget {
const _AccountView({
required this.account,
});
@override
Widget build(BuildContext context) {
return _AccountTile(
account: account,
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
tooltip: L10n.global().deleteTooltip, tooltip: L10n.global().deleteTooltip,
onPressed: () => _onRemoveItemPressed(a),
),
),
);
}).toList();
final addAccountOptions = [
SimpleDialogOption(
padding: const EdgeInsets.all(8),
onPressed: () { onPressed: () {
context.read<_Bloc>().add(_DeleteAccount(account));
},
),
onTap: () {
context.read<_Bloc>().add(_SwitchAccount(account));
},
);
}
final Account account;
}
class _AccountSettingsView extends StatelessWidget {
const _AccountSettingsView();
@override
Widget build(BuildContext context) {
return _IconTile(
icon: const Icon(Icons.manage_accounts_outlined),
title: Text(L10n.global().accountSettingsTooltip),
onTap: () {
Navigator.of(context)
..pop()
..pushNamed(
AccountSettingsWidget.routeName,
arguments: AccountSettingsWidgetArguments(
context.read<_Bloc>().activeAccount),
);
},
);
}
}
class _NewAccountView extends StatelessWidget {
const _NewAccountView();
@override
Widget build(BuildContext context) {
return _IconTile(
icon: const Icon(Icons.add),
title: Text(L10n.global().addServerTooltip),
onTap: () {
Navigator.of(context) Navigator.of(context)
..pop() ..pop()
..pushNamed(SignIn.routeName); ..pushNamed(SignIn.routeName);
}, },
child: Tooltip( );
message: L10n.global().addServerTooltip, }
child: const Center( }
child: Icon(Icons.add),
class _AboutChin extends StatelessWidget {
const _AboutChin();
@override
Widget build(BuildContext context) {
return StreamBuilder<ServerStatus?>(
stream: context.read<_Bloc>().accountController.serverController.status,
initialData: context
.read<_Bloc>()
.accountController
.serverController
.status
.valueOrNull,
builder: (context, snapshot) {
var text = "${L10n.global().appTitle} ${k.versionStr}";
if (snapshot.hasData) {
final status = snapshot.requireData!;
text +=
" ${_getSymbol()} ${status.productName} ${status.versionName}";
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
text,
style: Theme.of(context).textTheme.bodySmall,
), ),
), );
), },
];
final accountLabel = AccountPref.of(widget.account).getAccountLabel();
return SimpleDialog(
title: ListTile(
dense: true,
title: Text(
accountLabel ?? widget.account.url,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: accountLabel == null
? Text(
widget.account.username2,
style: const TextStyle(fontWeight: FontWeight.bold),
)
: null,
trailing: IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: L10n.global().settingsMenuLabel,
onPressed: _onEditPressed,
),
),
titlePadding: const EdgeInsetsDirectional.fromSTEB(8, 16, 8, 0),
contentPadding: const EdgeInsetsDirectional.fromSTEB(0, 12, 0, 8),
children: otherAccountOptions + addAccountOptions,
); );
} }
void _onItemPressed(Account account) { String _getSymbol() {
Pref().setCurrentAccountIndex(_accounts.indexOf(account)); final today = clock.now();
Navigator.of(context).pushNamedAndRemoveUntil(Home.routeName, (_) => false, if (today.month == 1 && today.day == 1) {
arguments: HomeArguments(account)); // firework
} return "\u{1f386}";
} else if (today.month == 4 && today.day == 10) {
Future<void> _onRemoveItemPressed(Account account) async { // initial commit!
try { return "\u{1f382}";
await _removeAccount(account); } else {
setState(() { const symbols = [
_accounts = Pref().getAccounts3()!; // cloud
}); "\u2601",
SnackBarManager().showSnackBar(SnackBar( // heart
content: "\u2665",
Text(L10n.global().removeServerSuccessNotification(account.url)), // star
duration: k.snackBarDurationNormal, "\u2b50",
)); // rainbow
} catch (e) { "\u{1f308}",
SnackBarManager().showSnackBar(SnackBar( // globe
content: Text(exception_util.toUserString(e)), "\u{1f310}",
duration: k.snackBarDurationNormal, // clover
)); "\u{1f340}",
];
return symbols[Random(_seed).nextInt(symbols.length)];
} }
} }
void _onEditPressed() { static final _seed = Random().nextInt(65536);
Navigator.of(context)
..pop()
..pushNamed(AccountSettingsWidget.routeName,
arguments: AccountSettingsWidgetArguments(widget.account));
}
Future<void> _removeAccount(Account account) async {
_log.info("[_removeAccount] Remove account: $account");
final accounts = Pref().getAccounts3()!;
final currentAccount = accounts[Pref().getCurrentAccountIndex()!];
accounts.remove(account);
final newAccountIndex = accounts.indexOf(currentAccount);
if (newAccountIndex == -1) {
throw StateError("Active account not found in resulting account list");
}
try {
await AccountPref.of(account).provider.clear();
} catch (e, stackTrace) {
_log.shout(
"[_removeAccount] Failed while removing account pref", e, stackTrace);
}
unawaited(Pref().setAccounts3(accounts));
unawaited(Pref().setCurrentAccountIndex(newAccountIndex));
// check if the same account (server + userId) still exists in known
// accounts
if (!accounts
.any((a) => a.url == account.url && a.userId == account.userId)) {
// account removed, clear cache db
await _removeAccountFromDb(account);
}
}
Future<void> _removeAccountFromDb(Account account) async {
try {
final c = KiwiContainer().resolve<DiContainer>();
await c.sqliteDb.use((db) async {
await db.deleteAccountOf(account);
});
} catch (e, stackTrace) {
_log.shout("[_removeAccountFromDb] Failed while removing account from db",
e, stackTrace);
}
}
late List<Account> _accounts;
} }

View file

@ -2,14 +2,96 @@
part of 'account_picker_dialog.dart'; part of 'account_picker_dialog.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call(
{List<Account>? accounts,
bool? isOpenDropdown,
Account? newSelectAccount,
ExceptionEvent? error});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call(
{dynamic accounts,
dynamic isOpenDropdown,
dynamic newSelectAccount = copyWithNull,
dynamic error = copyWithNull}) {
return _State(
accounts: accounts as List<Account>? ?? that.accounts,
isOpenDropdown: isOpenDropdown as bool? ?? that.isOpenDropdown,
newSelectAccount: newSelectAccount == copyWithNull
? that.newSelectAccount
: newSelectAccount as Account?,
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 // NpLogGenerator
// ************************************************************************** // **************************************************************************
extension _$_AccountPickerDialogStateNpLog on _AccountPickerDialogState { extension _$_BlocNpLog on _Bloc {
// ignore: unused_element // ignore: unused_element
Logger get _log => log; Logger get _log => log;
static final log = static final log = Logger("widget.account_picker_dialog._Bloc");
Logger("widget.account_picker_dialog._AccountPickerDialogState"); }
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {accounts: [length: ${accounts.length}], isOpenDropdown: $isOpenDropdown, newSelectAccount: $newSelectAccount, error: $error}";
}
}
extension _$_ToggleDropdownToString on _ToggleDropdown {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ToggleDropdown {}";
}
}
extension _$_SwitchAccountToString on _SwitchAccount {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SwitchAccount {account: $account}";
}
}
extension _$_DeleteAccountToString on _DeleteAccount {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_DeleteAccount {account: $account}";
}
}
extension _$_SetErrorToString on _SetError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetError {error: $error, stackTrace: $stackTrace}";
}
} }

View file

@ -0,0 +1,111 @@
part of '../account_picker_dialog.dart';
@npLog
class _Bloc extends Bloc<_Event, _State> implements BlocTag {
_Bloc({
required DiContainer container,
required this.accountController,
}) : _c = container,
super(_State.init(
accounts: container.pref.getAccounts3Or([]),
)) {
on<_ToggleDropdown>(_onToggleDropdown);
on<_SwitchAccount>(_onSwitchAccount);
on<_DeleteAccount>(_onDeleteAccount);
on<_SetError>(_onSetError);
}
@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 _onToggleDropdown(_ToggleDropdown ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(isOpenDropdown: !state.isOpenDropdown));
}
Future<void> _onSwitchAccount(_SwitchAccount ev, Emitter<_State> emit) async {
_log.info(ev);
await _prefLock.protect(() async {
final index = state.accounts.indexOf(ev.account);
if (index == -1) {
throw StateError("Account not found");
}
await _c.pref.setCurrentAccountIndex(index);
emit(state.copyWith(newSelectAccount: ev.account));
});
}
Future<void> _onDeleteAccount(_DeleteAccount ev, Emitter<_State> emit) async {
_log.info(ev);
emit(state.copyWith(
accounts: state.accounts.where((a) => a.id != ev.account.id).toList(),
));
try {
await _prefLock.protect(() async {
final accounts = _c.pref.getAccounts3()!;
final currentAccount = accounts[_c.pref.getCurrentAccountIndex()!];
accounts.remove(ev.account);
final newAccountIndex = accounts.indexOf(currentAccount);
if (newAccountIndex == -1) {
throw StateError(
"Active account not found in resulting account list");
}
try {
await AccountPref.of(ev.account).provider.clear();
} catch (e, stackTrace) {
_log.shout("[_onDeleteAccount] Failed while removing account pref", e,
stackTrace);
}
await Pref().setAccounts3(accounts);
await Pref().setCurrentAccountIndex(newAccountIndex);
// check if the same account (server + userId) still exists in known
// accounts
if (!accounts.any(
(a) => a.url == ev.account.url && a.userId == ev.account.userId)) {
// account removed, clear cache db
unawaited(_removeAccountFromDb(ev.account));
}
});
} catch (e) {
rethrow;
}
}
void _onSetError(_SetError ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
}
Future<void> _removeAccountFromDb(Account account) async {
try {
await _c.sqliteDb.use((db) async {
await db.deleteAccountOf(account);
});
} catch (e, stackTrace) {
_log.shout("[_removeAccountFromDb] Failed while removing account from db",
e, stackTrace);
}
}
@override
String get tag => _log.fullName;
final DiContainer _c;
final AccountController accountController;
late final Account activeAccount = accountController.account;
final _prefLock = Mutex();
var _isHandlingError = false;
}

View file

@ -0,0 +1,72 @@
part of '../account_picker_dialog.dart';
@genCopyWith
@toString
class _State {
const _State({
required this.accounts,
required this.isOpenDropdown,
this.newSelectAccount,
this.error,
});
factory _State.init({
required List<Account> accounts,
}) =>
_State(
accounts: accounts,
isOpenDropdown: false,
);
@override
String toString() => _$toString();
final List<Account> accounts;
final bool isOpenDropdown;
final Account? newSelectAccount;
final ExceptionEvent? error;
}
abstract class _Event {
const _Event();
}
@toString
class _ToggleDropdown implements _Event {
const _ToggleDropdown();
@override
String toString() => _$toString();
}
@toString
class _SwitchAccount implements _Event {
const _SwitchAccount(this.account);
@override
String toString() => _$toString();
final Account account;
}
@toString
class _DeleteAccount implements _Event {
const _DeleteAccount(this.account);
@override
String toString() => _$toString();
final Account account;
}
@toString
class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]);
@override
String toString() => _$toString();
final Object error;
final StackTrace? stackTrace;
}

View file

@ -4,16 +4,12 @@ import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart'; import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/help_utils.dart' as help_utils;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/widget/account_picker_dialog.dart'; import 'package:nc_photos/widget/account_picker_dialog.dart';
import 'package:nc_photos/widget/app_bar_circular_progress_indicator.dart'; import 'package:nc_photos/widget/app_bar_circular_progress_indicator.dart';
import 'package:nc_photos/widget/app_bar_title_container.dart'; import 'package:nc_photos/widget/app_bar_title_container.dart';
import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/translucent_sliver_app_bar.dart'; import 'package:nc_photos/widget/translucent_sliver_app_bar.dart';
/// AppBar for home screens /// AppBar for home screens
@ -35,9 +31,7 @@ class HomeSliverAppBar extends StatelessWidget {
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (_) => AccountPickerDialog( builder: (_) => const AccountPickerDialog(),
account: account,
),
); );
}, },
child: AppBarTitleContainer( child: AppBarTitleContainer(
@ -79,30 +73,13 @@ class HomeSliverAppBar extends StatelessWidget {
_DarkModeSwitch( _DarkModeSwitch(
onChanged: _onDarkModeChanged, onChanged: _onDarkModeChanged,
), ),
if (menuActions?.isNotEmpty == true)
PopupMenuButton<int>( PopupMenuButton<int>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip, tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) => itemBuilder: (_) => menuActions!,
(menuActions ?? []) +
[
PopupMenuItem(
value: _menuValueAbout,
child: Text(L10n.global().settingsMenuLabel),
),
PopupMenuItem(
value: _menuValueHelp,
child: Text(L10n.global().helpTooltip),
),
],
onSelected: (option) { onSelected: (option) {
if (option >= 0) { if (option >= 0) {
onSelectedMenuActions?.call(option); onSelectedMenuActions?.call(option);
} else {
if (option == _menuValueAbout) {
Navigator.of(context).pushNamed(Settings.routeName,
arguments: SettingsArguments(account));
} else if (option == _menuValueHelp) {
launch(help_utils.mainUrl);
}
} }
}, },
), ),
@ -126,9 +103,6 @@ class HomeSliverAppBar extends StatelessWidget {
final List<PopupMenuEntry<int>>? menuActions; final List<PopupMenuEntry<int>>? menuActions;
final void Function(int)? onSelectedMenuActions; final void Function(int)? onSelectedMenuActions;
final bool isShowProgressIcon; final bool isShowProgressIcon;
static const _menuValueAbout = -1;
static const _menuValueHelp = -2;
} }
class _DarkModeSwitch extends StatelessWidget { class _DarkModeSwitch extends StatelessWidget {

View file

@ -2,14 +2,11 @@ import 'dart:async';
import 'package:event_bus/event_bus.dart'; import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart'; import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/server_status.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -132,12 +129,6 @@ class _SettingsState extends State<Settings> {
value: _shouldProcessExifWifiOnly, value: _shouldProcessExifWifiOnly,
onChanged: _isEnableExif ? _onExifWifiOnlyChanged : null, onChanged: _isEnableExif ? _onExifWifiOnlyChanged : null,
), ),
_buildSubSettings(
context,
leading: const Icon(Icons.manage_accounts_outlined),
label: L10n.global().settingsAccountTitle,
builder: () => AccountSettingsWidget(account: widget.account),
),
_buildSubSettings( _buildSubSettings(
context, context,
leading: const Icon(Icons.image_outlined), leading: const Icon(Icons.image_outlined),
@ -213,29 +204,6 @@ class _SettingsState extends State<Settings> {
} }
}, },
), ),
StreamBuilder<ServerStatus?>(
stream:
context.read<AccountController>().serverController.status,
initialData: context
.read<AccountController>()
.serverController
.status
.valueOrNull,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return ListTile(
title: Text(L10n.global().settingsServerVersionTitle),
);
} else {
final status = snapshot.requireData!;
return ListTile(
title: Text(L10n.global().settingsServerVersionTitle),
subtitle: Text(
"${status.productName} ${status.majorVersion} (${status.versionName})"),
);
}
},
),
ListTile( ListTile(
title: Text(L10n.global().settingsSourceCodeTitle), title: Text(L10n.global().settingsSourceCodeTitle),
onTap: () { onTap: () {