Browse Source

Merge pull request #338 from Leptopoda/feature/routing

Feature/routing
pull/345/head
Nikolas Rimikis 2 years ago committed by GitHub
parent
commit
d47461f445
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      packages/app/pubspec.lock
  2. 3
      packages/neon/neon/lib/neon.dart
  3. 2
      packages/neon/neon/lib/src/app.dart
  4. 2
      packages/neon/neon/lib/src/pages/account_settings.dart
  5. 699
      packages/neon/neon/lib/src/pages/home.dart
  6. 1
      packages/neon/neon/lib/src/pages/login.dart
  7. 29
      packages/neon/neon/lib/src/pages/settings.dart
  8. 139
      packages/neon/neon/lib/src/router.dart
  9. 122
      packages/neon/neon/lib/src/router.g.dart
  10. 8
      packages/neon/neon/lib/src/utils/global_popups.dart
  11. 38
      packages/neon/neon/lib/src/utils/stream_listenable.dart
  12. 14
      packages/neon/neon/lib/src/widgets/exception.dart
  13. 4
      packages/neon/neon/pubspec.yaml

16
packages/app/pubspec.lock

@ -317,6 +317,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
go_router:
dependency: transitive
description:
name: go_router
sha256: "00d1b67d6e9fa443331da229084dd3eb04407f5a2dff22940bd7bba6af5722c3"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -434,6 +442,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
logging:
dependency: transitive
description:
name: logging
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
markdown: markdown:
dependency: transitive dependency: transitive
description: description:

3
packages/neon/neon/lib/neon.dart

@ -23,6 +23,7 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart
import 'package:neon/l10n/localizations.dart'; import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/push_notification.dart'; import 'package:neon/src/models/push_notification.dart';
import 'package:neon/src/router.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@ -67,7 +68,6 @@ part 'src/platform/abstract.dart';
part 'src/platform/android.dart'; part 'src/platform/android.dart';
part 'src/platform/linux.dart'; part 'src/platform/linux.dart';
part 'src/platform/platform.dart'; part 'src/platform/platform.dart';
part 'src/router.dart';
part 'src/utils/account_options.dart'; part 'src/utils/account_options.dart';
part 'src/utils/app_implementation.dart'; part 'src/utils/app_implementation.dart';
part 'src/utils/bloc.dart'; part 'src/utils/bloc.dart';
@ -89,6 +89,7 @@ part 'src/utils/settings_export_helper.dart';
part 'src/utils/sort_box_builder.dart'; part 'src/utils/sort_box_builder.dart';
part 'src/utils/sort_box_order_option_values.dart'; part 'src/utils/sort_box_order_option_values.dart';
part 'src/utils/storage.dart'; part 'src/utils/storage.dart';
part 'src/utils/stream_listenable.dart';
part 'src/utils/theme.dart'; part 'src/utils/theme.dart';
part 'src/utils/validators.dart'; part 'src/utils/validators.dart';
part 'src/widgets/account_avatar.dart'; part 'src/widgets/account_avatar.dart';

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

@ -295,7 +295,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
keepOriginalAccentColor: nextcloudTheme == null || (themeKeepOriginalAccentColor ?? false), keepOriginalAccentColor: nextcloudTheme == null || (themeKeepOriginalAccentColor ?? false),
oledAsDark: themeOLEDAsDark, oledAsDark: themeOLEDAsDark,
), ),
routerDelegate: _routerDelegate, routerConfig: _routerDelegate,
); );
}, },
); );

2
packages/neon/neon/lib/src/pages/account_settings.dart

@ -29,8 +29,6 @@ class AccountSettingsPage extends StatelessWidget {
AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID), AppLocalizations.of(context).accountOptionsRemoveConfirm(account.client.humanReadableID),
)) { )) {
bloc.removeAccount(account); bloc.removeAccount(account);
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
} }
}, },
tooltip: AppLocalizations.of(context).accountOptionsRemove, tooltip: AppLocalizations.of(context).accountOptionsRemove,

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

