Compare commits

...

3 Commits

Author SHA1 Message Date
Nikolas Rimikis ebc20a1a4c
refactor(neon,neon_files,neon_news,neon_notes,neon_notifications): deprecate AppImplementation.page 1 year ago
Nikolas Rimikis 23efce9d23
feat(neon): use go_router for apps 1 year ago
Nikolas Rimikis adab3c1722
feat(neon): give add relationship between AppsBloc and AppRouter 1 year ago
  1. 10
      packages/neon/neon/lib/src/app.dart
  2. 14
      packages/neon/neon/lib/src/blocs/accounts.dart
  3. 15
      packages/neon/neon/lib/src/blocs/apps.dart
  4. 19
      packages/neon/neon/lib/src/models/app_implementation.dart
  5. 39
      packages/neon/neon/lib/src/pages/home.dart
  6. 3
      packages/neon/neon/lib/src/pages/settings.dart
  7. 123
      packages/neon/neon/lib/src/router.dart
  8. 320
      packages/neon/neon/lib/src/router.g.dart
  9. 2
      packages/neon/neon/lib/src/widgets/account_switcher.dart
  10. 44
      packages/neon/neon/lib/src/widgets/app_bar.dart
  11. 3
      packages/neon/neon_files/lib/neon_files.dart
  12. 3
      packages/neon/neon_news/lib/neon_news.dart
  13. 3
      packages/neon/neon_notes/lib/neon_notes.dart
  14. 3
      packages/neon/neon_notifications/lib/neon_notifications.dart
  15. 44
      packages/neon/neon_notifications/lib/routes.dart

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

@ -42,15 +42,12 @@ class NeonApp extends StatefulWidget {
// ignore: prefer_mixin // ignore: prefer_mixin
class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.TrayListener, WindowListener { class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.TrayListener, WindowListener {
final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true); final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true);
final _navigatorKey = GlobalKey<NavigatorState>(); late final GlobalKey<NavigatorState> _navigatorKey;
late final Iterable<AppImplementation> _appImplementations; late final Iterable<AppImplementation> _appImplementations;
late final NeonPlatform _platform; late final NeonPlatform _platform;
late final GlobalOptions _globalOptions; late final GlobalOptions _globalOptions;
late final AccountsBloc _accountsBloc; late final AccountsBloc _accountsBloc;
late final _routerDelegate = AppRouter( late final AppRouter _routerDelegate;
navigatorKey: _navigatorKey,
accountsBloc: _accountsBloc,
);
Rect? _lastBounds; Rect? _lastBounds;
@ -63,6 +60,9 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
_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);
_routerDelegate = _accountsBloc.router;
_navigatorKey = _routerDelegate.navigatorKey;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
if (_platform.canUseSystemTray) { if (_platform.canUseSystemTray) {
tray.trayManager.addListener(this); tray.trayManager.addListener(this);

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

@ -10,6 +10,7 @@ import 'package:neon/src/blocs/user_statuses.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/platform/platform.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/settings/models/storage.dart'; import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/utils/account_options.dart'; import 'package:neon/src/utils/account_options.dart';
import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/global_options.dart';
@ -63,6 +64,10 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
this._globalOptions, this._globalOptions,
this._allAppImplementations, this._allAppImplementations,
) { ) {
router = AppRouter(
accountsBloc: this,
appImplementations: _allAppImplementations,
);
accounts accounts
..add(loadAccounts(_storage)) ..add(loadAccounts(_storage))
..listen((final as) async { ..listen((final as) async {
@ -105,6 +110,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
final GlobalOptions _globalOptions; final GlobalOptions _globalOptions;
final Iterable<AppImplementation> _allAppImplementations; final Iterable<AppImplementation> _allAppImplementations;
late final AppRouter router;
final _keyLastUsedAccount = 'last-used-account'; final _keyLastUsedAccount = 'last-used-account';
final _accountsOptions = <String, AccountSpecificOptions>{}; final _accountsOptions = <String, AccountSpecificOptions>{};
@ -130,8 +136,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
BehaviorSubject<List<Account>> accounts = BehaviorSubject<List<Account>>.seeded([]); BehaviorSubject<List<Account>> accounts = BehaviorSubject<List<Account>>.seeded([]);
@override @override
BehaviorSubject<Account?> activeAccount = BehaviorSubject<Account?>() BehaviorSubject<Account?> activeAccount = BehaviorSubject<Account?>();
..distinct((final current, final next) => current?.id != next?.id);
@override @override
void addAccount(final Account account) { void addAccount(final Account account) {
@ -167,7 +172,9 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
@override @override
void setActiveAccount(final Account account) { void setActiveAccount(final Account account) {
activeAccount.add(account); if (activeAccount.valueOrNull != account) {
activeAccount.add(account);
}
} }
@override @override
@ -239,6 +246,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
this, this,
account, account,
_allAppImplementations, _allAppImplementations,
router,
); );
} }

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

@ -10,6 +10,7 @@ import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_ids.dart'; import 'package:neon/src/models/app_ids.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/notifications_interface.dart'; import 'package:neon/src/models/notifications_interface.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/settings/models/nextcloud_app_options.dart'; import 'package:neon/src/settings/models/nextcloud_app_options.dart';
import 'package:neon/src/utils/request_manager.dart'; import 'package:neon/src/utils/request_manager.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -22,6 +23,9 @@ abstract class AppsBlocEvents {
/// If the app is already the active app nothing will happen. /// If the app is already the active app nothing will happen.
/// When using [skipAlreadySet] nothing will be done if there already is an active app. /// When using [skipAlreadySet] nothing will be done if there already is an active app.
void setActiveApp(final String appID, {final bool skipAlreadySet = false}); void setActiveApp(final String appID, {final bool skipAlreadySet = false});
/// Navigates to the active app.
void navigateActiveApp();
} }
abstract class AppsBlocStates { abstract class AppsBlocStates {
@ -46,6 +50,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
this._accountsBloc, this._accountsBloc,
this._account, this._account,
this._allAppImplementations, this._allAppImplementations,
this._router,
) { ) {
apps.listen((final result) { apps.listen((final result) {
appImplementations appImplementations
@ -76,6 +81,10 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
unawaited(_checkCompatibility()); unawaited(_checkCompatibility());
}); });
activeApp.distinct().listen((final app) {
_router.go(app.initialPath);
});
unawaited(refresh()); unawaited(refresh());
} }
@ -154,6 +163,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
final AccountsBloc _accountsBloc; final AccountsBloc _accountsBloc;
final Account _account; final Account _account;
final Iterable<AppImplementation> _allAppImplementations; final Iterable<AppImplementation> _allAppImplementations;
final AppRouter _router;
@override @override
void dispose() { void dispose() {
@ -221,6 +231,11 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
} }
} }
@override
void navigateActiveApp() {
_router.go(activeApp.value.initialPath);
}
T getAppBloc<T extends Bloc>(final AppImplementation<T, dynamic> appImplementation) => T getAppBloc<T extends Bloc>(final AppImplementation<T, dynamic> appImplementation) =>
appImplementation.getBloc(_account); appImplementation.getBloc(_account);

