Skip to content

Commit

Permalink
Merge pull request #2386 from nextcloud/refactor/neon_framework/url-l…
Browse files Browse the repository at this point in the history
…auncher-hack
  • Loading branch information
provokateurin authored Aug 16, 2024
2 parents ee355da + fe381fb commit b5ad460
Show file tree
Hide file tree
Showing 49 changed files with 197 additions and 547 deletions.
5 changes: 0 additions & 5 deletions packages/neon_framework/lib/src/models/account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,6 @@ abstract class Account implements Credentials, Findable, Built<Account, AccountB

return null;
}

/// Removes the [serverURL] part from the [uri].
///
/// Should be used when trying to push a [uri] from an API to the router as it might contain the scheme, host and sub path of the instance which will not work with the router.
Uri stripUri(Uri uri) => Uri.parse(uri.toString().replaceFirst(serverURL.toString(), ''));
}

/// QRcode Login credentials.
Expand Down
9 changes: 3 additions & 6 deletions packages/neon_framework/lib/src/pages/account_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import 'package:neon_framework/src/theme/dialog.dart';
import 'package:neon_framework/src/widgets/dialog.dart';
import 'package:neon_framework/src/widgets/error.dart';
import 'package:neon_framework/utils.dart';
import 'package:url_launcher/url_launcher.dart';

