Browse Source

Merge pull request #68 from jld3103/feature/navigation-modes

neon: Introduce navigation modes
pull/70/head
jld3103 2 years ago committed by GitHub
parent
commit
83d5c9a35c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      packages/neon/lib/l10n/en.arb
  2. 34
      packages/neon/lib/l10n/localizations.dart
  3. 17
      packages/neon/lib/l10n/localizations_en.dart
  4. 19
      packages/neon/lib/src/app.dart
  5. 7
      packages/neon/lib/src/apps/files/widgets/file_preview.dart
  6. 14
      packages/neon/lib/src/apps/news/pages/article.dart
  7. 4
      packages/neon/lib/src/blocs/apps.dart
  8. 1
      packages/neon/lib/src/neon.dart
  9. 346
      packages/neon/lib/src/pages/home/home.dart
  10. 8
      packages/neon/lib/src/pages/settings/settings.dart
  11. 3
      packages/neon/lib/src/utils/app_implementation.dart
  12. 26
      packages/neon/lib/src/utils/global_options.dart
  13. 9
      packages/neon/lib/src/widgets/account_avatar.dart
  14. 24
      packages/neon/lib/src/widgets/account_tile.dart
  15. 8
      packages/neon/lib/src/widgets/cached_url_image.dart
  16. 13
      packages/neon/lib/src/widgets/custom_linear_progress_indicator.dart
  17. 29
      packages/neon/lib/src/widgets/exception.dart
  18. 11
      packages/neon/lib/src/widgets/no_animation_page_route.dart

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

