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. 23
      packages/neon/lib/src/app.dart
  5. 7
      packages/neon/lib/src/apps/files/widgets/file_preview.dart
  6. 16
      packages/neon/lib/src/apps/news/pages/article.dart
  7. 6
      packages/neon/lib/src/blocs/apps.dart
  8. 1
      packages/neon/lib/src/neon.dart
  9. 658
      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. 26
      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",
"optionsCategoryStartup": "Startup",
"optionsCategorySystemTray": "System tray",
"optionsCategoryNavigation": "Navigation",
"optionsSortOrderAscending": "Ascending",
"optionsSortOrderDescending": "Descending",
"globalOptionsThemeMode": "Theme mode",
@ -94,8 +95,12 @@
"globalOptionsSystemTrayEnabled": "Enable system tray",
"globalOptionsSystemTrayHideToTrayWhenMinimized": "Hide to system tray when minimized",
"globalOptionsAccountsRememberLastUsedAccount": "Remember last used account",
"globaloptionsaccountsInitialAccount": "Initial account",
"globalOptionsAccountsInitialAccount": "Initial 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": {
"placeholders": {

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

@ -353,6 +353,12 @@ abstract class AppLocalizations {
/// **'System tray'**
String get optionsCategorySystemTray;
/// No description provided for @optionsCategoryNavigation.
///
/// In en, this message translates to:
/// **'Navigation'**
String get optionsCategoryNavigation;
/// No description provided for @optionsSortOrderAscending.
///
/// In en, this message translates to:
@ -485,11 +491,11 @@ abstract class AppLocalizations {
/// **'Remember last used account'**
String get globalOptionsAccountsRememberLastUsedAccount;
/// No description provided for @globaloptionsaccountsInitialAccount.
/// No description provided for @globalOptionsAccountsInitialAccount.
///
/// In en, this message translates to:
/// **'Initial account'**
String get globaloptionsaccountsInitialAccount;
String get globalOptionsAccountsInitialAccount;
/// No description provided for @globalOptionsAccountsAdd.
///
@ -497,6 +503,30 @@ abstract class AppLocalizations {
/// **'Add account'**
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.
///
/// In en, this message translates to:

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

@ -147,6 +147,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get optionsCategorySystemTray => 'System tray';
@override
String get optionsCategoryNavigation => 'Navigation';
@override
String get optionsSortOrderAscending => 'Ascending';
@ -216,11 +219,23 @@ class AppLocalizationsEn extends AppLocalizations {
String get globalOptionsAccountsRememberLastUsedAccount => 'Remember last used account';
@override
String get globaloptionsaccountsInitialAccount => 'Initial account';
String get globalOptionsAccountsInitialAccount => 'Initial account';
@override
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
String accountOptionsRemoveConfirm(String id) {
return 'Are you sure you want to remove the account $id?';

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

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

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

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

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

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

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

@ -36,8 +36,10 @@ class AppsBloc extends $AppsBloc {
_$refreshEvent.listen((final _) => _loadApps);
_$setActiveAppEvent.listen((final appId) async {
final data = (await _appImplementationsSubject.firstWhere((final result) => result.data != null)).data!;
if (data.where((final app) => app.id == appId).isNotEmpty && _activeAppSubject.valueOrNull != appId) {
_activeAppSubject.add(appId);
if (data.where((final app) => app.id == appId).isNotEmpty) {
if (_activeAppSubject.valueOrNull != appId) {
_activeAppSubject.add(appId);
}
} else {
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/neon_logo.dart';
part 'widgets/nextcloud_logo.dart';
part 'widgets/no_animation_page_route.dart';
part 'widgets/result_stream_builder.dart';
part 'widgets/standard_rx_result_builder.dart';

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

@ -1,5 +1,7 @@
part of '../../neon.dart';
const kQuickBarWidth = kAvatarSize + 20;
class HomePage extends StatefulWidget {
const HomePage({
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
void dispose() {
_capabilitiesBloc.dispose();
@ -345,290 +355,420 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
final accountsSnapshot,
final _,
) =>
WillPopScope(
onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context);
return true;
}
OptionBuilder<NavigationMode>(
option: _globalOptions.navigationMode,
builder: (final context, final navigationMode) => WillPopScope(
onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context);
return true;
}
_scaffoldKey.currentState!.openDrawer();
return false;
},
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: 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,
),
SizedBox(
height: 30,
width: 30,
child: CircularProgressIndicator(
color: Theme.of(context).colorScheme.onPrimary,
strokeWidth: 2,
),
),
],
],
),
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(
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(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (capabilitiesData != null) ...[
Text(
capabilitiesData.capabilities!.theming!.name!,
style: DefaultTextStyle.of(context).style.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
_scaffoldKey.currentState!.openDrawer();
return false;
},
child: Builder(
builder: (final context) {
if (accountsSnapshot.hasData) {
final accounts = accountsSnapshot.data!;
final account = accounts.singleWhere((final account) => account.id == widget.account.id);
final isQuickBar = navigationMode == NavigationMode.quickBar;
final drawer = Drawer(
width: isQuickBar ? kQuickBarWidth : null,
child: Container(
padding: isQuickBar ? const EdgeInsets.all(5) : null,
color: isQuickBar ? Theme.of(context).appBarTheme.backgroundColor : null,
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) {
if (isQuickBar) {
return Column(
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 (capabilitiesData.capabilities!.theming!.logo != null) ...[
Flexible(
child: CachedURLImage(
url: capabilitiesData.capabilities!.theming!.logo!,
requestManager: _requestManager,
client: widget.account.client,
),
],
Container(
margin: const EdgeInsets.only(
top: 10,
),
child: Divider(
height: 5,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
] else ...[
ExceptionWidget(
capabilitiesError,
onRetry: () {
_capabilitiesBloc.refresh();
},
),
CustomLinearProgressIndicator(
visible: capabilitiesLoading,
),
],
if (accountsSnapshot.data!.length != 1) ...[
DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).colorScheme.onPrimary,
value: widget.account.id,
items: accountsSnapshot.data!
.map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>(
value: account.id,
child: AccountTile(
account: account,
dense: true,
textColor: Theme.of(context).colorScheme.onPrimary,
);
}
return DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).appBarTheme.backgroundColor,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (capabilitiesData != null) ...[
Text(
capabilitiesData.capabilities!.theming!.name!,
style: DefaultTextStyle.of(context).style.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
if (capabilitiesData.capabilities!.theming!.logo != null) ...[
Flexible(
child: CachedURLImage(
url: capabilitiesData.capabilities!.theming!.logo!,
requestManager: _requestManager,
client: widget.account.client,
),
),
],
] else ...[
ExceptionWidget(
capabilitiesError,
onRetry: () {
_capabilitiesBloc.refresh();
},
),
CustomLinearProgressIndicator(
visible: capabilitiesLoading,
),
],
if (accounts.length != 1) ...[
DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor,
value: widget.account.id,
items: accounts
.map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>(
value: account.id,
child: AccountTile(
account: account,
dense: true,
textColor:
Theme.of(context).appBarTheme.foregroundColor,
),
),
),
)
.toList(),
onChanged: (final id) {
for (final account in accountsSnapshot.data!) {
if (account.id == id) {
accountsBloc.setActiveAccount(account);
break;
)
.toList(),
onChanged: (final id) {
for (final account in accounts) {
if (account.id == id) {
accountsBloc.setActiveAccount(account);
break;
}
}
}
},
},
),
),
),
],
],
],
),
);
}
return Container();
},
),
ExceptionWidget(
appsError,
onRetry: () {
_appsBloc.refresh();
},
),
CustomLinearProgressIndicator(
visible: appsLoading,
),
if (appsData != null) ...[
for (final appImplementation in appsData) ...[
ListTile(
key: Key('app-${appImplementation.id}'),
title: StreamBuilder<int>(
),
);
}
return Container();
},
),
ExceptionWidget(
appsError,
onlyIcon: isQuickBar,
onRetry: () {
_appsBloc.refresh();
},
),
CustomLinearProgressIndicator(
visible: appsLoading,
),
if (appsData != null) ...[
for (final appImplementation in appsData) ...[
StreamBuilder<int>(
stream: appImplementation.getUnreadCounter(_appsBloc) ??
BehaviorSubject<int>.seeded(0),
builder: (final context, final unreadCounterSnapshot) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(appImplementation.name(context)),
if (unreadCounterSnapshot.hasData &&
unreadCounterSnapshot.data! > 0) ...[
Text(
unreadCounterSnapshot.data!.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
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,
children: [
Text(appImplementation.name(context)),
if (unreadCount > 0) ...[
Text(
unreadCount.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
],
),
leading: appImplementation.buildIcon(context),
minLeadingWidth: 0,
onTap: () {
_appsBloc.setActiveApp(appImplementation.id);
if (navigationMode == NavigationMode.drawer) {
// Don't pop when the drawer is always shown
Navigator.of(context).pop();
}
},
);
},
),
leading: appImplementation.buildIcon(context),
minLeadingWidth: 0,
onTap: () {
_appsBloc.setActiveApp(appImplementation.id);
Navigator.of(context).pop();
},
),
],
],
],
],
),
),
),
),
ListTile(
key: const Key('settings'),
title: Text(AppLocalizations.of(context).settings),
leading: const Icon(Icons.settings),
minLeadingWidth: 0,
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => const SettingsPage(),
),
);
},
),
],
),
),
body: Column(
children: [
ServerStatus(
account: widget.account,
),
ExceptionWidget(
appsError,
onRetry: () {
_appsBloc.refresh();
},
),
if (appsData != null) ...[
if (appsData.isEmpty) ...[
Expanded(
child: Center(
child: Text(
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound,
textAlign: TextAlign.center,
if (isQuickBar) ...[
IconButton(
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.foregroundColor,
),
onPressed: _openSettings,
),
),
] else ...[
if (activeAppIDSnapshot.hasData) ...[
Expanded(
child: appsData
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!)
.buildPage(context, _appsBloc),
] else ...[
ListTile(
key: const Key('settings'),
title: Text(AppLocalizations.of(context).settings),
leading: const Icon(Icons.settings),
minLeadingWidth: 0,
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(
MaterialPageRoute(
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: Row(
children: [
if (navigationMode == NavigationMode.quickBar) ...[
drawer,
],
Expanded(
child: Column(
children: [
ServerStatus(
account: widget.account,
),
ExceptionWidget(
appsError,
onRetry: () {
_appsBloc.refresh();
},
),
if (appsData != null) ...[
if (appsData.isEmpty) ...[
Expanded(
child: Center(
child: Text(
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound,
textAlign: TextAlign.center,
),
),
),
] else ...[
if (activeAppIDSnapshot.hasData) ...[
Expanded(
child: appsData
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!)
.buildPage(context, _appsBloc),
),
],
],
],
],
),
),
],
),
),
),
],
],
),
);
}
return Container();
},
),
);
}
return Container();
},
),
),
),
),

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) ...[
SettingsCategory(
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 double height = 32,
final double width = 32,
final Color? color,
}) =>
SizedBox(
height: height,
width: width,
child: SvgPicture.asset(
'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,
rememberLastUsedAccount,
initialAccount,
navigationMode,
];
Future reset() async {
@ -192,9 +193,32 @@ class GlobalOptions {
late final initialAccount = SelectOption<String?>(
storage: _storage,
key: 'initial-account',
label: (final context) => AppLocalizations.of(context).globaloptionsaccountsInitialAccount,
label: (final context) => AppLocalizations.of(context).globalOptionsAccountsInitialAccount,
defaultValue: BehaviorSubject.seeded(null),
values: _accountsIDsSubject,
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 {
const AccountAvatar({
required this.account,
required this.requestManager,
super.key,
});
final Account account;
final RequestManager requestManager;
@override
Widget build(final BuildContext context) => Stack(
alignment: Alignment.center,
children: [
ResultStreamBuilder<Uint8List>(
// TODO: See TODO in cached_url_image.dart
stream: requestManager.wrapBytes(
stream: Provider.of<RequestManager>(context, listen: false).wrapBytes(
account.client.id,
'accounts-avatar-${account.id}',
() async => account.client.core.getAvatar(
@ -48,9 +47,7 @@ class AccountAvatar extends StatelessWidget {
),
],
if (avatarLoading) ...[
const CircularProgressIndicator(
strokeWidth: 2,
),
const CustomLinearProgressIndicator(),
],
],
),

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

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

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

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

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

@ -2,20 +2,29 @@ part of '../neon.dart';
class CustomLinearProgressIndicator extends StatelessWidget {
const CustomLinearProgressIndicator({
required this.visible,
this.visible = true,
this.margin = const EdgeInsets.symmetric(horizontal: 10),
this.color,
this.backgroundColor = Colors.transparent,
super.key,
});
final bool visible;
final EdgeInsets? margin;
final Color? color;
final Color? backgroundColor;
@override
Widget build(final BuildContext context) => Container(
margin: margin,
child: SizedBox(
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(
this.exception, {
required this.onRetry,
this.onlyIcon = false,
super.key,
});
final dynamic exception;
final Function() onRetry;
final bool onlyIcon;
static void showSnackbar(final BuildContext context, final dynamic exception) {
final details = _getExceptionDetails(context, exception);
@ -35,16 +37,31 @@ class ExceptionWidget extends StatelessWidget {
builder: (final context) {
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(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 30,
color: Colors.red,
),
errorIcon,
const SizedBox(
width: 10,
),
@ -65,7 +82,7 @@ class ExceptionWidget extends StatelessWidget {
),
] else ...[
ElevatedButton(
onPressed: () async => onRetry(),
onPressed: onRetry,
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