@ -4,12 +4,9 @@ const kQuickBarWidth = kAvatarSize + 20;
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({ const HomePage({
required this.account,
super.key, super.key,
}); });
final Account account;
@override @override
State<HomePage> createState() => _HomePageState(); State<HomePage> createState() => _HomePageState();
} }
@ -19,6 +16,7 @@ class _HomePageState extends State<HomePage> {
final _scaffoldKey = GlobalKey<ScaffoldState>(); final _scaffoldKey = GlobalKey<ScaffoldState>();
final drawerScrollController = ScrollController(); final drawerScrollController = ScrollController();
late Account _account;
late GlobalOptions _globalOptions; late GlobalOptions _globalOptions;
late AccountsBloc _accountsBloc; late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc; late AppsBloc _appsBloc;
@ -27,11 +25,11 @@ class _HomePageState extends State<HomePage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_globalOptions = Provider.of<GlobalOptions>(context, listen: false); _globalOptions = Provider.of<GlobalOptions>(context, listen: false);
_accountsBloc = Provider.of<AccountsBloc>(context, listen: false); _accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
_appsBloc = _accountsBloc.getAppsBloc(widget.account); _account = _accountsBloc.activeAccount.value!;
_capabilitiesBloc = _accountsBloc.getCapabilitiesBloc(widget.account); _appsBloc = _accountsBloc.getAppsBloc(_account);
_capabilitiesBloc = _accountsBloc.getCapabilitiesBloc(_account);
_appsBloc.openNotifications.listen((final _) async { _appsBloc.openNotifications.listen((final _) async {
final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull; final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull;
@ -61,9 +59,9 @@ class _HomePageState extends State<HomePage> {
]) { ]) {
try { try {
final (supported, _) = switch (id) { final (supported, _) = switch (id) {
'core' => await widget.account.client.core.isSupported(result.data), 'core' => await _account.client.core.isSupported(result.data),
'news' => await widget.account.client.news.isSupported(), 'news' => await _account.client.news.isSupported(),
'notes' => await widget.account.client.notes.isSupported(result.data), 'notes' => await _account.client.notes.isSupported(result.data),
_ => (true, null), _ => (true, null),
}; };
if (supported || !mounted) { if (supported || !mounted) {
@ -92,7 +90,7 @@ class _HomePageState extends State<HomePage> {
Future _checkMaintenanceMode() async { Future _checkMaintenanceMode() async {
try { try {
final status = await widget.account.client.core.getStatus(); final status = await _account.client.core.getStatus();
if (status.maintenance) { if (status.maintenance) {
if (!mounted) { if (!mounted) {
return; return;
@ -129,14 +127,6 @@ class _HomePageState extends State<HomePage> {
); );
} }
Future _openSettings() async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => const SettingsPage(),
),
);
}
Future _openNotifications( Future _openNotifications(
final NotificationsAppInterface app, final NotificationsAppInterface app,
final List<Account> accounts, final List<Account> accounts,
@ -207,381 +197,378 @@ class _HomePageState extends State<HomePage> {
builder: (final context) { builder: (final context) {
if (accountsSnapshot.hasData) { if (accountsSnapshot.hasData) {
final accounts = accountsSnapshot.data!; final accounts = accountsSnapshot.data!;
final account = accounts.find(widget.account.id)!; final account = accounts.find(_account.id);
if (account != null) {
final isQuickBar = navigationMode == NavigationMode.quickBar; final isQuickBar = navigationMode == NavigationMode.quickBar;
final drawer = Drawer( final drawer = Builder(
width: isQuickBar ? kQuickBarWidth : null, builder: (final context) => Drawer(
child: Container( width: isQuickBar ? kQuickBarWidth : null,
padding: isQuickBar ? const EdgeInsets.all(5) : null, child: Container(
child: Column( padding: isQuickBar ? const EdgeInsets.all(5) : null,
children: [ child: Column(
Expanded( children: [
child: Scrollbar( Expanded(
controller: drawerScrollController, child: Scrollbar(
interactive: true, controller: drawerScrollController,
child: ListView( interactive: true,
controller: drawerScrollController, child: ListView(
// Needed for the drawer header to also render in the statusbar controller: drawerScrollController,
padding: EdgeInsets.zero, // Needed for the drawer header to also render in the statusbar
children: [ padding: EdgeInsets.zero,
Builder( children: [
builder: (final context) { Builder(
if (accountsSnapshot.hasData) { builder: (final context) {
if (isQuickBar) { if (accountsSnapshot.hasData) {
return Column( if (isQuickBar) {
children: [ return Column(
if (accounts.length != 1) ...[ children: [
for (final account in accounts) ...[ if (accounts.length != 1) ...[
Container( for (final account in accounts) ...[
margin: const EdgeInsets.symmetric( Container(
vertical: 5, margin: const EdgeInsets.symmetric(
), vertical: 5,
child: IconButton(
onPressed: () {
_accountsBloc.setActiveAccount(account);
},
tooltip: account.client.humanReadableID,
icon: IntrinsicHeight(
child: NeonAccountAvatar(
account: account,
), ),
child: IconButton(
onPressed: () {
_accountsBloc.setActiveAccount(account);
},
tooltip: account.client.humanReadableID,
icon: IntrinsicHeight(
child: NeonAccountAvatar(
account: account,
),
),
),
),
],
Container(
margin: const EdgeInsets.only(
top: 10,
),
child: Divider(
height: 5,
color: Theme.of(context).appBarTheme.foregroundColor,
), ),
), ),
), ],
], ],
Container( );
margin: const EdgeInsets.only( }
top: 10, return DrawerHeader(
), decoration: BoxDecoration(
child: Divider( color: Theme.of(context).colorScheme.primary,
height: 5, ),
color: Theme.of(context).appBarTheme.foregroundColor, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
), mainAxisAlignment: MainAxisAlignment.spaceBetween,
], children: [
], if (capabilities.data != null) ...[
); if (capabilities.data!.capabilities.theming?.name !=
} null) ...[
return DrawerHeader( Text(
decoration: BoxDecoration( capabilities.data!.capabilities.theming!.name!,
color: Theme.of(context).colorScheme.primary, style: DefaultTextStyle.of(context).style.copyWith(
), color:
child: Column( Theme.of(context).appBarTheme.foregroundColor,
crossAxisAlignment: CrossAxisAlignment.start, ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, ),
children: [ ],
if (capabilities.data != null) ...[ if (capabilities.data!.capabilities.theming?.logo !=
if (capabilities.data!.capabilities.theming?.name != null) ...[ null) ...[
Text( Flexible(
capabilities.data!.capabilities.theming!.name!, child: NeonCachedUrlImage(
style: DefaultTextStyle.of(context).style.copyWith( url: capabilities.data!.capabilities.theming!.logo!,
color: Theme.of(context).appBarTheme.foregroundColor,
), ),
), ),
], ],
if (capabilities.data!.capabilities.theming?.logo != null) ...[ ] else ...[
Flexible( NeonException(
child: NeonCachedUrlImage( capabilities.error,
url: capabilities.data!.capabilities.theming!.logo!, onRetry: _capabilitiesBloc.refresh,
), ),
), NeonLinearProgressIndicator(
visible: capabilities.loading,
),
],
if (accounts.length != 1) ...[
DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor:
Theme.of(context).colorScheme.onBackground,
value: _account.id,
items: accounts
.map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>(
value: account.id,
child: NeonAccountTile(
account: account,
dense: true,
textColor: Theme.of(context)
.appBarTheme
.foregroundColor,
),
),
)
.toList(),
onChanged: (final id) {
if (id != null) {
_accountsBloc.setActiveAccount(accounts.find(id));
}
},
),
),
],
], ],
] else ...[
NeonException(
capabilities.error,
onRetry: _capabilitiesBloc.refresh,
),
NeonLinearProgressIndicator(
visible: capabilities.loading,
),
],
if (accounts.length != 1) ...[
DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor:
Theme.of(context).colorScheme.onBackground,
value: widget.account.id,
items: accounts
.map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>(
value: account.id,
child: NeonAccountTile(
account: account,
dense: true,
textColor:
Theme.of(context).appBarTheme.foregroundColor,
),
),
)
.toList(),
onChanged: (final id) {
if (id != null) {
_accountsBloc.setActiveAccount(accounts.find(id));
}
},
),
),
],
],
),
);
}
return Container();
},
),
NeonException(
appImplementations.error,
onlyIcon: isQuickBar,
onRetry: _appsBloc.refresh,
),
NeonLinearProgressIndicator(
visible: appImplementations.loading,
),
if (appImplementations.data != null) ...[
for (final appImplementation in appImplementations.data!) ...[
StreamBuilder<int>(
stream: appImplementation.getUnreadCounter(_appsBloc) ??
BehaviorSubject<int>.seeded(0),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
if (isQuickBar) {
return IconButton(
onPressed: () async {
await _appsBloc.setActiveApp(appImplementation.id);
},
tooltip: appImplementation.name(context),
icon: NeonAppImplementationIcon(
appImplementation: appImplementation,
unreadCount: unreadCount,
color: Theme.of(context).colorScheme.primary,
), ),
); );
} }
return ListTile( return Container();
key: Key('app-${appImplementation.id}'), },
title: Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, NeonException(
children: [ appImplementations.error,
Text(appImplementation.name(context)), onlyIcon: isQuickBar,
if (unreadCount > 0) ...[ onRetry: _appsBloc.refresh,
Text( ),
unreadCount.toString(), NeonLinearProgressIndicator(
style: TextStyle( visible: appImplementations.loading,
color: Theme.of(context).colorScheme.primary, ),
fontWeight: FontWeight.bold, if (appImplementations.data != null) ...[
fontSize: 14, for (final appImplementation in appImplementations.data!) ...[
), StreamBuilder<int>(
stream: appImplementation.getUnreadCounter(_appsBloc) ??
BehaviorSubject<int>.seeded(0),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
if (isQuickBar) {
return IconButton(
onPressed: () async {
await _appsBloc.setActiveApp(appImplementation.id);
},
tooltip: appImplementation.name(context),
icon: NeonAppImplementationIcon(
appImplementation: appImplementation,
unreadCount: unreadCount,
color: Theme.of(context).colorScheme.primary,
), ),
], );
],
),
leading: appImplementation.buildIcon(context),
minLeadingWidth: 0,
onTap: () async {
await _appsBloc.setActiveApp(appImplementation.id);
if (navigationMode == NavigationMode.drawer) {
// Don't pop when the drawer is always shown
if (!mounted) {
return;
}
Navigator.of(context).pop();
} }
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: () async {
await _appsBloc.setActiveApp(appImplementation.id);
if (!mounted) {
return;
}
Scaffold.maybeOf(context)?.closeDrawer();
},
);
}, },
); ),
}, ],
), ],
], ],
], ),
], ),
), ),
), if (isQuickBar) ...[
IconButton(
onPressed: () => const SettingsRoute().go(context),
tooltip: AppLocalizations.of(context).settings,
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
] else ...[
ListTile(
key: const Key('settings'),
title: Text(AppLocalizations.of(context).settings),
leading: const Icon(Icons.settings),
minLeadingWidth: 0,
onTap: () async {
Scaffold.maybeOf(context)?.closeDrawer();
const SettingsRoute().go(context);
},
),
],
],
), ),
if (isQuickBar) ...[ ),
IconButton(
onPressed: _openSettings,
tooltip: AppLocalizations.of(context).settings,
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
] 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 Row( return Row(
children: [ children: [
if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[
drawer, drawer,
], ],
Expanded( Expanded(
child: Scaffold( child: Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
drawer: navigationMode == NavigationMode.drawer ? drawer : null, drawer: navigationMode == NavigationMode.drawer ? drawer : null,
appBar: AppBar( appBar: AppBar(
scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null, scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null,
automaticallyImplyLeading: navigationMode == NavigationMode.drawer, automaticallyImplyLeading: navigationMode == NavigationMode.drawer,
leadingWidth: isQuickBar ? kQuickBarWidth : null, leadingWidth: isQuickBar ? kQuickBarWidth : null,
leading: isQuickBar leading: isQuickBar
? Container( ? Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
child: capabilities.data?.capabilities.theming?.logo != null child: capabilities.data?.capabilities.theming?.logo != null
? NeonCachedUrlImage( ? NeonCachedUrlImage(
url: capabilities.data!.capabilities.theming!.logo!, url: capabilities.data!.capabilities.theming!.logo!,
svgColor: Theme.of(context).iconTheme.color, svgColor: Theme.of(context).iconTheme.color,
) )
: null, : null,
) )
: null, : null,
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[
Flexible( Flexible(
child: Text( child: Text(
appImplementations.data! appImplementations.data!
.find(activeAppIDSnapshot.data!)! .find(activeAppIDSnapshot.data!)!
.name(context), .name(context),
),
), ),
), ],
], if (appImplementations.error != null) ...[
if (appImplementations.error != null) ...[ const SizedBox(
const SizedBox( width: 8,
width: 8,
),
NeonException(
appImplementations.error,
onRetry: _appsBloc.refresh,
onlyIcon: true,
),
],
if (appImplementations.loading) ...[
const SizedBox(
width: 8,
),
Expanded(
child: NeonLinearProgressIndicator(
color: Theme.of(context).appBarTheme.foregroundColor,
), ),
), NeonException(
appImplementations.error,
onRetry: _appsBloc.refresh,
onlyIcon: true,
),
],
if (appImplementations.loading) ...[
const SizedBox(
width: 8,
),
Expanded(
child: NeonLinearProgressIndicator(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
], ],
),
if (accounts.length > 1) ...[
Text(
account.client.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
], ],
), ],
if (accounts.length > 1) ...[ ),
Text( actions: [
account.client.humanReadableID, if (notificationsAppImplementation.data != null) ...[
style: Theme.of(context).textTheme.bodySmall, StreamBuilder<int>(
stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
return IconButton(
key: Key('app-${notificationsAppImplementation.data!.id}'),
onPressed: () async {
await _openNotifications(
notificationsAppImplementation.data!,
accounts,
account,
);
},
tooltip: AppLocalizations.of(context)
.appImplementationName(notificationsAppImplementation.data!.id),
icon: NeonAppImplementationIcon(
appImplementation: notificationsAppImplementation.data!,
unreadCount: unreadCount,
color: unreadCount > 0
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground,
size: const Size.square(kAvatarSize * 2 / 3),
),
);
},
), ),
], ],
], IconButton(
), onPressed: () {
actions: [ AccountSettingsRoute(accountid: account.id).go(context);
if (notificationsAppImplementation.data != null) ...[
StreamBuilder<int>(
stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
return IconButton(
key: Key('app-${notificationsAppImplementation.data!.id}'),
onPressed: () async {
await _openNotifications(
notificationsAppImplementation.data!,
accounts,
account,
);
},
tooltip: AppLocalizations.of(context)
.appImplementationName(notificationsAppImplementation.data!.id),
icon: NeonAppImplementationIcon(
appImplementation: notificationsAppImplementation.data!,
unreadCount: unreadCount,
color: unreadCount > 0
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground,
size: const Size.square(kAvatarSize * 2 / 3),
),
);
}, },
), tooltip: AppLocalizations.of(context).settingsAccount,
], icon: IntrinsicWidth(
IconButton( child: NeonAccountAvatar(
onPressed: () async { account: account,
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => AccountSettingsPage(
bloc: _accountsBloc,
account: account,
),
), ),
);
},
tooltip: AppLocalizations.of(context).settingsAccount,
icon: IntrinsicWidth(
child: NeonAccountAvatar(
account: account,
), ),
), ),
),
],
),
body: Row(
children: [
if (navigationMode == NavigationMode.quickBar) ...[
drawer,
], ],
Expanded( ),
child: Column( body: Row(
children: [ children: [
if (appImplementations.data != null) ...[ if (navigationMode == NavigationMode.quickBar) ...[
if (appImplementations.data!.isEmpty) ...[ drawer,
Expanded( ],
child: Center( Expanded(
child: Text( child: Column(
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, children: [
textAlign: TextAlign.center, if (appImplementations.data != null) ...[
), if (appImplementations.data!.isEmpty) ...[
),
),
] else ...[
if (activeAppIDSnapshot.hasData) ...[
Expanded( Expanded(
child: appImplementations.data! child: Center(
.find(activeAppIDSnapshot.data!)! child: Text(
.buildPage(context, _appsBloc), AppLocalizations.of(context)
.errorNoCompatibleNextcloudAppsFound,
textAlign: TextAlign.center,
),
),
), ),
] else ...[
if (activeAppIDSnapshot.hasData) ...[
Expanded(
child: appImplementations.data!
.find(activeAppIDSnapshot.data!)!
.buildPage(context, _appsBloc),
),
],
], ],
], ],
], ],
], ),
), ),
), ],
], ),
), ),
), ),
), ],
], );
); }
} }
return Container(); return Container();
}, },

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

@ -73,7 +73,6 @@ class _LoginPageState extends State<LoginPage> {
if (widget.serverURL != null) { if (widget.serverURL != null) {
_accountsBloc.updateAccount(account); _accountsBloc.updateAccount(account);
Navigator.of(context).pop();
} else { } else {
final existingAccount = _accountsBloc.accounts.value.find(account.id); final existingAccount = _accountsBloc.accounts.value.find(account.id);
if (existingAccount != null) { if (existingAccount != null) {

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

@ -80,14 +80,8 @@ class _SettingsPageState extends State<SettingsPage> {
CustomSettingsTile( CustomSettingsTile(
leading: appImplementation.buildIcon(context), leading: appImplementation.buildIcon(context),
title: Text(appImplementation.name(context)), title: Text(appImplementation.name(context)),
onTap: () async { onTap: () {
await Navigator.of(context).push( NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);
MaterialPageRoute(
builder: (final context) => NextcloudAppSettingsPage(
appImplementation: appImplementation,
),
),
);
}, },
), ),
], ],
@ -181,26 +175,15 @@ class _SettingsPageState extends State<SettingsPage> {
for (final account in accountsSnapshot.data!) ...[ for (final account in accountsSnapshot.data!) ...[
NeonAccountSettingsTile( NeonAccountSettingsTile(
account: account, account: account,
onTap: () async { onTap: () {
await Navigator.of(context).push( AccountSettingsRoute(accountid: account.id).go(context);
MaterialPageRoute(
builder: (final context) => AccountSettingsPage(
bloc: accountsBloc,
account: account,
),
),
);
}, },
), ),
], ],
CustomSettingsTile( CustomSettingsTile(
title: ElevatedButton.icon( title: ElevatedButton.icon(
onPressed: () async { onPressed: () {
await Navigator.of(context).push( const LoginRoute().go(context);
MaterialPageRoute(
builder: (final context) => const LoginPage(),
),
);
}, },
icon: const Icon(MdiIcons.accountPlus), icon: const Icon(MdiIcons.accountPlus),
label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd), label: Text(AppLocalizations.of(context).globalOptionsAccountsAdd),

139
packages/neon/neon/lib/src/router.dart

@ -1,41 +1,124 @@
part of '../neon.dart';
// ignore: prefer_mixin // ignore: prefer_mixin
class AppRouter extends RouterDelegate<Account> with ChangeNotifier, PopNavigatorRouterDelegateMixin<Account> { import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:neon/neon.dart';
import 'package:provider/provider.dart';
part 'router.g.dart';
class AppRouter extends GoRouter {
AppRouter({ AppRouter({
required this.navigatorKey, required final GlobalKey<NavigatorState> navigatorKey,
required this.accountsBloc, required final AccountsBloc accountsBloc,
}) : super(
refreshListenable: StreamListenable.behaviorSubject(accountsBloc.activeAccount),
navigatorKey: navigatorKey,
initialLocation: const HomeRoute().location,
redirect: (final context, final state) {
final account = accountsBloc.activeAccount.valueOrNull;
if (account == null) {
return const LoginRoute().location;
}
if (state.location == const LoginRoute().location) {
return const HomeRoute().location;
}
return null;
},
routes: $appRoutes,
);
}
@immutable
class AccountSettingsRoute extends GoRouteData {
const AccountSettingsRoute({
required this.accountid,
}); });
final AccountsBloc accountsBloc; final String accountid;
@override
Widget build(final BuildContext context, final GoRouterState state) {
final bloc = Provider.of<AccountsBloc>(context, listen: false);
final account = bloc.accounts.value.find(accountid)!;
return AccountSettingsPage(
bloc: bloc,
account: account,
);
}
}
@TypedGoRoute<HomeRoute>(
path: '/',
name: 'home',
routes: [
TypedGoRoute<SettingsRoute>(
path: 'settings',
name: 'Settings',
routes: [
TypedGoRoute<NextcloudAppSettingsRoute>(
path: ':appid',
name: 'NextcloudAppSettings',
),
TypedGoRoute<AccountSettingsRoute>(
path: 'account/:accountid',
name: 'AccountSettings',
),
],
)
],
)
@immutable
class HomeRoute extends GoRouteData {
const HomeRoute();
@override @override
final GlobalKey<NavigatorState> navigatorKey; Widget build(final BuildContext context, final GoRouterState state) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final account = accountsBloc.activeAccount.valueOrNull!;
return HomePage(key: Key(account.id));
}
}
@TypedGoRoute<LoginRoute>(
path: '/login',
name: 'login',
)
@immutable
class LoginRoute extends GoRouteData {
const LoginRoute({this.server});
final String? server;
@override @override
Future setNewRoutePath(final Account? configuration) async {} Widget build(final BuildContext context, final GoRouterState state) => LoginPage(serverURL: server);
}
@immutable
class NextcloudAppSettingsRoute extends GoRouteData {
const NextcloudAppSettingsRoute({
required this.appid,
});
final String appid;
@override @override
Account? get currentConfiguration => accountsBloc.activeAccount.valueOrNull; Widget build(final BuildContext context, final GoRouterState state) {
final appImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
final appImplementation = appImplementations.firstWhere((final app) => app.id == appid);
return NextcloudAppSettingsPage(appImplementation: appImplementation);
}
}
@immutable
class SettingsRoute extends GoRouteData {
const SettingsRoute();
@override @override
Widget build(final BuildContext context) => Navigator( Widget build(final BuildContext context, final GoRouterState state) => const SettingsPage();
key: navigatorKey,
onPopPage: (final route, final result) => route.didPop(result),
pages: [
if (currentConfiguration == null) ...[
const MaterialPage(
child: LoginPage(),
),
] else ...[
MaterialPage(
name: 'home',
child: HomePage(
key: Key(currentConfiguration!.id),
account: currentConfiguration!,
),
),
],
],
);
} }

122
packages/neon/neon/lib/src/router.g.dart

@ -0,0 +1,122 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'router.dart';
// **************************************************************************
// GoRouterGenerator
// **************************************************************************
List<RouteBase> get $appRoutes => [
$homeRoute,
$loginRoute,
];
RouteBase get $homeRoute => GoRouteData.$route(
path: '/',
name: 'home',
factory: $HomeRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'settings',
name: 'Settings',
factory: $SettingsRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: ':appid',
name: 'NextcloudAppSettings',
factory: $NextcloudAppSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'account/:accountid',
name: 'AccountSettings',
factory: $AccountSettingsRouteExtension._fromState,
),
],
),
],
);
extension $HomeRouteExtension on HomeRoute {
static HomeRoute _fromState(GoRouterState state) => const HomeRoute();
String get location => GoRouteData.$location(
'/',
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
}
extension $SettingsRouteExtension on SettingsRoute {
static SettingsRoute _fromState(GoRouterState state) => const SettingsRoute();
String get location => GoRouteData.$location(
'/settings',
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
}
extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute(
appid: state.pathParameters['appid']!,
);
String get location => GoRouteData.$location(
'/settings/${Uri.encodeComponent(appid)}',
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
}
extension $AccountSettingsRouteExtension on AccountSettingsRoute {
static AccountSettingsRoute _fromState(GoRouterState state) => AccountSettingsRoute(
accountid: state.pathParameters['accountid']!,
);
String get location => GoRouteData.$location(
'/settings/account/${Uri.encodeComponent(accountid)}',
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
}
RouteBase get $loginRoute => GoRouteData.$route(
path: '/login',
name: 'login',
factory: $LoginRouteExtension._fromState,
);
extension $LoginRouteExtension on LoginRoute {
static LoginRoute _fromState(GoRouterState state) => LoginRoute(
server: state.queryParameters['server'],
);
String get location => GoRouteData.$location(
'/login',
queryParams: {
if (server != null) 'server': server,
},
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
}

8
packages/neon/neon/lib/src/utils/global_popups.dart

@ -27,12 +27,8 @@ class GlobalPopups {
content: Text(AppLocalizations.of(context).firstLaunchGoToSettingsToEnablePushNotifications), content: Text(AppLocalizations.of(context).firstLaunchGoToSettingsToEnablePushNotifications),
action: SnackBarAction( action: SnackBarAction(
label: AppLocalizations.of(context).settings, label: AppLocalizations.of(context).settings,
onPressed: () async { onPressed: () {
await Navigator.of(context).push( const SettingsRoute().go(context);
MaterialPageRoute(
builder: (final context) => const SettingsPage(),
),
);
}, },
), ),
), ),

38
packages/neon/neon/lib/src/utils/stream_listenable.dart

@ -0,0 +1,38 @@
part of '../../neon.dart';
/// Listenable Stream
///
/// A class that implements [Listenable] for a stream.
/// Objects need to be manually disposed.
class StreamListenable extends ChangeNotifier {
/// Listenable Stream
///
/// Implementation for all types of [Stream]s.
/// For an implementation tailored towards [BehaviorSubject] have a look at [StreamListenable.behaviorSubject].
StreamListenable(final Stream<dynamic> stream) {
notifyListeners();
_subscription = stream.asBroadcastStream().listen((final value) {
notifyListeners();
});
}
/// Listenable BehaviorSubject
///
/// Implementation for a [BehaviorSubject]. It ensures to not unececcary notify listeners.
/// For an implementation tailored towards otnher kinds of [Stream] have a look at [StreamListenable].
StreamListenable.behaviorSubject(final BehaviorSubject<dynamic> subject) {
_subscription = subject.listen((final value) {
notifyListeners();
});
}
late final StreamSubscription<dynamic> _subscription;
@override
void dispose() {
unawaited(_subscription.cancel());
super.dispose();
}
}

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

@ -62,7 +62,7 @@ class NeonException extends StatelessWidget {
: AppLocalizations.of(context).actionRetry, : AppLocalizations.of(context).actionRetry,
onPressed: () async { onPressed: () async {
if (details.isUnauthorized) { if (details.isUnauthorized) {
await _openLoginPage(context); _openLoginPage(context);
} else { } else {
onRetry(); onRetry();
} }
@ -177,14 +177,10 @@ class NeonException extends StatelessWidget {
); );
} }
static Future _openLoginPage(final BuildContext context) async { static void _openLoginPage(final BuildContext context) {
await Navigator.of(context).push( LoginRoute(
MaterialPageRoute( server: Provider.of<AccountsBloc>(context, listen: false).activeAccount.value!.serverURL,
builder: (final context) => LoginPage( ).go(context);
serverURL: Provider.of<AccountsBloc>(context, listen: false).activeAccount.value!.serverURL,
),
),
);
} }
} }

4
packages/neon/neon/pubspec.yaml

@ -21,6 +21,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_native_splash: ^2.2.19 flutter_native_splash: ^2.2.19
flutter_svg: ^2.0.5 flutter_svg: ^2.0.5
go_router: ^7.1.1
http: ^0.13.6 http: ^0.13.6
intl: ^0.18.0 intl: ^0.18.0
json_annotation: ^4.8.1 json_annotation: ^4.8.1
@ -56,7 +57,8 @@ dependencies:
xml: ^6.3.0 xml: ^6.3.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.2 build_runner: ^2.4.4
go_router_builder: ^2.0.1
json_serializable: ^6.6.2 json_serializable: ^6.6.2
nit_picking: nit_picking:
git: git:

Loading…
Cancel
Save