From 415460df752ca94a2c0476ab130323019db8d429 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 15 Dec 2023 23:37:04 -0500 Subject: [PATCH] Custom link support (#918) --- android/app/src/main/AndroidManifest.xml | 10 +- lib/pages/add_app.dart | 101 ++++++++++--------- lib/pages/home.dart | 117 ++++++++++++++++------- pubspec.lock | 16 ++++ pubspec.yaml | 1 + 5 files changed, 165 insertions(+), 80 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d48e3f5..b265cfe 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ + + + + + + + diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 0ff0cda..f514a32 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,10 +23,10 @@ class AddAppPage extends StatefulWidget { const AddAppPage({super.key}); @override - State createState() => _AddAppPageState(); + State createState() => AddAppPageState(); } -class _AddAppPageState extends State { +class AddAppPageState extends State { bool gettingAppInfo = false; bool searching = false; @@ -36,9 +38,57 @@ class _AddAppPageState extends State { bool additionalSettingsValid = true; bool inferAppIdIfOptional = true; List pickedCategories = []; - int searchnum = 0; SourceProvider sourceProvider = SourceProvider(); + linkFn(String input) { + try { + if (input.isEmpty) { + throw UnsupportedURLError(); + } + sourceProvider.getSource(input); + changeUserInput(input, true, false); + } catch (e) { + showError(e, context); + } + } + + changeUserInput(String input, bool valid, bool isBuilding) { + userInput = input; + if (!isBuilding) { + setState(() { + var prevHost = pickedSource?.host; + try { + var naturalSource = + valid ? sourceProvider.getSource(userInput) : null; + if (naturalSource != null && + naturalSource.runtimeType.toString() != + HTML().runtimeType.toString()) { + // If input has changed to match a regular source, reset the override + pickedSourceOverride = null; + } + } catch (e) { + // ignore + } + var source = valid + ? sourceProvider.getSource(userInput, + overrideSource: pickedSourceOverride) + : null; + if (pickedSource.runtimeType != source.runtimeType || + (prevHost != null && prevHost != source?.host)) { + pickedSource = source; + additionalSettings = source != null + ? getDefaultValuesFromFormItems( + source.combinedAppSpecificSettingFormItems) + : {}; + additionalSettingsValid = source != null + ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source) + : true; + inferAppIdIfOptional = true; + } + }); + } + } + @override Widget build(BuildContext context) { AppsProvider appsProvider = context.read(); @@ -48,47 +98,6 @@ class _AddAppPageState extends State { bool doingSomething = gettingAppInfo || searching; - changeUserInput(String input, bool valid, bool isBuilding, - {bool isSearch = false}) { - userInput = input; - if (!isBuilding) { - setState(() { - if (isSearch) { - searchnum++; - } - var prevHost = pickedSource?.host; - try { - var naturalSource = - valid ? sourceProvider.getSource(userInput) : null; - if (naturalSource != null && - naturalSource.runtimeType.toString() != - HTML().runtimeType.toString()) { - // If input has changed to match a regular source, reset the override - pickedSourceOverride = null; - } - } catch (e) { - // ignore - } - var source = valid - ? sourceProvider.getSource(userInput, - overrideSource: pickedSourceOverride) - : null; - if (pickedSource.runtimeType != source.runtimeType || - (prevHost != null && prevHost != source?.host)) { - pickedSource = source; - additionalSettings = source != null - ? getDefaultValuesFromFormItems( - source.combinedAppSpecificSettingFormItems) - : {}; - additionalSettingsValid = source != null - ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source) - : true; - inferAppIdIfOptional = true; - } - }); - } - } - Future getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly, {bool ignoreHideSetting = false}) async { var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly; @@ -205,7 +214,7 @@ class _AddAppPageState extends State { children: [ Expanded( child: GeneratedForm( - key: Key(searchnum.toString()), + key: Key(Random().nextInt(10000).toString()), items: [ [ GeneratedFormTextField('appSourceURL', @@ -325,7 +334,7 @@ class _AddAppPageState extends State { ); }); if (selectedUrls != null && selectedUrls.isNotEmpty) { - changeUserInput(selectedUrls[0], true, false, isSearch: true); + changeUserInput(selectedUrls[0], true, false); } } } catch (e) { diff --git a/lib/pages/home.dart b/lib/pages/home.dart index c14e83f..c2dd91d 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:animations/animations.dart'; +import 'package:app_links/app_links.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -30,53 +33,95 @@ class _HomePageState extends State { bool isReversing = false; int prevAppCount = -1; bool prevIsLoading = true; + late AppLinks _appLinks; + StreamSubscription? _linkSubscription; List pages = [ NavigationPageItem(tr('appsString'), Icons.apps, AppsPage(key: GlobalKey())), - NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()), + NavigationPageItem( + tr('addApp'), Icons.add, AddAppPage(key: GlobalKey())), NavigationPageItem( tr('importExport'), Icons.import_export, const ImportExportPage()), NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()) ]; + @override + void initState() { + super.initState(); + initDeepLinks(); + } + + Future initDeepLinks() async { + _appLinks = AppLinks(); + + goToAddApp(Uri uri) async { + switchToPage(1); + while ( + (pages[1].widget.key as GlobalKey?)?.currentState == + null) { + await Future.delayed(const Duration(microseconds: 1)); + } + (pages[1].widget.key as GlobalKey?) + ?.currentState + ?.linkFn(uri.path.length > 1 ? uri.path.substring(1) : ""); + } + + // Check initial link if app was in cold state (terminated) + final appLink = await _appLinks.getInitialAppLink(); + if (appLink != null) { + await goToAddApp(appLink); + } + + // Handle link when app is in warm state (front or background) + _linkSubscription = _appLinks.uriLinkStream.listen((uri) async { + await goToAddApp(uri); + }); + } + + setIsReversing(int targetIndex) { + bool reversing = selectedIndexHistory.isNotEmpty && + selectedIndexHistory.last > targetIndex; + setState(() { + isReversing = reversing; + }); + } + + switchToPage(int index) async { + setIsReversing(index); + if (index == 0) { + while ((pages[0].widget.key as GlobalKey).currentState != + null) { + // Avoid duplicate GlobalKey error + await Future.delayed(const Duration(microseconds: 1)); + } + setState(() { + selectedIndexHistory.clear(); + }); + } else if (selectedIndexHistory.isEmpty || + (selectedIndexHistory.isNotEmpty && + selectedIndexHistory.last != index)) { + // while (index == 1 && + // (pages[0].widget.key as GlobalKey).currentState != + // null) { + // // Avoid duplicate GlobalKey error + // await Future.delayed(const Duration(microseconds: 1)); + // } + setState(() { + int existingInd = selectedIndexHistory.indexOf(index); + if (existingInd >= 0) { + selectedIndexHistory.removeAt(existingInd); + } + selectedIndexHistory.add(index); + }); + } + } + @override Widget build(BuildContext context) { AppsProvider appsProvider = context.watch(); SettingsProvider settingsProvider = context.watch(); - setIsReversing(int targetIndex) { - bool reversing = selectedIndexHistory.isNotEmpty && - selectedIndexHistory.last > targetIndex; - setState(() { - isReversing = reversing; - }); - } - - switchToPage(int index) async { - setIsReversing(index); - if (index == 0) { - while ((pages[0].widget.key as GlobalKey).currentState != - null) { - // Avoid duplicate GlobalKey error - await Future.delayed(const Duration(microseconds: 1)); - } - setState(() { - selectedIndexHistory.clear(); - }); - } else if (selectedIndexHistory.isEmpty || - (selectedIndexHistory.isNotEmpty && - selectedIndexHistory.last != index)) { - setState(() { - int existingInd = selectedIndexHistory.indexOf(index); - if (existingInd >= 0) { - selectedIndexHistory.removeAt(existingInd); - } - selectedIndexHistory.add(index); - }); - } - } - if (!prevIsLoading && prevAppCount >= 0 && appsProvider.apps.length > prevAppCount && @@ -143,4 +188,10 @@ class _HomePageState extends State { ?.clearSelected(); }); } + + @override + void dispose() { + super.dispose(); + _linkSubscription?.cancel(); + } } diff --git a/pubspec.lock b/pubspec.lock index 88905ae..cf74778 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,6 +42,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.10" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb" + url: "https://pub.dev" + source: hosted + version: "3.5.0" archive: dependency: transitive description: @@ -350,6 +358,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.4" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hsluv: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0b9b272..8a9ffeb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: connectivity_plus: ^5.0.0 shared_storage: ^0.8.0 crypto: ^3.0.3 + app_links: ^3.5.0 dev_dependencies: flutter_test: