Browse Source

Merge pull request #53 from jld3103/feature/improve-multi-account-ui

neon: Improve multi-account UI
pull/54/head
jld3103 2 years ago committed by GitHub
parent
commit
2e17fade97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      packages/neon/lib/l10n/en.arb
  2. 12
      packages/neon/lib/l10n/localizations.dart
  3. 9
      packages/neon/lib/l10n/localizations_en.dart
  4. 3
      packages/neon/lib/src/apps/files/blocs/files.dart
  5. 2
      packages/neon/lib/src/blocs/accounts.dart
  6. 8
      packages/neon/lib/src/models/account.dart
  7. 67
      packages/neon/lib/src/pages/home/home.dart
  8. 14
      packages/neon/lib/src/pages/settings/account_specific_settings.dart
  9. 57
      packages/neon/lib/src/pages/settings/settings.dart
  10. 10
      packages/neon/lib/src/pages/settings/widgets/account_settings_tile.dart
  11. 23
      packages/neon/lib/src/utils/global_options.dart
  12. 66
      packages/neon/lib/src/widgets/account_tile.dart
  13. 10
      packages/settings/lib/src/options/option.dart

12
packages/neon/lib/l10n/en.arb

@ -94,18 +94,16 @@
"globalOptionsSystemTrayEnabled": "Enable system tray",
"globalOptionsSystemTrayHideToTrayWhenMinimized": "Hide to system tray when minimized",
"globalOptionsAccountsRememberLastUsedAccount": "Remember last used account",
"globalOptionsAccountsRemoveConfirm": "Are you sure you want to remove the account {name} from {url}?",
"@globalOptionsAccountsRemoveConfirm": {
"globaloptionsaccountsInitialAccount": "Initial account",
"globalOptionsAccountsAdd": "Add account",
"accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?",
"@accountOptionsRemoveConfirm": {
"placeholders": {
"name": {
"type": "String"
},
"url": {
"id": {
"type": "String"
}
}
},
"globalOptionsAccountsAdd": "Add account",
"accountOptionsInitialApp": "App to show initially",
"accountOptionsAutomatic": "Automatic",
"licenses": "Licenses",

12
packages/neon/lib/l10n/localizations.dart

@ -485,11 +485,11 @@ abstract class AppLocalizations {
/// **'Remember last used account'**
String get globalOptionsAccountsRememberLastUsedAccount;
/// No description provided for @globalOptionsAccountsRemoveConfirm.
/// No description provided for @globaloptionsaccountsInitialAccount.
///
/// In en, this message translates to:
/// **'Are you sure you want to remove the account {name} from {url}?'**
String globalOptionsAccountsRemoveConfirm(String name, String url);
/// **'Initial account'**
String get globaloptionsaccountsInitialAccount;
/// No description provided for @globalOptionsAccountsAdd.
///
@ -497,6 +497,12 @@ abstract class AppLocalizations {
/// **'Add account'**
String get globalOptionsAccountsAdd;
/// No description provided for @accountOptionsRemoveConfirm.
///
/// In en, this message translates to:
/// **'Are you sure you want to remove the account {id}?'**
String accountOptionsRemoveConfirm(String id);
/// No description provided for @accountOptionsInitialApp.
///
/// In en, this message translates to:

9
packages/neon/lib/l10n/localizations_en.dart

@ -216,13 +216,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get globalOptionsAccountsRememberLastUsedAccount => 'Remember last used account';
@override
String globalOptionsAccountsRemoveConfirm(String name, String url) {
return 'Are you sure you want to remove the account $name from $url?';
}
String get globaloptionsaccountsInitialAccount => 'Initial account';
@override
String get globalOptionsAccountsAdd => 'Add account';
@override
String accountOptionsRemoveConfirm(String id) {
return 'Are you sure you want to remove the account $id?';
}
@override
String get accountOptionsInitialApp => 'App to show initially';

3
packages/neon/lib/src/apps/files/blocs/files.dart

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:neon/src/apps/files/app.dart';
import 'package:neon/src/apps/files/blocs/browser.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:open_file/open_file.dart';
@ -79,7 +80,7 @@ class FilesBloc extends $FilesBloc {
final file = File(
p.join(
await _platform.getUserAccessibleAppDataPath(),
'${client.username!}@${Uri.parse(client.baseURL).host}',
client.humanReadableID,
'files',
path.join(Platform.pathSeparator),
),

2
packages/neon/lib/src/blocs/accounts.dart

@ -106,7 +106,7 @@ class AccountsBloc extends $AccountsBloc {
final lastUsedAccountID = _storage.getString(_keyLastUsedAccount);
_activeAccountSubject.add(accounts.singleWhere((final account) => account.id == lastUsedAccountID));
} else {
_globalOptions.lastAccount.stream.first.then((final lastAccount) {
_globalOptions.initialAccount.stream.first.then((final lastAccount) {
final matches = accounts.where((final account) => account.id == lastAccount).toList();
if (matches.isNotEmpty) {
_activeAccountSubject.add(matches[0]);

8
packages/neon/lib/src/models/account.dart

@ -70,7 +70,7 @@ class Account {
Map<String, String> _idCache = {};
extension NextcloudClientID on NextcloudClient {
extension NextcloudClientHelpers on NextcloudClient {
String get id {
final key = '$username@$baseURL';
if (_idCache[key] != null) {
@ -78,6 +78,12 @@ extension NextcloudClientID on NextcloudClient {
}
return _idCache[key] = sha1.convert(utf8.encode(key)).toString();
}
String get humanReadableID {
final uri = Uri.parse(baseURL);
// Maybe also show path if it is not '/' ?
return '${username!}@${uri.port != 443 ? '${uri.host}:${uri.port}' : uri.host}';
}
}
class AccountSpecificOptions {

67
packages/neon/lib/src/pages/home/home.dart

@ -327,19 +327,26 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
_scaffoldKey.currentState!.openDrawer();
return false;
},
child: Scaffold(
child: Builder(
builder: (final context) {
if (accountsSnapshot.hasData) {
final accounts = accountsSnapshot.data!;
final account = accounts.singleWhere((final account) => account.id == widget.account.id);
return Scaffold(
key: _scaffoldKey,
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Row(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
Row(
children: [
if (appsData != null && activeAppIDSnapshot.hasData) ...[
Flexible(
child: Text(
appsData.singleWhere((final a) => a.id == activeAppIDSnapshot.data!).name(context),
appsData
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!)
.name(context),
),
),
],
@ -366,14 +373,17 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
),
),
],
const SizedBox(
width: 8,
),
],
),
if (accounts.length > 1) ...[
Text(
account.client.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
Row(
children: [
actions: [
if (appsData != null && activeAppIDSnapshot.hasData) ...[
IconButton(
icon: const Icon(Icons.settings),
@ -388,30 +398,25 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
);
},
),
Builder(
builder: (final context) {
if (accountsSnapshot.hasData) {
final matches = accountsSnapshot.data!
.where(
(final account) => account.id == widget.account.id,
)
.toList();
if (matches.length == 1) {
return AccountAvatar(
account: matches[0],
IconButton(
icon: AccountAvatar(
account: account,
requestManager: _requestManager,
),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => AccountSpecificSettingsPage(
bloc: accountsBloc,
account: account,
),
),
);
}
}
return Container();
},
),
],
],
),
],
),
),
drawer: Drawer(
child: Column(
children: [
@ -472,6 +477,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
value: account.id,
child: AccountTile(
account: account,
dense: true,
),
),
)
@ -515,7 +521,8 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(appImplementation.name(context)),
if (unreadCounterSnapshot.hasData && unreadCounterSnapshot.data! > 0) ...[
if (unreadCounterSnapshot.hasData &&
unreadCounterSnapshot.data! > 0) ...[
Text(
unreadCounterSnapshot.data!.toString(),
style: TextStyle(
@ -591,6 +598,10 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
],
],
),
);
}
return Container();
},
),
),
),

14
packages/neon/lib/src/pages/settings/account_specific_settings.dart

@ -11,7 +11,7 @@ class AccountSpecificSettingsPage extends StatelessWidget {
final Account account;
late final _options = bloc.getOptions(account)!;
late final _name = '${account.username}@${Uri.parse(account.serverURL).host}';
late final _name = account.client.humanReadableID;
@override
Widget build(final BuildContext context) => Scaffold(
@ -19,6 +19,18 @@ class AccountSpecificSettingsPage extends StatelessWidget {
appBar: AppBar(
title: Text(_name),
actions: [
IconButton(
onPressed: () async {
if (await showConfirmationDialog(
context,
AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID),
)) {
bloc.removeAccount(account);
Navigator.of(context).pop();
}
},
icon: const Icon(MdiIcons.delete),
),
IconButton(
onPressed: () async {
if (await showConfirmationDialog(

57
packages/neon/lib/src/pages/settings/settings.dart

@ -72,11 +72,7 @@ class _SettingsPageState extends State<SettingsPage> {
final context,
final pushNotificationsEnabledEnabledSnapshot,
) =>
OptionBuilder<bool>(
option: globalOptions.rememberLastUsedAccount,
builder: (final context, final rememberLastUsedAccount) => OptionBuilder<String?>(
option: globalOptions.lastAccount,
builder: (final context, final lastAccount) => SettingsList(
SettingsList(
categories: [
SettingsCategory(
title: Text(AppLocalizations.of(context).settingsApps),
@ -176,31 +172,15 @@ class _SettingsPageState extends State<SettingsPage> {
CheckBoxSettingsTile(
option: globalOptions.rememberLastUsedAccount,
),
DropdownButtonSettingsTile(
option: globalOptions.initialAccount,
),
],
for (final account in accountsSnapshot.data!) ...[
AccountSettingsTile(
account: account,
color: activeAccountSnapshot.data == account && accountsSnapshot.data!.length > 1
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
PopupMenuButton<SettingsAccountAction>(
itemBuilder: (final context) => [
PopupMenuItem(
value: SettingsAccountAction.settings,
child: Text(AppLocalizations.of(context).settings),
),
PopupMenuItem(
value: SettingsAccountAction.delete,
child: Text(AppLocalizations.of(context).delete),
),
],
onSelected: (final action) async {
switch (action) {
case SettingsAccountAction.settings:
await Navigator.of(context).push(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => AccountSpecificSettingsPage(
bloc: accountsBloc,
@ -208,31 +188,8 @@ class _SettingsPageState extends State<SettingsPage> {
),
),
);
break;
case SettingsAccountAction.delete:
if (await showConfirmationDialog(
context,
AppLocalizations.of(context).globalOptionsAccountsRemoveConfirm(
account.username,
account.serverURL,
),
)) {
accountsBloc.removeAccount(account);
}
break;
}
},
),
if (accountsSnapshot.data!.length > 1 && rememberLastUsedAccount != null) ...[
Radio<String>(
groupValue: lastAccount,
value: account.id,
onChanged: !rememberLastUsedAccount ? globalOptions.lastAccount.set : null,
),
],
],
),
),
],
CustomSettingsTile(
title: ElevatedButton.icon(
@ -337,8 +294,6 @@ class _SettingsPageState extends State<SettingsPage> {
],
),
),
),
),
);
},
),

10
packages/neon/lib/src/pages/settings/widgets/account_settings_tile.dart

@ -5,22 +5,20 @@ class AccountSettingsTile extends SettingsTile {
required this.account,
this.color,
this.trailing,
this.onTap,
super.key,
});
final Account account;
final Color? color;
final Widget? trailing;
final VoidCallback? onTap;
@override
Widget build(final BuildContext context) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: AccountTile(
Widget build(final BuildContext context) => AccountTile(
account: account,
color: color,
trailing: trailing,
),
onTap: onTap,
);
}

23
packages/neon/lib/src/utils/global_options.dart

@ -22,20 +22,26 @@ class GlobalOptions {
});
rememberLastUsedAccount.stream.listen((final remember) async {
_initialAccountEnabledSubject.add(!remember);
if (remember) {
await lastAccount.set(null);
await initialAccount.set(null);
} else {
await lastAccount.set((await lastAccount.values.first).keys.toList()[0]);
// Only override the initial account if there already has been a value,
// which means it's not the initial emit from rememberLastUsedAccount
if (initialAccount.hasValue) {
await initialAccount.set((await initialAccount.values.first).keys.toList()[0]);
}
}
});
}
final Storage _storage;
final PackageInfo _packageInfo;
final _accountsIDsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
final _themeOLEDAsDarkEnabledSubject = BehaviorSubject<bool>();
final _pushNotificationsEnabledEnabledSubject = BehaviorSubject<bool>();
final _pushNotificationsDistributorsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
final _accountsIDsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
final _initialAccountEnabledSubject = BehaviorSubject<bool>();
late final _distributorsMap = <String, String Function(BuildContext)>{
_packageInfo.packageName: (final context) =>
@ -61,7 +67,7 @@ class GlobalOptions {
systemTrayEnabled,
systemTrayHideToTrayWhenMinimized,
rememberLastUsedAccount,
lastAccount,
initialAccount,
];
Future reset() async {
@ -84,7 +90,7 @@ class GlobalOptions {
}
_accountsIDsSubject.add({
for (final account in accounts) ...{
account.id: (final _) => '',
account.id: (final context) => account.client.humanReadableID,
},
});
}
@ -183,11 +189,12 @@ class GlobalOptions {
defaultValue: BehaviorSubject.seeded(true),
);
late final lastAccount = SelectOption<String?>(
late final initialAccount = SelectOption<String?>(
storage: _storage,
key: 'last-account',
label: (final _) => '',
key: 'initial-account',
label: (final context) => AppLocalizations.of(context).globaloptionsaccountsInitialAccount,
defaultValue: BehaviorSubject.seeded(null),
values: _accountsIDsSubject,
enabled: _initialAccountEnabledSubject,
);
}

66
packages/neon/lib/src/widgets/account_tile.dart

@ -3,14 +3,18 @@ part of '../neon.dart';
class AccountTile extends StatefulWidget {
const AccountTile({
required this.account,
this.trailing,
this.color,
this.trailing,
this.onTap,
this.dense = false,
super.key,
});
final Account account;
final Widget? trailing;
final Color? color;
final Widget? trailing;
final VoidCallback? onTap;
final bool dense;
@override
State<AccountTile> createState() => _AccountTileState();
@ -27,21 +31,21 @@ class _AccountTileState extends State<AccountTile> {
}
@override
Widget build(final BuildContext context) => Row(
children: [
AccountAvatar(
Widget build(final BuildContext context) => ListTile(
onTap: widget.onTap,
dense: widget.dense,
contentPadding: widget.dense ? EdgeInsets.zero : null,
visualDensity: widget.dense
? const VisualDensity(
horizontal: -4,
vertical: -4,
)
: null,
leading: AccountAvatar(
account: widget.account,
requestManager: Provider.of<RequestManager>(context),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StandardRxResultBuilder<UserDetailsBloc, ProvisioningApiUserDetails>(
title: StandardRxResultBuilder<UserDetailsBloc, ProvisioningApiUserDetails>(
bloc: _userDetailsBloc,
state: (final bloc) => bloc.userDetails,
builder: (
@ -60,29 +64,6 @@ class _AccountTileState extends State<AccountTile> {
color: widget.color,
),
),
const SizedBox(
width: 5,
),
Text(
'(',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: widget.color,
),
),
],
Text(
widget.account.username,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: widget.color,
),
),
if (userDetailsData != null) ...[
Text(
')',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: widget.color,
),
),
],
if (userDetailsLoading) ...[
const SizedBox(
@ -110,18 +91,11 @@ class _AccountTileState extends State<AccountTile> {
],
),
),
Text(
widget.account.serverURL,
subtitle: Text(
widget.account.client.humanReadableID,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: widget.color,
),
),
],
),
),
if (widget.trailing != null) ...[
widget.trailing!,
],
],
);
}

10
packages/settings/lib/src/options/option.dart

@ -27,11 +27,19 @@ abstract class Option<T> {
late BehaviorSubject<T> stream;
T get value {
if (hasValue) {
return stream.value;
}
return defaultValue.value;
}
bool get hasValue {
if (!enabled.value) {
throw OptionDisableException();
}
return stream.value ?? defaultValue.value;
return stream.hasValue;
}
Future reset() async {

Loading…
Cancel
Save