/// Account settings page.
///
Expand Down Expand Up @@ -54,11 +53,9 @@ class AccountSettingsPage extends StatelessWidget {
case null:
break;
case AccountDeletion.remote:
await launchUrl(
account.serverURL.replace(
path: '${account.serverURL.path}/index.php/settings/user/drop_account',
),
);
if (context.mounted) {
await launchUrl(NeonProvider.of<Account>(context), '/index.php/settings/user/drop_account');
}
case AccountDeletion.local:
final isActive = bloc.activeAccount.valueOrNull == account;

Expand Down
9 changes: 3 additions & 6 deletions packages/neon_framework/lib/src/pages/login_flow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import 'dart:async';

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/l10n/localizations.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/bloc/result.dart';
import 'package:neon_framework/src/blocs/login_flow.dart';
import 'package:neon_framework/src/router.dart';
import 'package:neon_framework/src/widgets/error.dart';
import 'package:neon_framework/src/widgets/linear_progress_indicator.dart';
import 'package:neon_framework/utils.dart';
import 'package:nextcloud/core.dart' as core;
import 'package:url_launcher/url_launcher_string.dart';

@internal
class LoginFlowPage extends StatefulWidget {
Expand Down Expand Up @@ -39,10 +39,7 @@ class _LoginFlowPageState extends State<LoginFlowPage> {

initSubscription = bloc.init.listen((result) async {
if (result.hasData) {
await launchUrlString(
result.requireData.login,
mode: LaunchMode.externalApplication,
);
await launchUrl(NeonProvider.of<Account>(context), result.requireData.login);
}
});

Expand Down
17 changes: 4 additions & 13 deletions packages/neon_framework/lib/src/pages/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/l10n/localizations.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/blocs/accounts.dart';
import 'package:neon_framework/src/models/app_implementation.dart';
import 'package:neon_framework/src/platform/platform.dart';
import 'package:neon_framework/src/router.dart';
import 'package:neon_framework/src/settings/utils/settings_export_helper.dart';
Expand All @@ -21,15 +20,13 @@ import 'package:neon_framework/src/settings/widgets/text_settings_tile.dart';
import 'package:neon_framework/src/theme/branding.dart';
import 'package:neon_framework/src/theme/dialog.dart';
import 'package:neon_framework/src/theme/icons.dart';
import 'package:neon_framework/src/utils/adaptive.dart';
import 'package:neon_framework/src/utils/global_options.dart';
import 'package:neon_framework/src/utils/provider.dart';
import 'package:neon_framework/src/widgets/dialog.dart';
import 'package:neon_framework/src/widgets/error.dart';
import 'package:neon_framework/utils.dart';
import 'package:nextcloud/utils.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:url_launcher/url_launcher_string.dart';

final _log = Logger('SettingsPage');

Expand Down Expand Up @@ -196,10 +193,7 @@ class SettingsPage extends StatelessWidget {
),
title: Text(NeonLocalizations.of(context).sourceCode),
onTap: () async {
await launchUrlString(
branding.sourceCodeURL!,
mode: LaunchMode.externalApplication,
);
await launchUrl(NeonProvider.of<Account>(context), branding.sourceCodeURL!);
},
),
if (branding.issueTrackerURL != null)
Expand All @@ -210,10 +204,7 @@ class SettingsPage extends StatelessWidget {
),
title: Text(NeonLocalizations.of(context).issueTracker),
onTap: () async {
await launchUrlString(
branding.issueTrackerURL!,
mode: LaunchMode.externalApplication,
);
await launchUrl(NeonProvider.of<Account>(context), branding.issueTrackerURL!);
},
),
CustomSettingsTile(
Expand Down
75 changes: 5 additions & 70 deletions packages/neon_framework/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:go_router/go_router.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/blocs/accounts.dart';
import 'package:neon_framework/src/blocs/capabilities.dart';
import 'package:neon_framework/src/models/account.dart';
import 'package:neon_framework/src/pages/account_settings.dart';
import 'package:neon_framework/src/pages/app_implementation_settings.dart';
Expand All @@ -23,7 +22,6 @@ import 'package:neon_framework/src/pages/route_not_found.dart';
import 'package:neon_framework/src/pages/settings.dart';
import 'package:neon_framework/src/utils/findable.dart';
import 'package:neon_framework/src/utils/provider.dart';
import 'package:url_launcher/url_launcher.dart';

part 'router.g.dart';

Expand All @@ -36,49 +34,16 @@ GoRouter buildAppRouter({
debugLogDiagnostics: kDebugMode,
navigatorKey: navigatorKey,
initialLocation: const HomeRoute().location,
onException: onException,
errorBuilder: (context, state) => RouteNotFoundPage(
uri: state.uri,
),
redirect: redirect,
routes: $appRoutes,
);

/// Handles routing exceptions thrown by the [GoRouter] of [buildAppRouter].
@visibleForTesting
Future<void> onException(BuildContext context, GoRouterState state, GoRouter router) async {
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
if (accountsBloc.hasAccounts) {
final capabilitiesBloc = NeonProvider.of<CapabilitiesBloc>(context);
final capabilities = capabilitiesBloc.capabilities.valueOrNull?.data;
final modRewriteWorking = capabilities?.capabilities.coreCapabilities?.core.modRewriteWorking ?? false;

final account = NeonProvider.of<Account>(context);
var uri = account.completeUri(state.uri);

if (uri != state.uri && !modRewriteWorking && !uri.path.startsWith('/index.php')) {
uri = uri.replace(path: '/index.php${uri.path}');
}

await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);

return;
}

if (!context.mounted) {
return;
}

await router.push(RouteNotFoundRoute(uri: state.uri).location);
}

/// Handles redirects of the [GoRouter] of [buildAppRouter].
@visibleForTesting
String? redirect(BuildContext context, GoRouterState state) {
if (state.uri.path.startsWith('/index.php/')) {
return state.uri.path.substring(10);
}

final loginQRcode = LoginQRcode.tryParse(state.uri.toString());
if (loginQRcode != null) {
return LoginCheckServerStatusWithCredentialsRoute(
Expand All @@ -88,17 +53,9 @@ String? redirect(BuildContext context, GoRouterState state) {
).location;
}

final accountsBloc = NeonProvider.of<AccountsBloc>(context);
if (accountsBloc.hasAccounts && state.uri.hasScheme) {
final account = NeonProvider.of<Account>(context);
final strippedUri = account.stripUri(state.uri);
if (strippedUri != state.uri) {
return strippedUri.toString();
}
}

// Redirect to login screen when no account is logged in
// Redirect to login screen when no account is logged in.
// We only check the prefix of the current location as we also don't want to redirect on any of the other login routes.
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
if (!accountsBloc.hasAccounts && !state.matchedLocation.startsWith(const LoginRoute().location)) {
return const LoginRoute().location;
}
Expand Down Expand Up @@ -132,28 +89,6 @@ class AccountSettingsRoute extends GoRouteData {
}
}

/// {@template AppRoutes.RouteNotFoundRoute}
/// Route for the [RouteNotFoundPage].
/// {@endtemplate}
@TypedGoRoute<RouteNotFoundRoute>(path: '/not-found/:uri')
@immutable
class RouteNotFoundRoute extends GoRouteData {
/// {@macro AppRoutes.RouteNotFoundRoute}
const RouteNotFoundRoute({
required this.uri,
});

/// The URI of the route that caused the error.
final Uri uri;

@override
Widget build(BuildContext context, GoRouterState state) {
return RouteNotFoundPage(
uri: uri,
);
}
}

/// {@template AppRoutes.HomeRoute}
/// Route for the [HomePage].
/// {@endtemplate}
Expand Down
24 changes: 0 additions & 24 deletions packages/neon_framework/lib/src/router.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 12 additions & 5 deletions packages/neon_framework/lib/src/testing/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:meta/meta.dart';
// ignore: depend_on_referenced_packages
import 'package:mocktail/mocktail.dart';
import 'package:neon_framework/blocs.dart';
Expand All @@ -23,10 +22,18 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor
// ignore: depend_on_referenced_packages
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';

class MockAccount extends Mock implements Account {
@internal
@override
Uri completeUri(Uri uri) => uri;
// ignore: non_constant_identifier_names
Account MockAccount({
String serverURL = 'https://cloud.example.com:8443/nextcloud',
String username = 'username',
String password = 'password',
}) {
return Account(
(b) => b
..serverURL = Uri.parse(serverURL)
..username = username
..password = password,
);
}

class MockAccountCache<T extends Disposable> extends Mock implements AccountCache<T> {}
Expand Down
10 changes: 10 additions & 0 deletions packages/neon_framework/lib/src/utils/launch_url.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:neon_framework/models.dart';
import 'package:url_launcher/url_launcher.dart' as url_launcher;

/// Completes the [url] using the [account] if necessary and launches it in an external application.
Future<bool> launchUrl(Account account, String url) async {
return url_launcher.launchUrl(
account.completeUri(Uri.parse(url)),
mode: url_launcher.LaunchMode.externalApplication,
);
}
7 changes: 4 additions & 3 deletions packages/neon_framework/lib/src/widgets/app_bar.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/l10n/localizations.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/blocs/unified_search.dart';
import 'package:neon_framework/src/utils/global_options.dart' as global_options;
import 'package:neon_framework/src/utils/launch_url.dart';
import 'package:neon_framework/src/utils/provider.dart';
import 'package:neon_framework/src/widgets/account_switcher_button.dart';
import 'package:neon_framework/src/widgets/unified_search_results.dart';
Expand Down Expand Up @@ -106,9 +107,9 @@ class _NeonAppBarState extends State<NeonAppBar> {
viewBuilder: (_) => Padding(
padding: const EdgeInsets.all(8),
child: NeonUnifiedSearchResults(
onSelected: (entry) {
context.go(entry.resourceUrl);
onSelected: (entry) async {
searchController.closeView('');
await launchUrl(NeonProvider.of<Account>(context), entry.resourceUrl);
},
),
),
Expand Down
6 changes: 1 addition & 5 deletions packages/neon_framework/lib/src/widgets/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import 'package:nextcloud/core.dart' as core;
import 'package:nextcloud/user_status.dart' as user_status;
import 'package:nextcloud/utils.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:url_launcher/url_launcher_string.dart';

/// An button typically used in an [AlertDialog.adaptive].
///
Expand Down Expand Up @@ -700,10 +699,7 @@ class NeonUnifiedPushDialog extends StatelessWidget {
isDefaultAction: true,
onPressed: () async {
Navigator.pop(context);
await launchUrlString(
'https://f-droid.org/packages/$unifiedPushNextPushID',
mode: LaunchMode.externalApplication,
);
await launchUrl(NeonProvider.of<Account>(context), 'https://f-droid.org/packages/$unifiedPushNextPushID');
},
child: Text(
NeonLocalizations.of(context).nextPushSupportedInstall,
Expand Down
1 change: 1 addition & 0 deletions packages/neon_framework/lib/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export 'package:neon_framework/src/utils/dialog.dart';
export 'package:neon_framework/src/utils/exceptions.dart';
export 'package:neon_framework/src/utils/findable.dart';
export 'package:neon_framework/src/utils/hex_color.dart';
export 'package:neon_framework/src/utils/launch_url.dart';
export 'package:neon_framework/src/utils/provider.dart';
export 'package:neon_framework/src/utils/request_manager.dart' show RequestManager, UnwrapCallback;
export 'package:neon_framework/src/utils/validators.dart';
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dashboard_app/l10n/localizations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/utils.dart';
import 'package:nextcloud/dashboard.dart' as dashboard;

/// Displays a single dashboard widget and its items.
Expand Down Expand Up @@ -31,7 +32,7 @@ class DashboardWidget extends StatelessWidget {
child = Tooltip(
message: DashboardLocalizations.of(context).openWidget,
child: InkWell(
onTap: () => context.go(widget.widgetUrl!),
onTap: () async => launchUrl(NeonProvider.of<Account>(context), widget.widgetUrl!),
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: child,
),
Expand Down
Loading

0 comments on commit b5ad460

Please sign in to comment.