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",
"removeCollectionsFailedNotification": "Failed to remove some collections",
"accountSettingsTooltip": "Account settings",
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": {

View file

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

View file

@ -1,176 +1,386 @@
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_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:mutex/mutex.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/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.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/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/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/settings.dart';
import 'package:nc_photos/widget/sign_in.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/bloc.dart';
part 'account_picker_dialog/state_event.dart';
/// A dialog that allows the user to switch between accounts
class AccountPickerDialog extends StatefulWidget {
const AccountPickerDialog({
Key? key,
required this.account,
}) : super(key: key);
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
class AccountPickerDialog extends StatelessWidget {
const AccountPickerDialog({super.key});
@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;
}
@npLog
class _AccountPickerDialogState extends State<AccountPickerDialog> {
@override
initState() {
super.initState();
_accounts = Pref().getAccounts3Or([]);
}
class _IconTile extends StatelessWidget {
const _IconTile({
required this.icon,
required this.title,
this.onTap,
});
@override
build(BuildContext context) {
final otherAccountOptions =
_accounts.where((a) => a != widget.account).map((a) {
final label = AccountPref.of(a).getAccountLabel();
return SimpleDialogOption(
padding: const EdgeInsets.symmetric(horizontal: 8),
onPressed: () => _onItemPressed(a),
child: ListTile(
dense: true,
title: Text(label ?? a.url),
subtitle: label == null ? Text(a.username2) : null,
trailing: IconButton(
icon: const Icon(Icons.close),
tooltip: L10n.global().deleteTooltip,
onPressed: () => _onRemoveItemPressed(a),
),
),
);
}).toList();
final addAccountOptions = [
SimpleDialogOption(
padding: const EdgeInsets.all(8),
onPressed: () {
Navigator.of(context)
..pop()
..pushNamed(SignIn.routeName);
},
child: Tooltip(
message: L10n.global().addServerTooltip,
child: const Center(
child: Icon(Icons.add),
),
),
Widget build(BuildContext context) {
return ListTile(
leading: SizedBox.square(
dimension: 48,
child: Center(child: icon),
),
];
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,
title: title,
onTap: onTap,
);
}
void _onItemPressed(Account account) {
Pref().setCurrentAccountIndex(_accounts.indexOf(account));
Navigator.of(context).pushNamedAndRemoveUntil(Home.routeName, (_) => false,
arguments: HomeArguments(account));
}
Future<void> _onRemoveItemPressed(Account account) async {
try {
await _removeAccount(account);
setState(() {
_accounts = Pref().getAccounts3()!;
});
SnackBarManager().showSnackBar(SnackBar(
content:
Text(L10n.global().removeServerSuccessNotification(account.url)),
duration: k.snackBarDurationNormal,
));
} catch (e) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
}
}
void _onEditPressed() {
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;
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(
icon: const Icon(Icons.close),
tooltip: L10n.global().deleteTooltip,
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)
..pop()
..pushNamed(SignIn.routeName);
},
);
}
}
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,
),
);
},
);
}
String _getSymbol() {
final today = clock.now();
if (today.month == 1 && today.day == 1) {
// firework
return "\u{1f386}";
} else if (today.month == 4 && today.day == 10) {
// initial commit!
return "\u{1f382}";
} else {
const symbols = [
// cloud
"\u2601",
// heart
"\u2665",
// star
"\u2b50",
// rainbow
"\u{1f308}",
// globe
"\u{1f310}",
// clover
"\u{1f340}",
];
return symbols[Random(_seed).nextInt(symbols.length)];
}
}
static final _seed = Random().nextInt(65536);
}

View file

@ -2,14 +2,96 @@
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
// **************************************************************************
extension _$_AccountPickerDialogStateNpLog on _AccountPickerDialogState {
extension _$_BlocNpLog on _Bloc {
// ignore: unused_element
Logger get _log => log;
static final log =
Logger("widget.account_picker_dialog._AccountPickerDialogState");
static final log = Logger("widget.account_picker_dialog._Bloc");
}
// **************************************************************************
// 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: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/event/event.dart';
import 'package:nc_photos/help_utils.dart' as help_utils;
import 'package:nc_photos/pref.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/app_bar_circular_progress_indicator.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';
/// AppBar for home screens
@ -35,9 +31,7 @@ class HomeSliverAppBar extends StatelessWidget {
onTap: () {
showDialog(
context: context,
builder: (_) => AccountPickerDialog(
account: account,
),
builder: (_) => const AccountPickerDialog(),
);
},
child: AppBarTitleContainer(
@ -79,33 +73,16 @@ class HomeSliverAppBar extends StatelessWidget {
_DarkModeSwitch(
onChanged: _onDarkModeChanged,
),
PopupMenuButton<int>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) =>
(menuActions ?? []) +
[
PopupMenuItem(
value: _menuValueAbout,
child: Text(L10n.global().settingsMenuLabel),
),
PopupMenuItem(
value: _menuValueHelp,
child: Text(L10n.global().helpTooltip),
),
],
onSelected: (option) {
if (option >= 0) {
onSelectedMenuActions?.call(option);
} else {
if (option == _menuValueAbout) {
Navigator.of(context).pushNamed(Settings.routeName,
arguments: SettingsArguments(account));
} else if (option == _menuValueHelp) {
launch(help_utils.mainUrl);
if (menuActions?.isNotEmpty == true)
PopupMenuButton<int>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (_) => menuActions!,
onSelected: (option) {
if (option >= 0) {
onSelectedMenuActions?.call(option);
}
}
},
),
},
),
],
);
}
@ -126,9 +103,6 @@ class HomeSliverAppBar extends StatelessWidget {
final List<PopupMenuEntry<int>>? menuActions;
final void Function(int)? onSelectedMenuActions;
final bool isShowProgressIcon;
static const _menuValueAbout = -1;
static const _menuValueHelp = -2;
}
class _DarkModeSwitch extends StatelessWidget {

View file

@ -2,14 +2,11 @@ import 'dart:async';
import 'package:event_bus/event_bus.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/debug_util.dart';
import 'package:nc_photos/entity/server_status.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;
@ -132,12 +129,6 @@ class _SettingsState extends State<Settings> {
value: _shouldProcessExifWifiOnly,
onChanged: _isEnableExif ? _onExifWifiOnlyChanged : null,
),
_buildSubSettings(
context,
leading: const Icon(Icons.manage_accounts_outlined),
label: L10n.global().settingsAccountTitle,
builder: () => AccountSettingsWidget(account: widget.account),
),
_buildSubSettings(
context,
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(
title: Text(L10n.global().settingsSourceCodeTitle),
onTap: () {