@ -72,6 +72,7 @@
"optionsCategoryAccounts": "Accounts", "optionsCategoryAccounts": "Accounts",
"optionsCategoryStartup": "Startup", "optionsCategoryStartup": "Startup",
"optionsCategorySystemTray": "System tray", "optionsCategorySystemTray": "System tray",
"optionsCategoryNavigation": "Navigation",
"optionsSortOrderAscending": "Ascending", "optionsSortOrderAscending": "Ascending",
"optionsSortOrderDescending": "Descending", "optionsSortOrderDescending": "Descending",
"globalOptionsThemeMode": "Theme mode", "globalOptionsThemeMode": "Theme mode",
@ -94,8 +95,12 @@
"globalOptionsSystemTrayEnabled": "Enable system tray", "globalOptionsSystemTrayEnabled": "Enable system tray",
"globalOptionsSystemTrayHideToTrayWhenMinimized": "Hide to system tray when minimized", "globalOptionsSystemTrayHideToTrayWhenMinimized": "Hide to system tray when minimized",
"globalOptionsAccountsRememberLastUsedAccount": "Remember last used account", "globalOptionsAccountsRememberLastUsedAccount": "Remember last used account",
"globaloptionsaccountsInitialAccount": "Initial account", "globalOptionsAccountsInitialAccount": "Initial account",
"globalOptionsAccountsAdd": "Add account", "globalOptionsAccountsAdd": "Add account",
"globalOptionsNavigationMode": "Navigation mode",
"globalOptionsNavigationModeDrawer": "Drawer",
"globalOptionsNavigationModeDrawerAlwaysVisible": "Drawer always visible",
"globalOptionsNavigationModeQuickBar": "Quick bar",
"accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?", "accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?",
"@accountOptionsRemoveConfirm": { "@accountOptionsRemoveConfirm": {
"placeholders": { "placeholders": {

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

@ -353,6 +353,12 @@ abstract class AppLocalizations {
/// **'System tray'** /// **'System tray'**
String get optionsCategorySystemTray; String get optionsCategorySystemTray;
/// No description provided for @optionsCategoryNavigation.
///
/// In en, this message translates to:
/// **'Navigation'**
String get optionsCategoryNavigation;
/// No description provided for @optionsSortOrderAscending. /// No description provided for @optionsSortOrderAscending.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -485,11 +491,11 @@ abstract class AppLocalizations {
/// **'Remember last used account'** /// **'Remember last used account'**
String get globalOptionsAccountsRememberLastUsedAccount; String get globalOptionsAccountsRememberLastUsedAccount;
/// No description provided for @globaloptionsaccountsInitialAccount. /// No description provided for @globalOptionsAccountsInitialAccount.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Initial account'** /// **'Initial account'**
String get globaloptionsaccountsInitialAccount; String get globalOptionsAccountsInitialAccount;
/// No description provided for @globalOptionsAccountsAdd. /// No description provided for @globalOptionsAccountsAdd.
/// ///
@ -497,6 +503,30 @@ abstract class AppLocalizations {
/// **'Add account'** /// **'Add account'**
String get globalOptionsAccountsAdd; String get globalOptionsAccountsAdd;
/// No description provided for @globalOptionsNavigationMode.
///
/// In en, this message translates to:
/// **'Navigation mode'**
String get globalOptionsNavigationMode;
/// No description provided for @globalOptionsNavigationModeDrawer.
///
/// In en, this message translates to:
/// **'Drawer'**
String get globalOptionsNavigationModeDrawer;
/// No description provided for @globalOptionsNavigationModeDrawerAlwaysVisible.
///
/// In en, this message translates to:
/// **'Drawer always visible'**
String get globalOptionsNavigationModeDrawerAlwaysVisible;
/// No description provided for @globalOptionsNavigationModeQuickBar.
///
/// In en, this message translates to:
/// **'Quick bar'**
String get globalOptionsNavigationModeQuickBar;
/// No description provided for @accountOptionsRemoveConfirm. /// No description provided for @accountOptionsRemoveConfirm.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

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

@ -147,6 +147,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get optionsCategorySystemTray => 'System tray'; String get optionsCategorySystemTray => 'System tray';
@override
String get optionsCategoryNavigation => 'Navigation';
@override @override
String get optionsSortOrderAscending => 'Ascending'; String get optionsSortOrderAscending => 'Ascending';
@ -216,11 +219,23 @@ class AppLocalizationsEn extends AppLocalizations {
String get globalOptionsAccountsRememberLastUsedAccount => 'Remember last used account'; String get globalOptionsAccountsRememberLastUsedAccount => 'Remember last used account';
@override @override
String get globaloptionsaccountsInitialAccount => 'Initial account'; String get globalOptionsAccountsInitialAccount => 'Initial account';
@override @override
String get globalOptionsAccountsAdd => 'Add account'; String get globalOptionsAccountsAdd => 'Add account';
@override
String get globalOptionsNavigationMode => 'Navigation mode';
@override
String get globalOptionsNavigationModeDrawer => 'Drawer';
@override
String get globalOptionsNavigationModeDrawerAlwaysVisible => 'Drawer always visible';
@override
String get globalOptionsNavigationModeQuickBar => 'Quick bar';
@override @override
String accountOptionsRemoveConfirm(String id) { String accountOptionsRemoveConfirm(String id) {
return 'Are you sure you want to remove the account $id?'; return 'Are you sure you want to remove the account $id?';

19
packages/neon/lib/src/app.dart

@ -53,19 +53,26 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
(final _) => false, (final _) => false,
); );
} else { } else {
await _navigatorKey.currentState!.pushAndRemoveUntil( const settings = RouteSettings(
MaterialPageRoute(
settings: const RouteSettings(
name: 'home', name: 'home',
), );
builder: (final context) => HomePage( Widget builder(final context) => HomePage(
account: activeAccount, account: activeAccount,
onThemeChanged: (final theme) { onThemeChanged: (final theme) {
setState(() { setState(() {
_userTheme = theme; _userTheme = theme;
}); });
}, },
), );
await _navigatorKey.currentState!.pushAndRemoveUntil(
widget.globalOptions.navigationMode.value == NavigationMode.drawer
? MaterialPageRoute(
settings: settings,
builder: builder,
)
: NoAnimationPageRoute(
settings: settings,
builder: builder,
), ),
(final _) => false, (final _) => false,
); );

7
packages/neon/lib/src/apps/files/widgets/file_preview.dart

@ -96,11 +96,8 @@ class FilePreview extends StatelessWidget {
), ),
], ],
if (previewLoading) ...[ if (previewLoading) ...[
Center( const Center(
child: CircularProgressIndicator( child: CustomLinearProgressIndicator(),
strokeWidth: 2,
color: color,
),
), ),
], ],
], ],

14
packages/neon/lib/src/apps/news/pages/article.dart

@ -140,6 +140,7 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
), ),
body: widget.useWebView body: widget.useWebView
? Stack( ? Stack(
alignment: Alignment.center,
children: [ children: [
WebView( WebView(
javascriptMode: JavascriptMode.unrestricted, javascriptMode: JavascriptMode.unrestricted,
@ -160,11 +161,16 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
}, },
), ),
if (_webviewLoading) ...[ if (_webviewLoading) ...[
ColoredBox( Expanded(
child: ColoredBox(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: const Center( child: Center(
child: CircularProgressIndicator( child: LayoutBuilder(
strokeWidth: 3, builder: (final context, final constraints) => SizedBox(
width: constraints.maxWidth / 2,
child: const CustomLinearProgressIndicator(),
),
),
), ),
), ),
), ),

4
packages/neon/lib/src/blocs/apps.dart

@ -36,8 +36,10 @@ class AppsBloc extends $AppsBloc {
_$refreshEvent.listen((final _) => _loadApps); _$refreshEvent.listen((final _) => _loadApps);
_$setActiveAppEvent.listen((final appId) async { _$setActiveAppEvent.listen((final appId) async {
final data = (await _appImplementationsSubject.firstWhere((final result) => result.data != null)).data!; final data = (await _appImplementationsSubject.firstWhere((final result) => result.data != null)).data!;
if (data.where((final app) => app.id == appId).isNotEmpty && _activeAppSubject.valueOrNull != appId) { if (data.where((final app) => app.id == appId).isNotEmpty) {
if (_activeAppSubject.valueOrNull != appId) {
_activeAppSubject.add(appId); _activeAppSubject.add(appId);
}
} else { } else {
debugPrint('App $appId not found'); debugPrint('App $appId not found');
} }

1
packages/neon/lib/src/neon.dart

@ -96,5 +96,6 @@ part 'widgets/exception.dart';
part 'widgets/image_wrapper.dart'; part 'widgets/image_wrapper.dart';
part 'widgets/neon_logo.dart'; part 'widgets/neon_logo.dart';
part 'widgets/nextcloud_logo.dart'; part 'widgets/nextcloud_logo.dart';
part 'widgets/no_animation_page_route.dart';
part 'widgets/result_stream_builder.dart'; part 'widgets/result_stream_builder.dart';
part 'widgets/standard_rx_result_builder.dart'; part 'widgets/standard_rx_result_builder.dart';

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

@ -1,5 +1,7 @@
part of '../../neon.dart'; part of '../../neon.dart';
const kQuickBarWidth = kAvatarSize + 20;
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({ const HomePage({
required this.account, required this.account,
@ -291,6 +293,14 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
); );
} }
Future _openSettings() async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => const SettingsPage(),
),
);
}
@override @override
void dispose() { void dispose() {
_capabilitiesBloc.dispose(); _capabilitiesBloc.dispose();
@ -345,7 +355,9 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
final accountsSnapshot, final accountsSnapshot,
final _, final _,
) => ) =>
WillPopScope( OptionBuilder<NavigationMode>(
option: _globalOptions.navigationMode,
builder: (final context, final navigationMode) => WillPopScope(
onWillPop: () async { onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) { if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context); Navigator.pop(context);
@ -360,106 +372,64 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
if (accountsSnapshot.hasData) { if (accountsSnapshot.hasData) {
final accounts = accountsSnapshot.data!; final accounts = accountsSnapshot.data!;
final account = accounts.singleWhere((final account) => account.id == widget.account.id); final account = accounts.singleWhere((final account) => account.id == widget.account.id);
return Scaffold(
key: _scaffoldKey, final isQuickBar = navigationMode == NavigationMode.quickBar;
resizeToAvoidBottomInset: false, final drawer = Drawer(
appBar: AppBar( width: isQuickBar ? kQuickBarWidth : null,
title: Column( child: Container(
crossAxisAlignment: CrossAxisAlignment.start, padding: isQuickBar ? const EdgeInsets.all(5) : null,
color: isQuickBar ? Theme.of(context).appBarTheme.backgroundColor : null,
child: Column(
children: [ children: [
Row( Expanded(
child: Scrollbar(
child: ListView(
// Needed for the drawer header to also render in the statusbar
padding: EdgeInsets.zero,
children: [ children: [
if (appsData != null && activeAppIDSnapshot.hasData) ...[ Builder(
Flexible( builder: (final context) {
child: Text( if (accountsSnapshot.hasData) {
appsData if (isQuickBar) {
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!) return Column(
.name(context), children: [
), if (accounts.length != 1) ...[
for (final account in accounts) ...[
Container(
margin: const EdgeInsets.symmetric(
vertical: 5,
),
child: Tooltip(
message: account.client.humanReadableID,
child: IconButton(
onPressed: () {
accountsBloc.setActiveAccount(account);
},
icon: IntrinsicHeight(
child: AccountAvatar(
account: account,
), ),
],
if (appsError != null) ...[
const SizedBox(
width: 8,
), ),
Icon(
Icons.error_outline,
size: 30,
color: Theme.of(context).colorScheme.onPrimary,
), ),
],
if (appsLoading) ...[
const SizedBox(
width: 8,
),
SizedBox(
height: 30,
width: 30,
child: CircularProgressIndicator(
color: Theme.of(context).colorScheme.onPrimary,
strokeWidth: 2,
), ),
), ),
], ],
], Container(
margin: const EdgeInsets.only(
top: 10,
), ),
if (accounts.length > 1) ...[ child: Divider(
Text( height: 5,
account.client.humanReadableID, color: Theme.of(context).appBarTheme.foregroundColor,
style: Theme.of(context).textTheme.bodySmall!,
), ),
],
],
),
actions: [
if (appsData != null && activeAppIDSnapshot.hasData) ...[
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => NextcloudAppSpecificSettingsPage(
appImplementation:
appsData.singleWhere((final a) => a.id == activeAppIDSnapshot.data!),
),
),
);
},
),
IconButton(
icon: AccountAvatar(
account: account,
requestManager: _requestManager,
),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => AccountSpecificSettingsPage(
bloc: accountsBloc,
account: account,
),
),
);
},
), ),
], ],
], ],
), );
drawer: Drawer( }
child: Column(
children: [
Expanded(
child: Scrollbar(
child: ListView(
// Needed for the drawer header to also render in the statusbar
padding: EdgeInsets.zero,
children: [
Builder(
builder: (final context) {
if (accountsSnapshot.hasData) {
return DrawerHeader( return DrawerHeader(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).appBarTheme.backgroundColor,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -469,7 +439,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
Text( Text(
capabilitiesData.capabilities!.theming!.name!, capabilitiesData.capabilities!.theming!.name!,
style: DefaultTextStyle.of(context).style.copyWith( style: DefaultTextStyle.of(context).style.copyWith(
color: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).appBarTheme.foregroundColor,
), ),
), ),
if (capabilitiesData.capabilities!.theming!.logo != null) ...[ if (capabilitiesData.capabilities!.theming!.logo != null) ...[
@ -492,27 +462,28 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
visible: capabilitiesLoading, visible: capabilitiesLoading,
), ),
], ],
if (accountsSnapshot.data!.length != 1) ...[ if (accounts.length != 1) ...[
DropdownButtonHideUnderline( DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary, dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).colorScheme.onPrimary, iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor,
value: widget.account.id, value: widget.account.id,
items: accountsSnapshot.data! items: accounts
.map<DropdownMenuItem<String>>( .map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>( (final account) => DropdownMenuItem<String>(
value: account.id, value: account.id,
child: AccountTile( child: AccountTile(
account: account, account: account,
dense: true, dense: true,
textColor: Theme.of(context).colorScheme.onPrimary, textColor:
Theme.of(context).appBarTheme.foregroundColor,
), ),
), ),
) )
.toList(), .toList(),
onChanged: (final id) { onChanged: (final id) {
for (final account in accountsSnapshot.data!) { for (final account in accounts) {
if (account.id == id) { if (account.id == id) {
accountsBloc.setActiveAccount(account); accountsBloc.setActiveAccount(account);
break; break;
@ -531,6 +502,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
), ),
ExceptionWidget( ExceptionWidget(
appsError, appsError,
onlyIcon: isQuickBar,
onRetry: () { onRetry: () {
_appsBloc.refresh(); _appsBloc.refresh();
}, },
@ -540,19 +512,52 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
), ),
if (appsData != null) ...[ if (appsData != null) ...[
for (final appImplementation in appsData) ...[ for (final appImplementation in appsData) ...[
ListTile( StreamBuilder<int>(
key: Key('app-${appImplementation.id}'),
title: StreamBuilder<int>(
stream: appImplementation.getUnreadCounter(_appsBloc) ?? stream: appImplementation.getUnreadCounter(_appsBloc) ??
BehaviorSubject<int>.seeded(0), BehaviorSubject<int>.seeded(0),
builder: (final context, final unreadCounterSnapshot) => Row( builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
if (isQuickBar) {
return Tooltip(
message: appImplementation.name(context),
child: IconButton(
onPressed: () {
_appsBloc.setActiveApp(appImplementation.id);
},
icon: Stack(
alignment: Alignment.bottomRight,
children: [
Container(
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(
context,
height: kAvatarSize,
width: kAvatarSize,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
if (unreadCount > 0) ...[
Text(
unreadCount.toString(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
],
),
),
);
}
return ListTile(
key: Key('app-${appImplementation.id}'),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(appImplementation.name(context)), Text(appImplementation.name(context)),
if (unreadCounterSnapshot.hasData && if (unreadCount > 0) ...[
unreadCounterSnapshot.data! > 0) ...[
Text( Text(
unreadCounterSnapshot.data!.toString(), unreadCount.toString(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -562,12 +567,16 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
], ],
], ],
), ),
),
leading: appImplementation.buildIcon(context), leading: appImplementation.buildIcon(context),
minLeadingWidth: 0, minLeadingWidth: 0,
onTap: () { onTap: () {
_appsBloc.setActiveApp(appImplementation.id); _appsBloc.setActiveApp(appImplementation.id);
if (navigationMode == NavigationMode.drawer) {
// Don't pop when the drawer is always shown
Navigator.of(context).pop(); Navigator.of(context).pop();
}
},
);
}, },
), ),
], ],
@ -576,23 +585,146 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
), ),
), ),
), ),
if (isQuickBar) ...[
IconButton(
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.foregroundColor,
),
onPressed: _openSettings,
),
] else ...[
ListTile( ListTile(
key: const Key('settings'), key: const Key('settings'),
title: Text(AppLocalizations.of(context).settings), title: Text(AppLocalizations.of(context).settings),
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
minLeadingWidth: 0, minLeadingWidth: 0,
onTap: () async { onTap: () async {
if (navigationMode == NavigationMode.drawer) {
Navigator.of(context).pop();
}
await _openSettings();
},
),
],
],
),
),
);
return Scaffold(
body: Row(
children: [
if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[
drawer,
],
Expanded(
child: Scaffold(
key: _scaffoldKey,
resizeToAvoidBottomInset: false,
drawer: navigationMode == NavigationMode.drawer ? drawer : null,
appBar: AppBar(
scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null,
automaticallyImplyLeading: navigationMode == NavigationMode.drawer,
leadingWidth: isQuickBar ? kQuickBarWidth : null,
leading: isQuickBar
? Container(
padding: const EdgeInsets.all(5),
child: capabilitiesData?.capabilities?.theming?.logo != null
? CachedURLImage(
url: capabilitiesData!.capabilities!.theming!.logo!,
requestManager: _requestManager,
client: widget.account.client,
)
: null,
)
: null,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (appsData != null && activeAppIDSnapshot.hasData) ...[
Flexible(
child: Text(
appsData
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!)
.name(context),
),
),
],
if (appsError != null) ...[
const SizedBox(
width: 8,
),
Icon(
Icons.error_outline,
size: 30,
color: Theme.of(context).colorScheme.onPrimary,
),
],
if (appsLoading) ...[
const SizedBox(
width: 8,
),
Expanded(
child: CustomLinearProgressIndicator(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
],
),
if (accounts.length > 1) ...[
Text(
account.client.humanReadableID,
style: Theme.of(context).textTheme.bodySmall!,
),
],
],
),
actions: [
if (appsData != null && activeAppIDSnapshot.hasData) ...[
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (final context) => const SettingsPage(), builder: (final context) => NextcloudAppSpecificSettingsPage(
appImplementation: appsData
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!),
),
), ),
); );
}, },
), ),
], IconButton(
icon: IntrinsicWidth(
child: AccountAvatar(
account: account,
),
),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => AccountSpecificSettingsPage(
bloc: accountsBloc,
account: account,
),
),
);
},
), ),
],
],
), ),
body: Column( body: Row(
children: [
if (navigationMode == NavigationMode.quickBar) ...[
drawer,
],
Expanded(
child: Column(
children: [ children: [
ServerStatus( ServerStatus(
account: widget.account, account: widget.account,
@ -625,6 +757,13 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
], ],
], ],
), ),
),
],
),
),
),
],
),
); );
} }
return Container(); return Container();
@ -634,6 +773,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
), ),
), ),
), ),
),
); );
} }
} }

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

