Custom link support (#918)

This commit is contained in:
Imran Remtulla
2023-12-15 23:37:04 -05:00
parent 3eca704f4a
commit 415460df75
5 changed files with 165 additions and 80 deletions

View File

@ -8,7 +8,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleInstance"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
@ -30,6 +30,14 @@
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED" android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
android:exported="false"/> android:exported="false"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="obtainium"
android:host="add" />
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -21,10 +23,10 @@ class AddAppPage extends StatefulWidget {
const AddAppPage({super.key}); const AddAppPage({super.key});
@override @override
State<AddAppPage> createState() => _AddAppPageState(); State<AddAppPage> createState() => AddAppPageState();
} }
class _AddAppPageState extends State<AddAppPage> { class AddAppPageState extends State<AddAppPage> {
bool gettingAppInfo = false; bool gettingAppInfo = false;
bool searching = false; bool searching = false;
@ -36,9 +38,57 @@ class _AddAppPageState extends State<AddAppPage> {
bool additionalSettingsValid = true; bool additionalSettingsValid = true;
bool inferAppIdIfOptional = true; bool inferAppIdIfOptional = true;
List<String> pickedCategories = []; List<String> pickedCategories = [];
int searchnum = 0;
SourceProvider sourceProvider = SourceProvider(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppsProvider appsProvider = context.read<AppsProvider>(); AppsProvider appsProvider = context.read<AppsProvider>();
@ -48,47 +98,6 @@ class _AddAppPageState extends State<AddAppPage> {
bool doingSomething = gettingAppInfo || searching; 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<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly, Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
{bool ignoreHideSetting = false}) async { {bool ignoreHideSetting = false}) async {
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly; var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
@ -205,7 +214,7 @@ class _AddAppPageState extends State<AddAppPage> {
children: [ children: [
Expanded( Expanded(
child: GeneratedForm( child: GeneratedForm(
key: Key(searchnum.toString()), key: Key(Random().nextInt(10000).toString()),
items: [ items: [
[ [
GeneratedFormTextField('appSourceURL', GeneratedFormTextField('appSourceURL',
@ -325,7 +334,7 @@ class _AddAppPageState extends State<AddAppPage> {
); );
}); });
if (selectedUrls != null && selectedUrls.isNotEmpty) { if (selectedUrls != null && selectedUrls.isNotEmpty) {
changeUserInput(selectedUrls[0], true, false, isSearch: true); changeUserInput(selectedUrls[0], true, false);
} }
} }
} catch (e) { } catch (e) {

View File

@ -1,4 +1,7 @@
import 'dart:async';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:app_links/app_links.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -30,53 +33,95 @@ class _HomePageState extends State<HomePage> {
bool isReversing = false; bool isReversing = false;
int prevAppCount = -1; int prevAppCount = -1;
bool prevIsLoading = true; bool prevIsLoading = true;
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
List<NavigationPageItem> pages = [ List<NavigationPageItem> pages = [
NavigationPageItem(tr('appsString'), Icons.apps, NavigationPageItem(tr('appsString'), Icons.apps,
AppsPage(key: GlobalKey<AppsPageState>())), AppsPage(key: GlobalKey<AppsPageState>())),
NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()), NavigationPageItem(
tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
NavigationPageItem( NavigationPageItem(
tr('importExport'), Icons.import_export, const ImportExportPage()), tr('importExport'), Icons.import_export, const ImportExportPage()),
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()) NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
]; ];
@override
void initState() {
super.initState();
initDeepLinks();
}
Future<void> initDeepLinks() async {
_appLinks = AppLinks();
goToAddApp(Uri uri) async {
switchToPage(1);
while (
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
null) {
await Future.delayed(const Duration(microseconds: 1));
}
(pages[1].widget.key as GlobalKey<AddAppPageState>?)
?.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<AppsPageState>).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<AppsPageState>).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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppsProvider appsProvider = context.watch<AppsProvider>(); AppsProvider appsProvider = context.watch<AppsProvider>();
SettingsProvider settingsProvider = context.watch<SettingsProvider>(); SettingsProvider settingsProvider = context.watch<SettingsProvider>();
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<AppsPageState>).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 && if (!prevIsLoading &&
prevAppCount >= 0 && prevAppCount >= 0 &&
appsProvider.apps.length > prevAppCount && appsProvider.apps.length > prevAppCount &&
@ -143,4 +188,10 @@ class _HomePageState extends State<HomePage> {
?.clearSelected(); ?.clearSelected();
}); });
} }
@override
void dispose() {
super.dispose();
_linkSubscription?.cancel();
}
} }

View File

@ -42,6 +42,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.10" 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: archive:
dependency: transitive dependency: transitive
description: description:
@ -350,6 +358,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.2.4" version: "8.2.4"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hsluv: hsluv:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -67,6 +67,7 @@ dependencies:
connectivity_plus: ^5.0.0 connectivity_plus: ^5.0.0
shared_storage: ^0.8.0 shared_storage: ^0.8.0
crypto: ^3.0.3 crypto: ^3.0.3
app_links: ^3.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: