mirror of
https://github.com/lichess-org/mobile.git
synced 2026-05-26 13:50:52 +00:00
Improve Broadcast Deeplinks (#2597)
* add app_links * Update lib/src/app_links.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Code Review --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
<data android:pathPattern="/......../black" />
|
<data android:pathPattern="/......../black" />
|
||||||
<data android:pathPattern="/......../white" />
|
<data android:pathPattern="/......../white" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
|
||||||
</activity>
|
</activity>
|
||||||
<receiver android:exported="false"
|
<receiver android:exported="false"
|
||||||
android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"/>
|
android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"/>
|
||||||
|
|||||||
@@ -115,6 +115,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>FlutterDeepLinkingEnabled</key>
|
||||||
|
<false/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
|||||||
+24
-10
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -19,7 +22,6 @@ import 'package:lichess_mobile/src/network/socket.dart';
|
|||||||
import 'package:lichess_mobile/src/quick_actions.dart';
|
import 'package:lichess_mobile/src/quick_actions.dart';
|
||||||
import 'package:lichess_mobile/src/tab_scaffold.dart';
|
import 'package:lichess_mobile/src/tab_scaffold.dart';
|
||||||
import 'package:lichess_mobile/src/theme.dart';
|
import 'package:lichess_mobile/src/theme.dart';
|
||||||
import 'package:lichess_mobile/src/utils/navigation.dart';
|
|
||||||
import 'package:lichess_mobile/src/utils/screen.dart';
|
import 'package:lichess_mobile/src/utils/screen.dart';
|
||||||
|
|
||||||
/// Application initialization and main entry point.
|
/// Application initialization and main entry point.
|
||||||
@@ -61,6 +63,9 @@ class Application extends ConsumerStatefulWidget {
|
|||||||
class _AppState extends ConsumerState<Application> {
|
class _AppState extends ConsumerState<Application> {
|
||||||
/// Whether the app has checked for online status for the first time.
|
/// Whether the app has checked for online status for the first time.
|
||||||
bool _firstTimeOnlineCheck = false;
|
bool _firstTimeOnlineCheck = false;
|
||||||
|
final _appLinks = AppLinks();
|
||||||
|
final _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
StreamSubscription<Uri>? _linkSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -103,6 +108,13 @@ class _AppState extends ConsumerState<Application> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_initAppLinks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_linkSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -114,6 +126,7 @@ class _AppState extends ConsumerState<Application> {
|
|||||||
final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
|
final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
navigatorKey: _navigatorKey,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
...AppLocalizations.localizationsDelegates,
|
...AppLocalizations.localizationsDelegates,
|
||||||
MaterialLocalizationsEo.delegate,
|
MaterialLocalizationsEo.delegate,
|
||||||
@@ -129,16 +142,17 @@ class _AppState extends ConsumerState<Application> {
|
|||||||
context,
|
context,
|
||||||
).copyWith(height: isShortVerticalScreen(context) ? 60 : null),
|
).copyWith(height: isShortVerticalScreen(context) ? 60 : null),
|
||||||
),
|
),
|
||||||
onGenerateRoute: (settings) =>
|
home: const MainTabScaffold(),
|
||||||
settings.name != null ? resolveAppLinkUri(context, Uri.parse(settings.name!)) : null,
|
|
||||||
onGenerateInitialRoutes: (initialRoute) {
|
|
||||||
final homeRoute = buildScreenRoute<void>(context, screen: const MainTabScaffold());
|
|
||||||
return <Route<dynamic>?>[
|
|
||||||
homeRoute,
|
|
||||||
resolveAppLinkUri(context, Uri.parse(initialRoute)),
|
|
||||||
].nonNulls.toList(growable: false);
|
|
||||||
},
|
|
||||||
navigatorObservers: [rootNavPageRouteObserver],
|
navigatorObservers: [rootNavPageRouteObserver],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initAppLinks() async {
|
||||||
|
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
|
||||||
|
final context = _navigatorKey.currentContext;
|
||||||
|
if (context != null && context.mounted) {
|
||||||
|
handleAppLink(context, uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-27
@@ -14,56 +14,77 @@ import 'package:lichess_mobile/src/view/study/study_screen.dart';
|
|||||||
import 'package:lichess_mobile/src/view/tournament/tournament_screen.dart';
|
import 'package:lichess_mobile/src/view/tournament/tournament_screen.dart';
|
||||||
import 'package:lichess_mobile/src/view/user/user_or_profile_screen.dart';
|
import 'package:lichess_mobile/src/view/user/user_or_profile_screen.dart';
|
||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
/// Resolves an app link [Uri] to a corresponding [Route].
|
final _logger = Logger('AppLinks');
|
||||||
Route<dynamic>? resolveAppLinkUri(BuildContext context, Uri appLinkUri) {
|
|
||||||
if (appLinkUri.pathSegments.isEmpty) return null;
|
|
||||||
|
|
||||||
|
/// Resolves an app link [Uri] to one or more corresponding [Route]s.
|
||||||
|
List<Route<dynamic>>? resolveAppLinkUri(BuildContext context, Uri appLinkUri) {
|
||||||
|
if (appLinkUri.pathSegments.isEmpty) return null;
|
||||||
|
_logger.info('Resolving app link: $appLinkUri');
|
||||||
switch (appLinkUri.pathSegments[0]) {
|
switch (appLinkUri.pathSegments[0]) {
|
||||||
case 'study':
|
case 'study':
|
||||||
final id = appLinkUri.pathSegments[1];
|
final id = appLinkUri.pathSegments[1];
|
||||||
return StudyScreen.buildRoute(context, StudyId(id));
|
return [StudyScreen.buildRoute(context, StudyId(id))];
|
||||||
case 'broadcast':
|
case 'broadcast':
|
||||||
final roundId = BroadcastRoundId(appLinkUri.pathSegments[3]);
|
final roundId = BroadcastRoundId(appLinkUri.pathSegments[3]);
|
||||||
if (appLinkUri.pathSegments.length > 4) {
|
if (appLinkUri.pathSegments.length > 4) {
|
||||||
final gameId = BroadcastGameId(appLinkUri.pathSegments[4]);
|
final gameId = BroadcastGameId(appLinkUri.pathSegments[4]);
|
||||||
return BroadcastGameScreen.buildRoute(context, roundId: roundId, gameId: gameId);
|
return [
|
||||||
|
BroadcastRoundScreenLoading.buildRoute(
|
||||||
|
context,
|
||||||
|
roundId,
|
||||||
|
initialTab: BroadcastRoundTab.boards,
|
||||||
|
),
|
||||||
|
BroadcastGameScreen.buildRoute(context, roundId: roundId, gameId: gameId),
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
final tab = BroadcastRoundTab.tabOrNullFromString(appLinkUri.fragment);
|
final tab = BroadcastRoundTab.tabOrNullFromString(appLinkUri.fragment);
|
||||||
return BroadcastRoundScreenLoading.buildRoute(context, roundId, initialTab: tab);
|
return [BroadcastRoundScreenLoading.buildRoute(context, roundId, initialTab: tab)];
|
||||||
}
|
}
|
||||||
case 'tournament':
|
case 'tournament':
|
||||||
final tournamentId = TournamentId(appLinkUri.pathSegments[1]);
|
final tournamentId = TournamentId(appLinkUri.pathSegments[1]);
|
||||||
return TournamentScreen.buildRoute(context, tournamentId);
|
return [TournamentScreen.buildRoute(context, tournamentId)];
|
||||||
|
|
||||||
case 'training':
|
case 'training':
|
||||||
final id = appLinkUri.pathSegments[1];
|
final id = appLinkUri.pathSegments[1];
|
||||||
return PuzzleScreen.buildRoute(
|
return [
|
||||||
context,
|
PuzzleScreen.buildRoute(context, angle: PuzzleAngle.fromKey('mix'), puzzleId: PuzzleId(id)),
|
||||||
angle: PuzzleAngle.fromKey('mix'),
|
];
|
||||||
puzzleId: PuzzleId(id),
|
|
||||||
);
|
|
||||||
case _:
|
case _:
|
||||||
final gameId = GameId(appLinkUri.pathSegments[0]);
|
final gameId = GameId(appLinkUri.pathSegments[0]);
|
||||||
final orientation = appLinkUri.pathSegments.getOrNull(1);
|
final orientation = appLinkUri.pathSegments.getOrNull(1);
|
||||||
final int ply = int.tryParse(appLinkUri.fragment) ?? 0;
|
final int ply = int.tryParse(appLinkUri.fragment) ?? 0;
|
||||||
// The game id can also be a challenge. Challenge by link is not supported yet so let's ignore it.
|
// The game id can also be a challenge. Challenge by link is not supported yet so let's ignore it.
|
||||||
if (gameId.isValid) {
|
if (gameId.isValid) {
|
||||||
return AnalysisScreen.buildRoute(
|
return [
|
||||||
context,
|
AnalysisScreen.buildRoute(
|
||||||
AnalysisOptions.archivedGame(
|
context,
|
||||||
orientation: orientation == 'black' ? Side.black : Side.white,
|
AnalysisOptions.archivedGame(
|
||||||
gameId: gameId,
|
orientation: orientation == 'black' ? Side.black : Side.white,
|
||||||
initialMoveCursor: ply,
|
gameId: gameId,
|
||||||
|
initialMoveCursor: ply,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles an app link [Uri] by navigating to the corresponding screen(s).
|
||||||
|
void handleAppLink(BuildContext context, Uri uri) {
|
||||||
|
final routes = resolveAppLinkUri(context, uri);
|
||||||
|
if (routes != null) {
|
||||||
|
for (final route in routes) {
|
||||||
|
Navigator.of(context).push(route);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
launchUrl(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const kLichessLinkifiers = [UrlLinkifier(), EmailLinkifier(), UserTagLinkifier()];
|
const kLichessLinkifiers = [UrlLinkifier(), EmailLinkifier(), UserTagLinkifier()];
|
||||||
|
|
||||||
/// Handles link clicks in Linkify widgets throughout the app.
|
/// Handles link clicks in Linkify widgets throughout the app.
|
||||||
@@ -71,13 +92,7 @@ void onLinkifyOpen(BuildContext context, LinkableElement link) {
|
|||||||
if (link is UrlElement && link.url.startsWith(RegExp('https?:\\/\\/$kLichessHost'))) {
|
if (link is UrlElement && link.url.startsWith(RegExp('https?:\\/\\/$kLichessHost'))) {
|
||||||
// Handle Lichess links specifically
|
// Handle Lichess links specifically
|
||||||
final appLinkUri = Uri.parse(link.url);
|
final appLinkUri = Uri.parse(link.url);
|
||||||
final route = resolveAppLinkUri(context, appLinkUri);
|
handleAppLink(context, appLinkUri);
|
||||||
if (route != null) {
|
|
||||||
Navigator.of(context).push(route);
|
|
||||||
} else {
|
|
||||||
// If the link is not recognized, open it in a browser
|
|
||||||
launchUrl(appLinkUri);
|
|
||||||
}
|
|
||||||
} else if (link.originText.startsWith('@')) {
|
} else if (link.originText.startsWith('@')) {
|
||||||
final username = link.originText.substring(1);
|
final username = link.originText.substring(1);
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <dynamic_system_colors/dynamic_color_plugin.h>
|
#include <dynamic_system_colors/dynamic_color_plugin.h>
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
#include <gtk/gtk_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
@@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
|
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
dynamic_system_colors
|
dynamic_system_colors
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
|
gtk
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.3"
|
||||||
|
app_links:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: app_links
|
||||||
|
sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
app_links_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_linux
|
||||||
|
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
app_links_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_platform_interface
|
||||||
|
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
app_links_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_web
|
||||||
|
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
app_settings:
|
app_settings:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -726,6 +758,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
gtk:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: gtk
|
||||||
|
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ environment:
|
|||||||
flutter: ^3.38.0
|
flutter: ^3.38.0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
app_links: ^7.0.0
|
||||||
app_settings: ^7.0.0
|
app_settings: ^7.0.0
|
||||||
async: ^2.10.0
|
async: ^2.10.0
|
||||||
auto_size_text: ^3.0.0
|
auto_size_text: ^3.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user