@ -107,6 +107,14 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
], ],
), ),
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryNavigation),
tiles: [
DropdownButtonSettingsTile(
option: globalOptions.navigationMode,
),
],
),
if (platform.canUsePushNotifications) ...[ if (platform.canUsePushNotifications) ...[
SettingsCategory( SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications), title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),

3
packages/neon/lib/src/utils/app_implementation.dart

@ -42,13 +42,14 @@ abstract class AppImplementation<T extends RxBlocBase, R extends NextcloudAppSpe
final BuildContext context, { final BuildContext context, {
final double height = 32, final double height = 32,
final double width = 32, final double width = 32,
final Color? color,
}) => }) =>
SizedBox( SizedBox(
height: height, height: height,
width: width, width: width,
child: SvgPicture.asset( child: SvgPicture.asset(
'assets/apps/$id.svg', 'assets/apps/$id.svg',
color: Theme.of(context).colorScheme.primary, color: color ?? Theme.of(context).colorScheme.primary,
), ),
); );

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

@ -68,6 +68,7 @@ class GlobalOptions {
systemTrayHideToTrayWhenMinimized, systemTrayHideToTrayWhenMinimized,
rememberLastUsedAccount, rememberLastUsedAccount,
initialAccount, initialAccount,
navigationMode,
]; ];
Future reset() async { Future reset() async {
@ -192,9 +193,32 @@ class GlobalOptions {
late final initialAccount = SelectOption<String?>( late final initialAccount = SelectOption<String?>(
storage: _storage, storage: _storage,
key: 'initial-account', key: 'initial-account',
label: (final context) => AppLocalizations.of(context).globaloptionsaccountsInitialAccount, label: (final context) => AppLocalizations.of(context).globalOptionsAccountsInitialAccount,
defaultValue: BehaviorSubject.seeded(null), defaultValue: BehaviorSubject.seeded(null),
values: _accountsIDsSubject, values: _accountsIDsSubject,
enabled: _initialAccountEnabledSubject, enabled: _initialAccountEnabledSubject,
); );
late final navigationMode = SelectOption<NavigationMode>(
storage: _storage,
key: 'navigation-mode',
label: (final context) => AppLocalizations.of(context).globalOptionsNavigationMode,
defaultValue: BehaviorSubject.seeded(
Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible,
),
values: BehaviorSubject.seeded({
NavigationMode.drawer: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawer,
if (!Platform.isAndroid && !Platform.isIOS) ...{
NavigationMode.drawerAlwaysVisible: (final context) =>
AppLocalizations.of(context).globalOptionsNavigationModeDrawerAlwaysVisible,
},
NavigationMode.quickBar: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeQuickBar,
}),
);
}
enum NavigationMode {
drawer,
drawerAlwaysVisible,
quickBar,
} }

9
packages/neon/lib/src/widgets/account_avatar.dart

@ -5,19 +5,18 @@ const kAvatarSize = 40.0;
class AccountAvatar extends StatelessWidget { class AccountAvatar extends StatelessWidget {
const AccountAvatar({ const AccountAvatar({
required this.account, required this.account,
required this.requestManager,
super.key, super.key,
}); });
final Account account; final Account account;
final RequestManager requestManager;
@override @override
Widget build(final BuildContext context) => Stack( Widget build(final BuildContext context) => Stack(
alignment: Alignment.center,
children: [ children: [
ResultStreamBuilder<Uint8List>( ResultStreamBuilder<Uint8List>(
// TODO: See TODO in cached_url_image.dart // TODO: See TODO in cached_url_image.dart
stream: requestManager.wrapBytes( stream: Provider.of<RequestManager>(context, listen: false).wrapBytes(
account.client.id, account.client.id,
'accounts-avatar-${account.id}', 'accounts-avatar-${account.id}',
() async => account.client.core.getAvatar( () async => account.client.core.getAvatar(
@ -48,9 +47,7 @@ class AccountAvatar extends StatelessWidget {
), ),
], ],
if (avatarLoading) ...[ if (avatarLoading) ...[
const CircularProgressIndicator( const CustomLinearProgressIndicator(),
strokeWidth: 2,
),
], ],
], ],
), ),

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

@ -33,9 +33,10 @@ class AccountTile extends StatelessWidget {
vertical: -4, vertical: -4,
) )
: null, : null,
leading: AccountAvatar( leading: IntrinsicWidth(
child: AccountAvatar(
account: account, account: account,
requestManager: Provider.of<RequestManager>(context), ),
), ),
title: StandardRxResultBuilder<UserDetailsBloc, ProvisioningApiUserDetails>( title: StandardRxResultBuilder<UserDetailsBloc, ProvisioningApiUserDetails>(
bloc: userDetailsBloc, bloc: userDetailsBloc,
@ -64,12 +65,9 @@ class AccountTile extends StatelessWidget {
const SizedBox( const SizedBox(
width: 5, width: 5,
), ),
SizedBox( Expanded(
height: 10, child: CustomLinearProgressIndicator(
width: 10, color: textColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: color,
), ),
), ),
], ],
@ -77,10 +75,12 @@ class AccountTile extends StatelessWidget {
const SizedBox( const SizedBox(
width: 5, width: 5,
), ),
Icon( ExceptionWidget(
Icons.error_outline, userDetailsError,
size: 20, onlyIcon: true,
color: color, onRetry: () {
userDetailsBloc.refresh();
},
), ),
], ],
], ],

8
packages/neon/lib/src/widgets/cached_url_image.dart

@ -81,11 +81,9 @@ class CachedURLImage extends StatelessWidget {
), ),
], ],
if (loading) ...[ if (loading) ...[
Container( SizedBox(
margin: const EdgeInsets.all(3), width: width,
child: const CircularProgressIndicator( child: const CustomLinearProgressIndicator(),
strokeWidth: 2,
),
), ),
], ],
], ],