19
packages/neon/neon/lib/src/models/app_implementation.dart

@ -54,8 +54,6 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
BehaviorSubject<int>? getUnreadCounter(final T bloc) => null; BehaviorSubject<int>? getUnreadCounter(final T bloc) => null;
Widget get page;
NeonNavigationDestination destination(final BuildContext context) { NeonNavigationDestination destination(final BuildContext context) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false); final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final account = accountsBloc.activeAccount.value!; final account = accountsBloc.activeAccount.value!;
@ -68,29 +66,20 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
); );
} }
/// Main branch displayed in the home page.
///
/// There's usually no need to override this.
StatefulShellBranch get mainBranch => StatefulShellBranch(
routes: [
route,
],
);
/// Route for the app. /// Route for the app.
/// ///
/// All pages of the app must be specified as subroutes. /// All pages of the app must be specified as subroutes.
/// If this is not [GoRoute] an inital route name must be specified by overriding [initialRouteName]. /// If this is not a [GoRoute] an inital path must be specified by overriding [initialPath].
RouteBase get route; RouteBase get route;
/// Name of the initial route for this app. /// Name of the initial route for this app.
/// ///
/// Subclasses that don't provide a [GoRoute] for [route] must override this. /// Subclasses that don't provide a [GoRoute] for [route] must override this.
String get initialRouteName { String get initialPath {
final route = this.route; final route = this.route;
if (route is GoRoute && route.name != null) { if (route is GoRoute) {
return route.name!; return route.path;
} }
throw FlutterError('No name for the initial route provided.'); throw FlutterError('No name for the initial route provided.');

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

@ -20,9 +20,12 @@ const kQuickBarWidth = kAvatarSize + 20;
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({ const HomePage({
this.appView,
super.key, super.key,
}); });
final Widget? appView;
@override @override
State<HomePage> createState() => _HomePageState(); State<HomePage> createState() => _HomePageState();
} }
@ -123,34 +126,22 @@ class _HomePageState extends State<HomePage> {
const drawer = NeonDrawer(); const drawer = NeonDrawer();
const appBar = NeonAppBar(); const appBar = NeonAppBar();
final appView = ResultBuilder<Iterable<AppImplementation>>.behaviorSubject( final appView = widget.appView ??
stream: _appsBloc.appImplementations, ResultBuilder<Iterable<AppImplementation>>.behaviorSubject(
builder: (final context, final appImplementations) { stream: _appsBloc.appImplementations,
if (!appImplementations.hasData) { builder: (final context, final appImplementations) {
return const SizedBox(); if (appImplementations.hasData && appImplementations.requireData.isEmpty) {
} return Center(
child: Text(
if (appImplementations.requireData.isEmpty) { AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound,
return Center( textAlign: TextAlign.center,
child: Text( ),
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, );
textAlign: TextAlign.center,
),
);
}
return StreamBuilder<AppImplementation>(
stream: _appsBloc.activeApp,
builder: (final context, final activeAppIDSnapshot) {
if (!activeAppIDSnapshot.hasData) {
return const SizedBox();
} }
return activeAppIDSnapshot.requireData.page; return const SizedBox();
}, },
); );
},
);
final body = ValueListenableBuilder<global_options.NavigationMode>( final body = ValueListenableBuilder<global_options.NavigationMode>(
valueListenable: _globalOptions.navigationMode, valueListenable: _globalOptions.navigationMode,

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

@ -59,6 +59,9 @@ class _SettingsPageState extends State<SettingsPage> {
final appImplementations = Provider.of<Iterable<AppImplementation>>(context); final appImplementations = Provider.of<Iterable<AppImplementation>>(context);
final appBar = AppBar( final appBar = AppBar(
leading: BackButton(
onPressed: () => const HomeRoute().go(context),
),
title: Text(AppLocalizations.of(context).settings), title: Text(AppLocalizations.of(context).settings),
actions: [ actions: [
IconButton( IconButton(

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

@ -2,6 +2,7 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -9,6 +10,7 @@ import 'package:meta/meta.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/notifications_interface.dart';
import 'package:neon/src/pages/account_settings.dart'; import 'package:neon/src/pages/account_settings.dart';
import 'package:neon/src/pages/home.dart'; import 'package:neon/src/pages/home.dart';
import 'package:neon/src/pages/login.dart'; import 'package:neon/src/pages/login.dart';
@ -25,9 +27,21 @@ part 'router.g.dart';
@internal @internal
class AppRouter extends GoRouter { class AppRouter extends GoRouter {
AppRouter({ factory AppRouter({
required final GlobalKey<NavigatorState> navigatorKey,
required final AccountsBloc accountsBloc, required final AccountsBloc accountsBloc,
required final Iterable<AppImplementation> appImplementations,
final GlobalKey<NavigatorState>? navigatorKey,
}) =>
AppRouter._(
navigatorKey: navigatorKey ?? GlobalKey<NavigatorState>(),
accountsBloc: accountsBloc,
appImplementations: appImplementations,
);
AppRouter._({
required this.navigatorKey,
required final AccountsBloc accountsBloc,
required final Iterable<AppImplementation> appImplementations,
}) : super( }) : super(
debugLogDiagnostics: kDebugMode, debugLogDiagnostics: kDebugMode,
refreshListenable: StreamListenable(accountsBloc.activeAccount), refreshListenable: StreamListenable(accountsBloc.activeAccount),
@ -50,8 +64,34 @@ class AppRouter extends GoRouter {
return null; return null;
}, },
routes: $appRoutes, routes: [
ShellRoute(
routes: appImplementations
.whereNot((final a) => a is NotificationsAppInterface)
.map((final a) => a.route)
.toList(),
builder: _mainViewBuilder,
),
...appImplementations.whereType<NotificationsAppInterface>().map((final a) => a.route),
...$appRoutes,
],
); );
final GlobalKey<NavigatorState> navigatorKey;
}
Widget _mainViewBuilder(
final BuildContext context,
final GoRouterState state, [
final Widget? child,
]) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final account = accountsBloc.activeAccount.valueOrNull!;
return HomePage(
appView: child,
key: Key(account.id),
);
} }
@immutable @immutable
@ -77,51 +117,20 @@ class AccountSettingsRoute extends GoRouteData {
@TypedGoRoute<HomeRoute>( @TypedGoRoute<HomeRoute>(
path: '/', path: '/',
name: 'home', name: 'home',
routes: [
TypedGoRoute<SettingsRoute>(
path: 'settings',
name: 'Settings',
routes: [
TypedGoRoute<NextcloudAppSettingsRoute>(
path: 'apps/:appid',
name: 'NextcloudAppSettings',
),
TypedGoRoute<_AddAccountRoute>(
path: 'account/add',
name: 'addAccount',
routes: [
TypedGoRoute<_AddAccountFlowRoute>(
path: 'flow',
),
TypedGoRoute<_AddAccountQrcodeRoute>(
path: 'qrcode',
),
TypedGoRoute<_AddAccountCheckServerStatusRoute>(
path: 'check/server',
),
TypedGoRoute<_AddAccountCheckAccountRoute>(
path: 'check/account',
),
],
),
TypedGoRoute<AccountSettingsRoute>(
path: 'account/:accountid',
name: 'AccountSettings',
),
],
),
],
) )
@immutable @immutable
class HomeRoute extends GoRouteData { class HomeRoute extends GoRouteData {
const HomeRoute(); const HomeRoute();
@override @override
Widget build(final BuildContext context, final GoRouterState state) { Widget build(final BuildContext context, final GoRouterState state) => _mainViewBuilder(context, state);
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final account = accountsBloc.activeAccount.valueOrNull!;
return HomePage(key: Key(account.id)); @override
String? redirect(final BuildContext context, final GoRouterState state) {
final appsBloc = Provider.of<AccountsBloc>(context, listen: false).activeAppsBloc;
final activeApp = appsBloc.activeApp.valueOrNull;
return activeApp?.initialPath;
} }
} }
@ -367,6 +376,38 @@ class NextcloudAppSettingsRoute extends GoRouteData {
} }
} }
@TypedGoRoute<SettingsRoute>(
path: '/settings',
name: 'Settings',
routes: [
TypedGoRoute<NextcloudAppSettingsRoute>(
path: 'apps/:appid',
name: 'NextcloudAppSettings',
),
TypedGoRoute<_AddAccountRoute>(
path: 'account/add',
name: 'addAccount',
routes: [
TypedGoRoute<_AddAccountFlowRoute>(
path: 'flow',
),
TypedGoRoute<_AddAccountQrcodeRoute>(
path: 'qrcode',
),
TypedGoRoute<_AddAccountCheckServerStatusRoute>(
path: 'check/server',
),
TypedGoRoute<_AddAccountCheckAccountRoute>(
path: 'check/account',
),
],
),
TypedGoRoute<AccountSettingsRoute>(
path: 'account/:accountid',
name: 'AccountSettings',
),
],
)
@immutable @immutable
class SettingsRoute extends GoRouteData { class SettingsRoute extends GoRouteData {
const SettingsRoute({this.initialCategory}); const SettingsRoute({this.initialCategory});

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

@ -9,54 +9,13 @@ part of 'router.dart';
List<RouteBase> get $appRoutes => [ List<RouteBase> get $appRoutes => [
$homeRoute, $homeRoute,
$loginRoute, $loginRoute,
$settingsRoute,
]; ];
RouteBase get $homeRoute => GoRouteData.$route( RouteBase get $homeRoute => GoRouteData.$route(
path: '/', path: '/',
name: 'home', name: 'home',
factory: $HomeRouteExtension._fromState, factory: $HomeRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'settings',
name: 'Settings',
factory: $SettingsRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'apps/:appid',
name: 'NextcloudAppSettings',
factory: $NextcloudAppSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'account/add',
name: 'addAccount',
factory: $_AddAccountRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'flow',
factory: $_AddAccountFlowRouteExtension._fromState,
),
GoRouteData.$route(
path: 'qrcode',
factory: $_AddAccountQrcodeRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/server',
factory: $_AddAccountCheckServerStatusRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/account',
factory: $_AddAccountCheckAccountRouteExtension._fromState,
),
],
),
GoRouteData.$route(
path: 'account/:accountid',
name: 'AccountSettings',
factory: $AccountSettingsRouteExtension._fromState,
),
],
),
],
); );
extension $HomeRouteExtension on HomeRoute { extension $HomeRouteExtension on HomeRoute {
@ -75,17 +34,35 @@ extension $HomeRouteExtension on HomeRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $SettingsRouteExtension on SettingsRoute { RouteBase get $loginRoute => GoRouteData.$route(
static SettingsRoute _fromState(GoRouterState state) => SettingsRoute( path: '/login',
initialCategory: name: 'login',
_$convertMapValue('initial-category', state.uri.queryParameters, _$SettingsCageoriesEnumMap._$fromName), factory: $LoginRouteExtension._fromState,
); routes: [
GoRouteData.$route(
path: 'flow',
factory: $LoginFlowRouteExtension._fromState,
),
GoRouteData.$route(
path: 'qrcode',
factory: $LoginQrcodeRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/server',
factory: $LoginCheckServerStatusRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/account',
factory: $LoginCheckAccountRouteExtension._fromState,
),
],
);
extension $LoginRouteExtension on LoginRoute {
static LoginRoute _fromState(GoRouterState state) => const LoginRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings', '/login',
queryParams: {
if (initialCategory != null) 'initial-category': _$SettingsCageoriesEnumMap[initialCategory!],
},
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@ -97,24 +74,16 @@ extension $SettingsRouteExtension on SettingsRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
const _$SettingsCageoriesEnumMap = { extension $LoginFlowRouteExtension on LoginFlowRoute {
SettingsCageories.apps: 'apps', static LoginFlowRoute _fromState(GoRouterState state) => LoginFlowRoute(
SettingsCageories.theme: 'theme', serverUrl: state.uri.queryParameters['server-url']!,
SettingsCageories.navigation: 'navigation',
SettingsCageories.pushNotifications: 'push-notifications',
SettingsCageories.startup: 'startup',
SettingsCageories.systemTray: 'system-tray',
SettingsCageories.accounts: 'accounts',
SettingsCageories.other: 'other',
};
extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute(
appid: state.pathParameters['appid']!,
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/apps/${Uri.encodeComponent(appid)}', '/login/flow',
queryParams: {
'server-url': serverUrl,
},
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@ -126,11 +95,11 @@ extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $_AddAccountRouteExtension on _AddAccountRoute { extension $LoginQrcodeRouteExtension on LoginQrcodeRoute {
static _AddAccountRoute _fromState(GoRouterState state) => const _AddAccountRoute(); static LoginQrcodeRoute _fromState(GoRouterState state) => const LoginQrcodeRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/account/add', '/login/qrcode',
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@ -142,15 +111,19 @@ extension $_AddAccountRouteExtension on _AddAccountRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $_AddAccountFlowRouteExtension on _AddAccountFlowRoute { extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute {
static _AddAccountFlowRoute _fromState(GoRouterState state) => _AddAccountFlowRoute( static LoginCheckServerStatusRoute _fromState(GoRouterState state) => LoginCheckServerStatusRoute(
serverUrl: state.uri.queryParameters['server-url']!, serverUrl: state.uri.queryParameters['server-url']!,
loginName: state.uri.queryParameters['login-name'],
password: state.uri.queryParameters['password'],
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/account/add/flow', '/login/check/server',
queryParams: { queryParams: {
'server-url': serverUrl, 'server-url': serverUrl,
if (loginName != null) 'login-name': loginName,
if (password != null) 'password': password,
}, },
); );
@ -163,31 +136,19 @@ extension $_AddAccountFlowRouteExtension on _AddAccountFlowRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $_AddAccountQrcodeRouteExtension on _AddAccountQrcodeRoute { extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute {
static _AddAccountQrcodeRoute _fromState(GoRouterState state) => const _AddAccountQrcodeRoute(); static LoginCheckAccountRoute _fromState(GoRouterState state) => LoginCheckAccountRoute(
String get location => GoRouteData.$location(
'/settings/account/add/qrcode',
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) => context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
extension $_AddAccountCheckServerStatusRouteExtension on _AddAccountCheckServerStatusRoute {
static _AddAccountCheckServerStatusRoute _fromState(GoRouterState state) => _AddAccountCheckServerStatusRoute(
serverUrl: state.uri.queryParameters['server-url']!, serverUrl: state.uri.queryParameters['server-url']!,
loginName: state.uri.queryParameters['login-name']!,
password: state.uri.queryParameters['password']!,
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/account/add/check/server', '/login/check/account',
queryParams: { queryParams: {
'server-url': serverUrl, 'server-url': serverUrl,
'login-name': loginName,
'password': password,
}, },
); );
@ -200,19 +161,57 @@ extension $_AddAccountCheckServerStatusRouteExtension on _AddAccountCheckServerS
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $_AddAccountCheckAccountRouteExtension on _AddAccountCheckAccountRoute { RouteBase get $settingsRoute => GoRouteData.$route(
static _AddAccountCheckAccountRoute _fromState(GoRouterState state) => _AddAccountCheckAccountRoute( path: '/settings',
serverUrl: state.uri.queryParameters['server-url']!, name: 'Settings',
loginName: state.uri.queryParameters['login-name']!, factory: $SettingsRouteExtension._fromState,
password: state.uri.queryParameters['password']!, routes: [
GoRouteData.$route(
path: 'apps/:appid',
name: 'NextcloudAppSettings',
factory: $NextcloudAppSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'account/add',
name: 'addAccount',
factory: $_AddAccountRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'flow',
factory: $_AddAccountFlowRouteExtension._fromState,
),
GoRouteData.$route(
path: 'qrcode',
factory: $_AddAccountQrcodeRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/server',
factory: $_AddAccountCheckServerStatusRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/account',
factory: $_AddAccountCheckAccountRouteExtension._fromState,
),
],
),
GoRouteData.$route(
path: 'account/:accountid',
name: 'AccountSettings',
factory: $AccountSettingsRouteExtension._fromState,
),
],
);
extension $SettingsRouteExtension on SettingsRoute {
static SettingsRoute _fromState(GoRouterState state) => SettingsRoute(
initialCategory:
_$convertMapValue('initial-category', state.uri.queryParameters, _$SettingsCageoriesEnumMap._$fromName),
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/account/add/check/account', '/settings',
queryParams: { queryParams: {
'server-url': serverUrl, if (initialCategory != null) 'initial-category': _$SettingsCageoriesEnumMap[initialCategory!],
'login-name': loginName,
'password': password,
}, },
); );
@ -225,13 +224,24 @@ extension $_AddAccountCheckAccountRouteExtension on _AddAccountCheckAccountRoute
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $AccountSettingsRouteExtension on AccountSettingsRoute { const _$SettingsCageoriesEnumMap = {
static AccountSettingsRoute _fromState(GoRouterState state) => AccountSettingsRoute( SettingsCageories.apps: 'apps',
accountid: state.pathParameters['accountid']!, SettingsCageories.theme: 'theme',
SettingsCageories.navigation: 'navigation',
SettingsCageories.pushNotifications: 'push-notifications',
SettingsCageories.startup: 'startup',
SettingsCageories.systemTray: 'system-tray',
SettingsCageories.accounts: 'accounts',
SettingsCageories.other: 'other',
};
extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute(
appid: state.pathParameters['appid']!,
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/account/${Uri.encodeComponent(accountid)}', '/settings/apps/${Uri.encodeComponent(appid)}',
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@ -243,48 +253,11 @@ extension $AccountSettingsRouteExtension on AccountSettingsRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
T? _$convertMapValue<T>( extension $_AddAccountRouteExtension on _AddAccountRoute {
String key, static _AddAccountRoute _fromState(GoRouterState state) => const _AddAccountRoute();
Map<String, String> map,
T Function(String) converter,
) {
final value = map[key];
return value == null ? null : converter(value);
}
extension<T extends Enum> on Map<T, String> {
T _$fromName(String value) => entries.singleWhere((element) => element.value == value).key;
}
RouteBase get $loginRoute => GoRouteData.$route(
path: '/login',
name: 'login',
factory: $LoginRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'flow',
factory: $LoginFlowRouteExtension._fromState,
),
GoRouteData.$route(
path: 'qrcode',
factory: $LoginQrcodeRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/server',
factory: $LoginCheckServerStatusRouteExtension._fromState,
),
GoRouteData.$route(
path: 'check/account',
factory: $LoginCheckAccountRouteExtension._fromState,
),
],
);
extension $LoginRouteExtension on LoginRoute {
static LoginRoute _fromState(GoRouterState state) => const LoginRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/login', '/settings/account/add',
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@ -296,13 +269,13 @@ extension $LoginRouteExtension on LoginRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $LoginFlowRouteExtension on LoginFlowRoute { extension $_AddAccountFlowRouteExtension on _AddAccountFlowRoute {
static LoginFlowRoute _fromState(GoRouterState state) => LoginFlowRoute( static _AddAccountFlowRoute _fromState(GoRouterState state) => _AddAccountFlowRoute(
serverUrl: state.uri.queryParameters['server-url']!, serverUrl: state.uri.queryParameters['server-url']!,
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/login/flow', '/settings/account/add/flow',
queryParams: { queryParams: {
'server-url': serverUrl, 'server-url': serverUrl,
}, },
@ -317,11 +290,11 @@ extension $LoginFlowRouteExtension on LoginFlowRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $LoginQrcodeRouteExtension on LoginQrcodeRoute { extension $_AddAccountQrcodeRouteExtension on _AddAccountQrcodeRoute {
static LoginQrcodeRoute _fromState(GoRouterState state) => const LoginQrcodeRoute(); static _AddAccountQrcodeRoute _fromState(GoRouterState state) => const _AddAccountQrcodeRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/login/qrcode', '/settings/account/add/qrcode',
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@ -333,19 +306,15 @@ extension $LoginQrcodeRouteExtension on LoginQrcodeRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute { extension $_AddAccountCheckServerStatusRouteExtension on _AddAccountCheckServerStatusRoute {
static LoginCheckServerStatusRoute _fromState(GoRouterState state) => LoginCheckServerStatusRoute( static _AddAccountCheckServerStatusRoute _fromState(GoRouterState state) => _AddAccountCheckServerStatusRoute(
serverUrl: state.uri.queryParameters['server-url']!, serverUrl: state.uri.queryParameters['server-url']!,
loginName: state.uri.queryParameters['login-name'],
password: state.uri.queryParameters['password'],
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/login/check/server', '/settings/account/add/check/server',
queryParams: { queryParams: {
'server-url': serverUrl, 'server-url': serverUrl,
if (loginName != null) 'login-name': loginName,
if (password != null) 'password': password,
}, },
); );
@ -358,15 +327,15 @@ extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute { extension $_AddAccountCheckAccountRouteExtension on _AddAccountCheckAccountRoute {
static LoginCheckAccountRoute _fromState(GoRouterState state) => LoginCheckAccountRoute( static _AddAccountCheckAccountRoute _fromState(GoRouterState state) => _AddAccountCheckAccountRoute(
serverUrl: state.uri.queryParameters['server-url']!, serverUrl: state.uri.queryParameters['server-url']!,
loginName: state.uri.queryParameters['login-name']!, loginName: state.uri.queryParameters['login-name']!,
password: state.uri.queryParameters['password']!, password: state.uri.queryParameters['password']!,
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/login/check/account', '/settings/account/add/check/account',
queryParams: { queryParams: {
'server-url': serverUrl, 'server-url': serverUrl,
'login-name': loginName, 'login-name': loginName,
@ -382,3 +351,34 @@ extension $LoginCheckAccountRouteExtension on LoginCheckAccountRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(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);
void replace(BuildContext context) => context.replace(location);
}
T? _$convertMapValue<T>(
String key,
Map<String, String> map,
T Function(String) converter,
) {
final value = map[key];
return value == null ? null : converter(value);
}
extension<T extends Enum> on Map<T, String> {
T _$fromName(String value) => entries.singleWhere((element) => element.value == value).key;
}

2
packages/neon/neon/lib/src/widgets/account_switcher.dart

@ -57,7 +57,7 @@ class AccountSwitcherButton extends StatelessWidget {
title: Text(AppLocalizations.of(context).settingsAccountManage), title: Text(AppLocalizations.of(context).settingsAccountManage),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
const SettingsRoute(initialCategory: SettingsCageories.accounts).push(context); const SettingsRoute(initialCategory: SettingsCageories.accounts).go(context);
}, },
), ),
], ],

44
packages/neon/neon/lib/src/widgets/app_bar.dart

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart'; import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/result_builder.dart'; import 'package:neon/src/bloc/result_builder.dart';
@ -100,7 +101,6 @@ class NotificationIconButton extends StatefulWidget {
class _NotificationIconButtonState extends State<NotificationIconButton> { class _NotificationIconButtonState extends State<NotificationIconButton> {
late AccountsBloc _accountsBloc; late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc; late AppsBloc _appsBloc;
late List<Account> _accounts;
late Account _account; late Account _account;
late StreamSubscription notificationSubscription; late StreamSubscription notificationSubscription;
@ -109,13 +109,12 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
super.initState(); super.initState();
_accountsBloc = Provider.of<AccountsBloc>(context, listen: false); _accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
_appsBloc = _accountsBloc.activeAppsBloc; _appsBloc = _accountsBloc.activeAppsBloc;
_accounts = _accountsBloc.accounts.value;
_account = _accountsBloc.activeAccount.value!; _account = _accountsBloc.activeAccount.value!;
notificationSubscription = _appsBloc.openNotifications.listen((final _) async { notificationSubscription = _appsBloc.openNotifications.listen((final _) {
final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull; final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull;
if (notificationsAppImplementation != null && notificationsAppImplementation.hasData) { if (notificationsAppImplementation != null && notificationsAppImplementation.hasData) {
await _openNotifications(notificationsAppImplementation.data!); _openNotifications(notificationsAppImplementation.data!);
} }
}); });
} }
@ -127,37 +126,8 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
super.dispose(); super.dispose();
} }
// TODO: migrate to go_router with a separate page void _openNotifications(final NotificationsAppInterface app) {
Future<void> _openNotifications( GoRouter.of(context).go(app.initialPath);
final NotificationsAppInterface app,
) async {
final page = Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(app.name(context)),
if (_accounts.length > 1) ...[
Text(
_account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
),
body: app.page,
);
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => Provider<NotificationsBlocInterface>(
create: (final context) => app.getBloc(_account),
child: page,
),
),
);
} }
@override @override
@ -177,8 +147,8 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
final unreadCount = unreadCounterSnapshot.data ?? 0; final unreadCount = unreadCounterSnapshot.data ?? 0;
return IconButton( return IconButton(
key: Key('app-${notificationsImplementationData.id}'), key: Key('app-${notificationsImplementationData.id}'),
onPressed: () async { onPressed: () {
await _openNotifications(notificationsImplementationData); _openNotifications(notificationsImplementationData);
}, },
tooltip: AppLocalizations.of(context).appImplementationName(notificationsImplementationData.id), tooltip: AppLocalizations.of(context).appImplementationName(notificationsImplementationData.id),
icon: NeonAppImplementationIcon( icon: NeonAppImplementationIcon(

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

@ -66,9 +66,6 @@ class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
platform, platform,
); );
@override
Widget get page => const FilesMainPage();
@override @override
RouteBase get route => $filesAppRoute; RouteBase get route => $filesAppRoute;
} }

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

@ -73,9 +73,6 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
account, account,
); );
@override
Widget get page => const NewsMainPage();
@override @override
RouteBase get route => $newsAppRoute; RouteBase get route => $newsAppRoute;

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

@ -63,9 +63,6 @@ class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> {
account, account,
); );
@override
Widget get page => const NotesMainPage();
@override @override
RouteBase get route => $notesAppRoute; RouteBase get route => $notesAppRoute;
} }

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

@ -43,9 +43,6 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
account, account,
); );
@override
Widget get page => const NotificationsMainPage();
@override @override
RouteBase get route => $notificationsAppRoute; RouteBase get route => $notificationsAppRoute;

44
packages/neon/neon_notifications/lib/routes.dart

@ -1,8 +1,11 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:neon/blocs.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/models.dart'; import 'package:neon/models.dart';
import 'package:neon/utils.dart'; import 'package:neon/utils.dart';
import 'package:neon_notifications/neon_notifications.dart'; import 'package:neon_notifications/neon_notifications.dart';
import 'package:provider/provider.dart';
part 'routes.g.dart'; part 'routes.g.dart';
@ -15,5 +18,42 @@ class NotificationsAppRoute extends NeonAppRoute {
const NotificationsAppRoute(); const NotificationsAppRoute();
@override @override
Widget build(final BuildContext context, final GoRouterState state) => const NotificationsMainPage(); Widget build(final BuildContext context, final GoRouterState state) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final appsBloc = accountsBloc.activeAppsBloc;
final app = appsBloc.notificationsAppImplementation.value.data!;
final accounts = accountsBloc.accounts.value;
final account = accountsBloc.activeAccount.value!;
Widget title = Text(AppLocalizations.of(context).appImplementationName(AppIDs.notifications));
if (accounts.length > 1) {
title = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
Text(
account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
final page = Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: BackButton(
onPressed: appsBloc.navigateActiveApp,
),
title: title,
),
body: const NotificationsMainPage(),
);
return Provider<NotificationsBlocInterface>(
create: (final context) => app.getBloc(account),
child: page,
);
}
} }

Loading…
Cancel
Save