diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6984e133e7..5030c533b1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/lib/src/app.dart b/lib/src/app.dart index 52d6d55b89..bee6d29036 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; @@ -23,6 +24,8 @@ import 'package:lichess_mobile/src/quick_actions.dart'; import 'package:lichess_mobile/src/tab_scaffold.dart'; import 'package:lichess_mobile/src/theme.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/more/import_pgn_screen.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; /// Application initialization and main entry point. class AppInitializationScreen extends ConsumerWidget { @@ -65,7 +68,9 @@ class _AppState extends ConsumerState { bool _firstTimeOnlineCheck = false; final _appLinks = AppLinks(); final _navigatorKey = GlobalKey(); + StreamSubscription? _linkSubscription; + StreamSubscription>? _intentSub; @override void initState() { @@ -109,11 +114,13 @@ class _AppState extends ConsumerState { super.initState(); _initAppLinks(); + _initSharingIntent(); } @override void dispose() { _linkSubscription?.cancel(); + _intentSub?.cancel(); super.dispose(); } @@ -149,10 +156,47 @@ class _AppState extends ConsumerState { Future _initAppLinks() async { _linkSubscription = _appLinks.uriLinkStream.listen((uri) { + // File links are handled by the sharing intent logic, so we can ignore them here. + if (uri.scheme == 'file' || uri.scheme == 'content') { + return; + } final context = _navigatorKey.currentContext; if (context != null && context.mounted) { handleAppLink(context, uri); } }); } + + void _initSharingIntent() { + // Warm start + _intentSub = ReceiveSharingIntent.instance.getMediaStream().listen(( + List value, + ) { + _processSharedFiles(value); + }); + + // Cold start + ReceiveSharingIntent.instance.getInitialMedia().then((List value) { + _processSharedFiles(value); + ReceiveSharingIntent.instance.reset(); + }); + } + + Future _processSharedFiles(List files) async { + if (files.isEmpty) return; + final filePath = files.first.path; + try { + final context = _navigatorKey.currentContext; + if (context == null || !context.mounted) return; + + final file = File(filePath); + final pgnText = await file.readAsString(); + + if (context.mounted) { + ImportPgnScreen.handlePgnText(context, pgnText); + } + } catch (e) { + debugPrint('Failed to process incoming file: $e'); + } + } } diff --git a/lib/src/view/more/import_pgn_screen.dart b/lib/src/view/more/import_pgn_screen.dart index e1c5307f15..52c18cae2f 100644 --- a/lib/src/view/more/import_pgn_screen.dart +++ b/lib/src/view/more/import_pgn_screen.dart @@ -22,6 +22,43 @@ class ImportPgnScreen extends StatelessWidget { return buildScreenRoute(context, screen: const ImportPgnScreen()); } + static void handlePgnText(BuildContext context, String text) { + try { + final games = PgnGame.parseMultiGamePgn(text); + + if (games.isEmpty) { + showSnackBar(context, context.l10n.invalidPgn, type: .error); + return; + } + + if (games.length == 1) { + final game = games.first; + final rule = Rule.fromPgn(game.headers['Variant']); + + Navigator.of(context, rootNavigator: true).push( + AnalysisScreen.buildRoute( + context, + AnalysisOptions.pgn( + id: const StringId('pgn_import_single_game'), + orientation: .white, + pgn: text, + isComputerAnalysisAllowed: true, + initialMoveCursor: game.moves.mainline().isEmpty ? 0 : 1, + variant: rule != null ? Variant.fromRule(rule) : .standard, + ), + ), + ); + } else { + Navigator.of( + context, + rootNavigator: true, + ).push(PgnGamesListScreen.buildRoute(context, games.lock)); + } + } catch (_) { + showSnackBar(context, context.l10n.invalidPgn, type: .error); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -73,43 +110,6 @@ class _BodyState extends State<_Body> { ); } - void _handlePgnText(String text) { - try { - final games = PgnGame.parseMultiGamePgn(text); - - if (games.isEmpty) { - showSnackBar(context, context.l10n.invalidPgn, type: SnackBarType.error); - return; - } - - if (games.length == 1) { - final game = games.first; - final rule = Rule.fromPgn(game.headers['Variant']); - - Navigator.of(context, rootNavigator: true).push( - AnalysisScreen.buildRoute( - context, - AnalysisOptions.pgn( - id: const StringId('pgn_import_single_game'), - orientation: Side.white, - pgn: text, - isComputerAnalysisAllowed: true, - initialMoveCursor: game.moves.mainline().isEmpty ? 0 : 1, - variant: rule != null ? Variant.fromRule(rule) : Variant.standard, - ), - ), - ); - } else { - Navigator.of( - context, - rootNavigator: true, - ).push(PgnGamesListScreen.buildRoute(context, games.lock)); - } - } catch (_) { - showSnackBar(context, context.l10n.invalidPgn, type: SnackBarType.error); - } - } - Future _getClipboardData() async { final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data?.text == null) return; @@ -118,7 +118,7 @@ class _BodyState extends State<_Body> { final text = data!.text!.trim(); if (text.isEmpty) return; - _handlePgnText(text); + ImportPgnScreen.handlePgnText(context, text); } Future _pickPgnFile() async { @@ -132,7 +132,7 @@ class _BodyState extends State<_Body> { if (result != null && result.files.single.bytes != null) { final content = utf8.decode(result.files.single.bytes!); if (mounted) { - _handlePgnText(content); + ImportPgnScreen.handlePgnText(context, content); } } } catch (e) { diff --git a/pubspec.lock b/pubspec.lock index 233a947a24..2a81dc2761 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1279,6 +1279,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + receive_sharing_intent: + dependency: "direct main" + description: + path: "." + ref: "2cea396843cd3ab1b5ec4334be4233864637874e" + resolved-ref: "2cea396843cd3ab1b5ec4334be4233864637874e" + url: "https://github.com/KasemJaffer/receive_sharing_intent" + source: git + version: "1.8.1" result_extensions: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ee558355bb..bd34714eec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,6 +68,10 @@ dependencies: popover: ^0.4.0 pub_semver: ^2.1.4 quick_actions: ^1.1.0 + receive_sharing_intent: + git: + url: https://github.com/KasemJaffer/receive_sharing_intent + ref: 2cea396843cd3ab1b5ec4334be4233864637874e result_extensions: ^0.2.0 share_plus: ^12.0.0 shared_preferences: ^2.1.0