13
packages/neon/lib/src/widgets/custom_linear_progress_indicator.dart

@ -2,20 +2,29 @@ part of '../neon.dart';
class CustomLinearProgressIndicator extends StatelessWidget { class CustomLinearProgressIndicator extends StatelessWidget {
const CustomLinearProgressIndicator({ const CustomLinearProgressIndicator({
required this.visible, this.visible = true,
this.margin = const EdgeInsets.symmetric(horizontal: 10), this.margin = const EdgeInsets.symmetric(horizontal: 10),
this.color,
this.backgroundColor = Colors.transparent,
super.key, super.key,
}); });
final bool visible; final bool visible;
final EdgeInsets? margin; final EdgeInsets? margin;
final Color? color;
final Color? backgroundColor;
@override @override
Widget build(final BuildContext context) => Container( Widget build(final BuildContext context) => Container(
margin: margin, margin: margin,
child: SizedBox( child: SizedBox(
height: 3, height: 3,
child: visible ? const LinearProgressIndicator() : null, child: visible
? LinearProgressIndicator(
color: color,
backgroundColor: backgroundColor,
)
: null,
), ),
); );
} }

29
packages/neon/lib/src/widgets/exception.dart

@ -4,11 +4,13 @@ class ExceptionWidget extends StatelessWidget {
const ExceptionWidget( const ExceptionWidget(
this.exception, { this.exception, {
required this.onRetry, required this.onRetry,
this.onlyIcon = false,
super.key, super.key,
}); });
final dynamic exception; final dynamic exception;
final Function() onRetry; final Function() onRetry;
final bool onlyIcon;
static void showSnackbar(final BuildContext context, final dynamic exception) { static void showSnackbar(final BuildContext context, final dynamic exception) {
final details = _getExceptionDetails(context, exception); final details = _getExceptionDetails(context, exception);
@ -35,16 +37,31 @@ class ExceptionWidget extends StatelessWidget {
builder: (final context) { builder: (final context) {
final details = _getExceptionDetails(context, exception); final details = _getExceptionDetails(context, exception);
const errorIcon = Icon(
Icons.error_outline,
size: 30,
color: Colors.red,
);
if (onlyIcon) {
return IconButton(
onPressed: () async {
if (details.isUnauthorized) {
await _openLoginPage(context);
} else {
onRetry();
}
},
icon: errorIcon,
);
}
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon( errorIcon,
Icons.error_outline,
size: 30,
color: Colors.red,
),
const SizedBox( const SizedBox(
width: 10, width: 10,
), ),
@ -65,7 +82,7 @@ class ExceptionWidget extends StatelessWidget {
), ),
] else ...[ ] else ...[
ElevatedButton( ElevatedButton(
onPressed: () async => onRetry(), onPressed: onRetry,
child: Text(AppLocalizations.of(context).retry), child: Text(AppLocalizations.of(context).retry),
), ),
], ],

11
packages/neon/lib/src/widgets/no_animation_page_route.dart

@ -0,0 +1,11 @@
part of '../neon.dart';
class NoAnimationPageRoute extends MaterialPageRoute {
NoAnimationPageRoute({
required super.builder,
super.settings,
});
@override
Duration get transitionDuration => Duration.zero;
}
Loading…
Cancel
Save