diff --git a/assets/translations/de.json b/assets/translations/de.json index e20a601..7e0f16e 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -188,6 +188,17 @@ "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", + "install": "Install", + "markInstalled": "Mark Installed", + "update": "Update", + "markUpdated": "Mark Updated", + "additionalOptions": "Additional Options", + "disableVersionDetection": "Disable Version Detection", + "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", + "downloadingX": "Downloading {}", + "downloadNotifDescription": "Notifies the user of the progress in downloading an App", + "noAPKFound": "No APK found", + "noVersionDetection": "No version detection", "tooManyRequestsTryAgainInMinutes": { "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut", "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut" diff --git a/assets/translations/en.json b/assets/translations/en.json index 8aa0a06..215fd2b 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -188,6 +188,17 @@ "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", + "install": "Install", + "markInstalled": "Mark Installed", + "update": "Update", + "markUpdated": "Mark Updated", + "additionalOptions": "Additional Options", + "disableVersionDetection": "Disable Version Detection", + "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", + "downloadingX": "Downloading {}", + "downloadNotifDescription": "Notifies the user of the progress in downloading an App", + "noAPKFound": "No APK found", + "noVersionDetection": "No version detection", "tooManyRequestsTryAgainInMinutes": { "one": "Too many requests (rate limited) - try again in {} minute", "other": "Too many requests (rate limited) - try again in {} minutes" diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 0866789..985646c 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -188,6 +188,17 @@ "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", + "install": "Install", + "markInstalled": "Mark Installed", + "update": "Update", + "markUpdated": "Mark Updated", + "additionalOptions": "Additional Options", + "disableVersionDetection": "Disable Version Detection", + "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", + "downloadingX": "Downloading {}", + "downloadNotifDescription": "Notifies the user of the progress in downloading an App", + "noAPKFound": "No APK found", + "noVersionDetection": "No version detection", "tooManyRequestsTryAgainInMinutes": { "one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva", "other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva" @@ -232,4 +243,4 @@ "one": "A(z) {} és 1 további alkalmazás frissítve.", "other": "{} és további {} alkalmazás frissítve." } -} +} \ No newline at end of file diff --git a/assets/translations/it.json b/assets/translations/it.json index 46d2824..79086e8 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -188,6 +188,17 @@ "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", + "install": "Install", + "markInstalled": "Mark Installed", + "update": "Update", + "markUpdated": "Mark Updated", + "additionalOptions": "Additional Options", + "disableVersionDetection": "Disable Version Detection", + "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", + "downloadingX": "Downloading {}", + "downloadNotifDescription": "Notifies the user of the progress in downloading an App", + "noAPKFound": "No APK found", + "noVersionDetection": "No version detection", "tooManyRequestsTryAgainInMinutes": { "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" @@ -232,4 +243,4 @@ "one": "{} e un'altra App sono state aggiornate.", "other": "{} e altre {} App sono state aggiornate." } -} +} \ No newline at end of file diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 11e87da..278a962 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -188,6 +188,17 @@ "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", + "install": "Install", + "markInstalled": "Mark Installed", + "update": "Update", + "markUpdated": "Mark Updated", + "additionalOptions": "Additional Options", + "disableVersionDetection": "Disable Version Detection", + "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", + "downloadingX": "Downloading {}", + "downloadNotifDescription": "Notifies the user of the progress in downloading an App", + "noAPKFound": "No APK found", + "noVersionDetection": "No version detection", "tooManyRequestsTryAgainInMinutes": { "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" @@ -232,4 +243,4 @@ "one": "{}とさらに{}個のアプリがアップデートされました。", "other": "{}とさらに{}個のアプリがアップデートされました。" } -} +} \ No newline at end of file diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 50e8040..e88a99f 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -188,6 +188,17 @@ "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", + "install": "Install", + "markInstalled": "Mark Installed", + "update": "Update", + "markUpdated": "Mark Updated", + "additionalOptions": "Additional Options", + "disableVersionDetection": "Disable Version Detection", + "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", + "downloadingX": "Downloading {}", + "downloadNotifDescription": "Notifies the user of the progress in downloading an App", + "noAPKFound": "No APK found", + "noVersionDetection": "No version detection", "tooManyRequestsTryAgainInMinutes": { "one": "请求过多 (API 限制) - 在 {} 分钟后重试", "other": "请求过多 (API 限制) - 在 {} 分钟后重试" diff --git a/lib/app_sources/apkmirror.dart b/lib/app_sources/apkmirror.dart index 1372d16..b562b89 100644 --- a/lib/app_sources/apkmirror.dart +++ b/lib/app_sources/apkmirror.dart @@ -25,8 +25,9 @@ class APKMirror extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { Response res = await get(Uri.parse('$standardUrl/feed')); if (res.statusCode == 200) { String? titleString = parse(res.body) diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index 8b7af89..f5ef19f 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -32,7 +32,7 @@ class FDroid extends AppSource { @override String? tryInferringAppId(String standardUrl, - {List additionalData = const []}) { + {Map additionalSettings = const {}}) { return Uri.parse(standardUrl).pathSegments.last; } @@ -60,8 +60,9 @@ class FDroid extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { String? appId = tryInferringAppId(standardUrl); return getAPKUrlsFromFDroidPackagesAPIResponse( await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')), diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index c13a51d..dd4c85f 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -9,13 +9,12 @@ class FDroidRepo extends AppSource { FDroidRepo() { name = tr('fdroidThirdPartyRepo'); - additionalSourceAppSpecificFormItems = [ + additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormItem( + GeneratedFormItem('appIdOrName', label: tr('appIdOrName'), hint: tr('reposHaveMultipleApps'), - required: true, - key: 'appIdOrName') + required: true) ] ]; } @@ -33,13 +32,10 @@ class FDroidRepo extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { - String? appIdOrName = findGeneratedFormValueByKey( - additionalSourceAppSpecificFormItems - .reduce((value, element) => [...value, ...element]), - additionalData, - 'appIdOrName'); + String standardUrl, + Map additionalSettings, + ) async { + String? appIdOrName = additionalSettings['appIdOrName']; if (appIdOrName == null) { throw NoReleasesError(); } diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index 3fdbf4a..3cffd37 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -12,12 +12,9 @@ class GitHub extends AppSource { GitHub() { host = 'github.com'; - additionalSourceAppSpecificDefaults = ['true', 'true', '']; - additionalSourceSpecificSettingFormItems = [ - GeneratedFormItem( + GeneratedFormItem('github-creds', label: tr('githubPATLabel'), - id: 'github-creds', required: false, additionalValidators: [ (value) { @@ -52,17 +49,21 @@ class GitHub extends AppSource { ]) ]; - additionalSourceAppSpecificFormItems = [ + additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormItem( - label: tr('includePrereleases'), type: FormItemType.bool) + GeneratedFormItem('includePrereleases', + label: tr('includePrereleases'), + type: FormItemType.bool, + defaultValue: 'true') ], [ - GeneratedFormItem( - label: tr('fallbackToOlderReleases'), type: FormItemType.bool) + GeneratedFormItem('fallbackToOlderReleases', + label: tr('fallbackToOlderReleases'), + type: FormItemType.bool, + defaultValue: 'true') ], [ - GeneratedFormItem( + GeneratedFormItem('filterReleaseTitlesByRegEx', label: tr('filterReleaseTitlesByRegEx'), type: FormItemType.string, required: false, @@ -99,7 +100,7 @@ class GitHub extends AppSource { SettingsProvider settingsProvider = SettingsProvider(); await settingsProvider.initializeSettings(); String? creds = settingsProvider - .getSettingString(additionalSourceSpecificSettingFormItems[0].id); + .getSettingString(additionalSourceSpecificSettingFormItems[0].key); return creds != null && creds.isNotEmpty ? '$creds@' : ''; } @@ -109,15 +110,16 @@ class GitHub extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { - var includePrereleases = - additionalData.isNotEmpty && additionalData[0] == 'true'; + String standardUrl, + Map additionalSettings, + ) async { + var includePrereleases = additionalSettings['includePrereleases'] == 'true'; var fallbackToOlderReleases = - additionalData.length >= 2 && additionalData[1] == 'true'; - var regexFilter = additionalData.length >= 3 && additionalData[2].isNotEmpty - ? additionalData[2] - : null; + additionalSettings['fallbackToOlderReleases'] == 'true'; + var regexFilter = + additionalSettings['filterReleaseTitlesByRegEx']?.isNotEmpty == true + ? additionalSettings['filterReleaseTitlesByRegEx'] + : null; Response res = await get(Uri.parse( 'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases')); if (res.statusCode == 200) { @@ -148,7 +150,7 @@ class GitHub extends AppSource { continue; } var apkUrls = getReleaseAPKUrls(releases[i]); - if (apkUrls.isEmpty && !trackOnly) { + if (apkUrls.isEmpty && additionalSettings['trackOnly'] != 'true') { continue; } targetRelease = releases[i]; diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 6c2f5ab..225c610 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -25,8 +25,9 @@ class GitLab extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); if (res.statusCode == 200) { var standardUri = Uri.parse(standardUrl); diff --git a/lib/app_sources/izzyondroid.dart b/lib/app_sources/izzyondroid.dart index 13ea4bd..eb174f8 100644 --- a/lib/app_sources/izzyondroid.dart +++ b/lib/app_sources/izzyondroid.dart @@ -23,14 +23,15 @@ class IzzyOnDroid extends AppSource { @override String? tryInferringAppId(String standardUrl, - {List additionalData = const []}) { + {Map additionalSettings = const {}}) { return FDroid().tryInferringAppId(standardUrl); } @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { String? appId = tryInferringAppId(standardUrl); return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( await get( diff --git a/lib/app_sources/mullvad.dart b/lib/app_sources/mullvad.dart index 353e460..283cf37 100644 --- a/lib/app_sources/mullvad.dart +++ b/lib/app_sources/mullvad.dart @@ -24,8 +24,9 @@ class Mullvad extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { Response res = await get(Uri.parse('$standardUrl/en/download/android')); if (res.statusCode == 200) { var version = parse(res.body) diff --git a/lib/app_sources/signal.dart b/lib/app_sources/signal.dart index 517a7df..90004a0 100644 --- a/lib/app_sources/signal.dart +++ b/lib/app_sources/signal.dart @@ -18,8 +18,9 @@ class Signal extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { Response res = await get(Uri.parse('https://updates.$host/android/latest.json')); if (res.statusCode == 200) { diff --git a/lib/app_sources/sourceforge.dart b/lib/app_sources/sourceforge.dart index 6961ea8..d847dbd 100644 --- a/lib/app_sources/sourceforge.dart +++ b/lib/app_sources/sourceforge.dart @@ -23,8 +23,9 @@ class SourceForge extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { Response res = await get(Uri.parse('$standardUrl/rss?path=/')); if (res.statusCode == 200) { var parsedHtml = parse(res.body); diff --git a/lib/app_sources/steammobile.dart b/lib/app_sources/steammobile.dart index f202bd8..8fe8ece 100644 --- a/lib/app_sources/steammobile.dart +++ b/lib/app_sources/steammobile.dart @@ -9,13 +9,10 @@ class SteamMobile extends AppSource { SteamMobile() { host = 'store.steampowered.com'; name = tr('steam'); - additionalSourceAppSpecificFormItems = [ + additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormItem( - label: tr('app'), - key: 'app', - required: true, - opts: apks.entries.toList()) + GeneratedFormItem('app', + label: tr('app'), required: true, opts: apks.entries.toList()) ] ]; } @@ -32,15 +29,12 @@ class SteamMobile extends AppSource { @override Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) async { + String standardUrl, + Map additionalSettings, + ) async { Response res = await get(Uri.parse('https://$host/mobile')); if (res.statusCode == 200) { - var apkNamePrefix = findGeneratedFormValueByKey( - additionalSourceAppSpecificFormItems - .reduce((value, element) => [...value, ...element]), - additionalData, - 'app'); + var apkNamePrefix = additionalSettings['app']; if (apkNamePrefix == null) { throw NoReleasesError(); } diff --git a/lib/components/generated_form.dart b/lib/components/generated_form.dart index c0ae67f..d927371 100644 --- a/lib/components/generated_form.dart +++ b/lib/components/generated_form.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; enum FormItemType { string, bool } typedef OnValueChanges = void Function( - List values, bool valid, bool isBuilding); + Map values, bool valid, bool isBuilding); class GeneratedFormItem { late String key; @@ -13,22 +13,21 @@ class GeneratedFormItem { late bool required; late int max; late List additionalValidators; - late String id; late List belowWidgets; late String? hint; late List>? opts; + late String? defaultValue; - GeneratedFormItem( + GeneratedFormItem(this.key, {this.label = 'Input', this.type = FormItemType.string, this.required = true, this.max = 1, this.additionalValidators = const [], - this.id = 'input', this.belowWidgets = const [], this.hint, this.opts, - this.key = 'default'}) { + this.defaultValue}) { if (type != FormItemType.string) { required = false; } @@ -37,14 +36,10 @@ class GeneratedFormItem { class GeneratedForm extends StatefulWidget { const GeneratedForm( - {super.key, - required this.items, - required this.onValueChanges, - required this.defaultValues}); + {super.key, required this.items, required this.onValueChanges}); final List> items; final OnValueChanges onValueChanges; - final List defaultValues; @override State createState() => _GeneratedFormState(); @@ -52,17 +47,18 @@ class GeneratedForm extends StatefulWidget { class _GeneratedFormState extends State { final _formKey = GlobalKey(); - late List> values; + Map values = {}; late List> formInputs; List> rows = []; // If any value changes, call this to update the parent with value and validity void someValueChanged({bool isBuilding = false}) { - List returnValues = []; + Map returnValues = {}; var valid = true; - for (int r = 0; r < values.length; r++) { - for (int i = 0; i < values[r].length; i++) { - returnValues.add(values[r][i]); + for (int r = 0; r < widget.items.length; r++) { + for (int i = 0; i < widget.items[r].length; i++) { + returnValues[widget.items[r][i].key] = + values[widget.items[r][i].key] ?? ''; if (formInputs[r][i] is TextFormField) { valid = valid && ((formInputs[r][i].key as GlobalKey) @@ -80,16 +76,13 @@ class _GeneratedFormState extends State { super.initState(); // Initialize form values as all empty + values.clear(); int j = 0; - values = widget.items - .map((row) => row.map((e) { - return j < widget.defaultValues.length - ? widget.defaultValues[j++] - : e.opts != null - ? e.opts!.first.key - : ''; - }).toList()) - .toList(); + for (var row in widget.items) { + for (var e in row) { + values[e.key] = e.defaultValue ?? e.opts?.first.key ?? ''; + } + } // Dynamically create form inputs formInputs = widget.items.asMap().entries.map((row) { @@ -98,11 +91,11 @@ class _GeneratedFormState extends State { final formFieldKey = GlobalKey(); return TextFormField( key: formFieldKey, - initialValue: values[row.key][e.key], + initialValue: values[e.value.key], autovalidateMode: AutovalidateMode.onUserInteraction, onChanged: (value) { setState(() { - values[row.key][e.key] = value; + values[e.value.key] = value; someValueChanged(); }); }, @@ -131,14 +124,14 @@ class _GeneratedFormState extends State { } return DropdownButtonFormField( decoration: InputDecoration(labelText: e.value.label), - value: values[row.key][e.key], + value: values[e.value.key], items: e.value.opts! .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) .toList(), onChanged: (value) { setState(() { - values[row.key][e.key] = value ?? e.value.opts!.first.key; + values[e.value.key] = value ?? e.value.opts!.first.key; someValueChanged(); }); }); @@ -160,10 +153,10 @@ class _GeneratedFormState extends State { children: [ Text(widget.items[r][e].label), Switch( - value: values[r][e] == 'true', + value: values[widget.items[r][e].key] == 'true', onChanged: (value) { setState(() { - values[r][e] = value ? 'true' : ''; + values[widget.items[r][e].key] = value ? 'true' : ''; someValueChanged(); }); }) @@ -217,18 +210,3 @@ class _GeneratedFormState extends State { )); } } - -String? findGeneratedFormValueByKey( - List items, List values, String key) { - var foundIndex = -1; - for (var i = 0; i < items.length; i++) { - if (items[i].key == key) { - foundIndex = i; - break; - } - } - if (foundIndex >= 0 && foundIndex < values.length) { - return values[foundIndex]; - } - return null; -} diff --git a/lib/components/generated_form_modal.dart b/lib/components/generated_form_modal.dart index 5ea70ff..2a24ae1 100644 --- a/lib/components/generated_form_modal.dart +++ b/lib/components/generated_form_modal.dart @@ -8,14 +8,12 @@ class GeneratedFormModal extends StatefulWidget { {super.key, required this.title, required this.items, - required this.defaultValues, this.initValid = false, this.message = ''}); final String title; final String message; final List> items; - final List defaultValues; final bool initValid; @override @@ -23,13 +21,12 @@ class GeneratedFormModal extends StatefulWidget { } class _GeneratedFormModalState extends State { - List values = []; + Map values = {}; bool valid = false; @override void initState() { super.initState(); - values = widget.defaultValues; valid = widget.initValid || widget.items.isEmpty; } @@ -57,8 +54,7 @@ class _GeneratedFormModalState extends State { this.valid = valid; }); } - }, - defaultValues: widget.defaultValues) + }) ]), actions: [ TextButton( diff --git a/lib/main.dart b/lib/main.dart index 2f614d9..3be599c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -200,9 +200,8 @@ class _ObtainiumState extends State { currentReleaseTag, [], 0, - ['true'], + {}, null, - false, false) ]); } diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 6dd6b5f..4408a3c 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -27,10 +27,8 @@ class _AddAppPageState extends State { String userInput = ''; String searchQuery = ''; AppSource? pickedSource; - List sourceSpecificAdditionalData = []; - bool sourceSpecificDataIsValid = true; - List otherAdditionalData = []; - bool otherAdditionalDataIsValid = true; + Map additionalSettings = {}; + bool additionalSettingsValid = true; @override Widget build(BuildContext context) { @@ -43,10 +41,12 @@ class _AddAppPageState extends State { var source = valid ? sourceProvider.getSource(userInput) : null; if (pickedSource.runtimeType != source.runtimeType) { pickedSource = source; - sourceSpecificAdditionalData = - source != null ? source.additionalSourceAppSpecificDefaults : []; - sourceSpecificDataIsValid = source != null - ? !sourceProvider.ifSourceAppsRequireAdditionalData(source) + additionalSettings = source != null + ? getDefaultValuesFromFormItems( + source.combinedAppSpecificSettingFormItems) + : {}; + additionalSettingsValid = source != null + ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source) : true; } } @@ -66,11 +66,9 @@ class _AddAppPageState extends State { }); var settingsProvider = context.read(); () async { - var userPickedTrackOnly = findGeneratedFormValueByKey( - pickedSource!.additionalAppSpecificSourceAgnosticFormItems, - otherAdditionalData, - 'trackOnlyFormItemKey') == - 'true'; + var userPickedTrackOnly = additionalSettings['trackOnly'] == 'true'; + var userPickedNoVersionDetection = + additionalSettings['noVersionDetection'] == 'true'; var cont = true; if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && await showDialog( @@ -83,7 +81,6 @@ class _AddAppPageState extends State { : tr('app') ]), items: const [], - defaultValues: const [], message: '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}', ); @@ -91,17 +88,32 @@ class _AddAppPageState extends State { null) { cont = false; } + if (userPickedNoVersionDetection && + await showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('disableVersionDetection'), + items: const [], + message: tr('noVersionDetectionExplanation'), + ); + }) == + null) { + cont = false; + } if (cont) { HapticFeedback.selectionClick(); var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; App app = await sourceProvider.getApp( - pickedSource!, userInput, sourceSpecificAdditionalData, - trackOnly: trackOnly); + pickedSource!, userInput, additionalSettings, + trackOnlyOverride: trackOnly, + noVersionDetectionOverride: userPickedNoVersionDetection); if (!trackOnly) { await settingsProvider.getInstallPermission(); } // Only download the APK here if you need to for the package ID - if (sourceProvider.isTempId(app.id) && !app.trackOnly) { + if (sourceProvider.isTempId(app.id) && + app.additionalSettings['trackOnly'] != 'true') { // ignore: use_build_context_synchronously var apkUrl = await appsProvider.confirmApkUrl(app, context); if (apkUrl == null) { @@ -116,7 +128,7 @@ class _AddAppPageState extends State { if (appsProvider.apps.containsKey(app.id)) { throw ObtainiumError(tr('appAlreadyAdded')); } - if (app.trackOnly) { + if (app.additionalSettings['trackOnly'] == 'true') { app.installedVersion = app.latestVersion; } await appsProvider.saveApps([app]); @@ -156,34 +168,33 @@ class _AddAppPageState extends State { Expanded( child: GeneratedForm( items: [ - [ - GeneratedFormItem( - label: tr('appSourceURL'), - additionalValidators: [ - (value) { - try { - sourceProvider - .getSource(value ?? '') - .standardizeURL( - preStandardizeUrl( - value ?? '')); - } catch (e) { - return e is String - ? e - : e is ObtainiumError - ? e.toString() - : tr('error'); - } - return null; - } - ]) - ] - ], + [ + GeneratedFormItem('appSourceURL', + label: tr('appSourceURL'), + additionalValidators: [ + (value) { + try { + sourceProvider + .getSource(value ?? '') + .standardizeURL( + preStandardizeUrl( + value ?? '')); + } catch (e) { + return e is String + ? e + : e is ObtainiumError + ? e.toString() + : tr('error'); + } + return null; + } + ]) + ] + ], onValueChanges: (values, valid, isBuilding) { - changeUserInput( - values[0], valid, isBuilding); - }, - defaultValues: const [])), + changeUserInput(values['appSourceURL']!, + valid, isBuilding); + })), const SizedBox( width: 16, ), @@ -193,13 +204,9 @@ class _AddAppPageState extends State { onPressed: gettingAppInfo || pickedSource == null || (pickedSource! - .additionalSourceAppSpecificFormItems + .combinedAppSpecificSettingFormItems .isNotEmpty && - !sourceSpecificDataIsValid) || - (pickedSource! - .additionalAppSpecificSourceAgnosticDefaults - .isNotEmpty && - !otherAdditionalDataIsValid) + !additionalSettingsValid) ? null : addApp, child: Text(tr('add'))) @@ -224,7 +231,7 @@ class _AddAppPageState extends State { child: GeneratedForm( items: [ [ - GeneratedFormItem( + GeneratedFormItem('searchSomeSources', label: tr('searchSomeSourcesLabel'), required: false), ] @@ -232,11 +239,11 @@ class _AddAppPageState extends State { onValueChanges: (values, valid, isBuilding) { if (values.isNotEmpty && valid) { setState(() { - searchQuery = values[0].trim(); + searchQuery = + values['searchSomeSources']!.trim(); }); } - }, - defaultValues: const ['']), + }), ), const SizedBox( width: 16, @@ -292,15 +299,8 @@ class _AddAppPageState extends State { ], ), if (pickedSource != null && - (pickedSource!.additionalSourceAppSpecificDefaults - .isNotEmpty || - pickedSource! - .additionalAppSpecificSourceAgnosticFormItems - .where((e) => pickedSource!.enforceTrackOnly - ? e.key != 'trackOnlyFormItemKey' - : true) - .map((e) => [e]) - .isNotEmpty)) + (pickedSource! + .combinedAppSpecificSettingFormItems.isNotEmpty)) Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -316,46 +316,17 @@ class _AddAppPageState extends State { const SizedBox( height: 16, ), - if (pickedSource! - .additionalSourceAppSpecificFormItems - .isNotEmpty) - GeneratedForm( - items: pickedSource! - .additionalSourceAppSpecificFormItems, - onValueChanges: (values, valid, isBuilding) { - if (!isBuilding) { - setState(() { - sourceSpecificAdditionalData = values; - sourceSpecificDataIsValid = valid; - }); - } - }, - defaultValues: pickedSource! - .additionalSourceAppSpecificDefaults), - if (pickedSource! - .additionalAppSpecificSourceAgnosticDefaults - .isNotEmpty) - const SizedBox( - height: 8, - ), GeneratedForm( items: pickedSource! - .additionalAppSpecificSourceAgnosticFormItems - .where((e) => pickedSource!.enforceTrackOnly - ? e.key != 'trackOnlyFormItemKey' - : true) - .map((e) => [e]) - .toList(), + .combinedAppSpecificSettingFormItems, onValueChanges: (values, valid, isBuilding) { if (!isBuilding) { setState(() { - otherAdditionalData = values; - otherAdditionalDataIsValid = valid; + additionalSettings = values; + additionalSettingsValid = valid; }); } - }, - defaultValues: pickedSource! - .additionalAppSpecificSourceAgnosticDefaults), + }), ], ) else diff --git a/lib/pages/app.dart b/lib/pages/app.dart index ed8415d..0326a3a 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -40,6 +40,7 @@ class _AppPageState extends State { prevApp = app; getUpdate(app.app.id); } + var trackOnly = app?.app.additionalSettings['trackOnly'] == 'true'; return Scaffold( appBar: settingsProvider.showAppWebpage ? AppBar() : null, backgroundColor: Theme.of(context).colorScheme.surface, @@ -72,7 +73,9 @@ class _AppPageState extends State { height: 25, ), Text( - app?.installedInfo?.name ?? app?.app.name ?? 'App', + app?.installedInfo?.name ?? + app?.app.name ?? + tr('app'), textAlign: TextAlign.center, style: Theme.of(context).textTheme.displayLarge, ), @@ -111,7 +114,7 @@ class _AppPageState extends State { Text( '${tr('installedVersionX', args: [ app?.app.installedVersion ?? tr('none') - ])}${app?.app.trackOnly == true ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ + ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ tr('app') ])}' : ''}', textAlign: TextAlign.center, @@ -151,7 +154,7 @@ class _AppPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (app?.app.installedVersion != null && - app?.app.trackOnly == false && + !trackOnly && app?.app.installedVersion != app?.app.latestVersion) IconButton( onPressed: app?.downloadProgress != null @@ -199,30 +202,48 @@ class _AppPageState extends State { ); }); }, - tooltip: 'Mark as Updated', + tooltip: tr('markUpdated'), icon: const Icon(Icons.done)), if (source != null && - source.additionalSourceAppSpecificFormItems - .isNotEmpty) + source + .combinedAppSpecificSettingFormItems.isNotEmpty) IconButton( onPressed: app?.downloadProgress != null ? null : () { - showDialog>( + showDialog>( context: context, builder: (BuildContext ctx) { + var items = source + .combinedAppSpecificSettingFormItems + .map((row) { + row.map((e) { + if (app?.app.additionalSettings[ + e.key] != + null) { + e.defaultValue = app?.app + .additionalSettings[ + e.key]; + } + return e; + }).toList(); + return row; + }).toList(); return GeneratedFormModal( - title: 'Additional Options', - items: source - .additionalSourceAppSpecificFormItems, - defaultValues: app != null - ? app.app.additionalData - : source - .additionalSourceAppSpecificDefaults); + title: tr('additionalOptions'), + items: items); }).then((values) { if (app != null && values != null) { var changedApp = app.app; - changedApp.additionalData = values; + changedApp.additionalSettings = + values; + if (source.enforceTrackOnly) { + changedApp.additionalSettings[ + 'trackOnly'] = 'true'; + showError( + tr('appsFromSourceAreTrackOnly'), + context); + } appsProvider.saveApps( [changedApp]).then((value) { getUpdate(changedApp.id); @@ -230,7 +251,7 @@ class _AppPageState extends State { } }); }, - tooltip: 'Additional Options', + tooltip: tr('additionalOptions'), icon: const Icon(Icons.settings)), const SizedBox(width: 16.0), Expanded( @@ -242,7 +263,9 @@ class _AppPageState extends State { ? () { HapticFeedback.heavyImpact(); () async { - if (app?.app.trackOnly != true) { + if (app?.app.additionalSettings[ + 'trackOnly'] != + 'true') { await settingsProvider .getInstallPermission(); } @@ -264,12 +287,12 @@ class _AppPageState extends State { } : null, child: Text(app?.app.installedVersion == null - ? app?.app.trackOnly == false - ? 'Install' - : 'Mark Installed' - : app?.app.trackOnly == false - ? 'Update' - : 'Mark Updated'))), + ? !trackOnly + ? tr('install') + : tr('markInstalled') + : !trackOnly + ? tr('update') + : tr('markUpdated')))), const SizedBox(width: 16.0), ElevatedButton( onPressed: app?.downloadProgress != null diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 4c34dd8..71f3cb9 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -139,14 +139,16 @@ class AppsPageState extends State { List trackOnlyUpdateIdsAllOrSelected = []; existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) { - if (appsProvider.apps[id]!.app.trackOnly) { + if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == + 'true') { trackOnlyUpdateIdsAllOrSelected.add(id); return false; } return true; }).toList(); newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) { - if (appsProvider.apps[id]!.app.trackOnly) { + if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == + 'true') { trackOnlyUpdateIdsAllOrSelected.add(id); return false; } @@ -271,7 +273,7 @@ class AppsPageState extends State { SizedBox( width: 100, child: Text( - '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.trackOnly == true ? ' ${tr('estimateInBrackets')}' : ''}', + '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBrackets')}' : ''}', overflow: TextOverflow.fade, textAlign: TextAlign.end, )), @@ -289,7 +291,7 @@ class AppsPageState extends State { child: appsProvider.areDownloadsRunning() ? Text(tr('pleaseWait')) : Text( - '${tr('updateAvailable')}${sortedApps[index].app.trackOnly ? ' ${tr('estimateInBracketsShort')}' : ''}', + '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBracketsShort')}' : ''}', style: TextStyle( fontStyle: FontStyle.italic, decoration: changesUrl == null @@ -343,13 +345,12 @@ class AppsPageState extends State { : IconButton( visualDensity: VisualDensity.compact, onPressed: () { - showDialog?>( + showDialog?>( context: context, builder: (BuildContext ctx) { return GeneratedFormModal( title: tr('removeSelectedAppsQuestion'), items: const [], - defaultValues: const [], initValid: true, message: tr( 'xWillBeRemovedButRemainInstalled', @@ -376,41 +377,42 @@ class AppsPageState extends State { ? null : () { HapticFeedback.heavyImpact(); - List formInputs = []; - List defaultValues = []; + List formItems = []; if (existingUpdateIdsAllOrSelected.isNotEmpty) { - formInputs.add(GeneratedFormItem( + formItems.add(GeneratedFormItem('updates', label: tr('updateX', args: [ plural('apps', existingUpdateIdsAllOrSelected.length) ]), type: FormItemType.bool, - key: 'updates')); - defaultValues.add('true'); + defaultValue: 'true')); } if (newInstallIdsAllOrSelected.isNotEmpty) { - formInputs.add(GeneratedFormItem( + formItems.add(GeneratedFormItem('installs', label: tr('installX', args: [ plural('apps', newInstallIdsAllOrSelected.length) ]), type: FormItemType.bool, - key: 'installs')); - defaultValues - .add(defaultValues.isEmpty ? 'true' : ''); + defaultValue: + existingUpdateIdsAllOrSelected.isNotEmpty + ? 'true' + : '')); } if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { - formInputs.add(GeneratedFormItem( + formItems.add(GeneratedFormItem('trackonlies', label: tr('markXTrackOnlyAsUpdated', args: [ plural('apps', trackOnlyUpdateIdsAllOrSelected.length) ]), type: FormItemType.bool, - key: 'trackonlies')); - defaultValues - .add(defaultValues.isEmpty ? 'true' : ''); + defaultValue: existingUpdateIdsAllOrSelected + .isNotEmpty || + newInstallIdsAllOrSelected.isNotEmpty + ? 'true' + : '')); } - showDialog?>( + showDialog?>( context: context, builder: (BuildContext ctx) { var totalApps = existingUpdateIdsAllOrSelected @@ -420,27 +422,21 @@ class AppsPageState extends State { return GeneratedFormModal( title: tr('changeX', args: [plural('apps', totalApps)]), - items: formInputs.map((e) => [e]).toList(), - defaultValues: defaultValues, + items: formItems.map((e) => [e]).toList(), initValid: true, ); }).then((values) { if (values != null) { if (values.isEmpty) { - values = defaultValues; + values = getDefaultValuesFromFormItems( + [formItems]); } bool shouldInstallUpdates = - findGeneratedFormValueByKey( - formInputs, values, 'updates') == - 'true'; + values['updates'] == 'true'; bool shouldInstallNew = - findGeneratedFormValueByKey( - formInputs, values, 'installs') == - 'true'; + values['installs'] == 'true'; bool shouldMarkTrackOnlies = - findGeneratedFormValueByKey(formInputs, - values, 'trackonlies') == - 'true'; + values['trackonlies'] == 'true'; (() async { if (shouldInstallNew || shouldInstallUpdates) { @@ -613,7 +609,6 @@ class AppsPageState extends State { title: tr( 'resetInstallStatusForSelectedAppsQuestion'), items: const [], - defaultValues: const [], initValid: true, message: tr( 'installStatusOfXWillBeResetExplanation', @@ -683,36 +678,42 @@ class AppsPageState extends State { : FontWeight.bold), ), onPressed: () { - showDialog?>( + showDialog?>( context: context, builder: (BuildContext ctx) { + var vals = filter == null + ? AppsFilter().toValuesMap() + : filter!.toValuesMap(); return GeneratedFormModal( title: tr('filterApps'), items: [ [ - GeneratedFormItem( - label: tr('appName'), required: false), - GeneratedFormItem( - label: tr('author'), required: false) + GeneratedFormItem('appName', + label: tr('appName'), + required: false, + defaultValue: vals['appName']), + GeneratedFormItem('author', + label: tr('author'), + required: false, + defaultValue: vals['author']) ], [ - GeneratedFormItem( + GeneratedFormItem('upToDateApps', label: tr('upToDateApps'), - type: FormItemType.bool) + type: FormItemType.bool, + defaultValue: vals['upToDateApps']) ], [ - GeneratedFormItem( + GeneratedFormItem('nonInstalledApps', label: tr('nonInstalledApps'), - type: FormItemType.bool) + type: FormItemType.bool, + defaultValue: vals['nonInstalledApps']) ] - ], - defaultValues: filter == null - ? AppsFilter().toValuesArray() - : filter!.toValuesArray()); + ]); }).then((values) { if (values != null) { setState(() { - filter = AppsFilter.fromValuesArray(values); + filter = AppsFilter.fromValuesMap(values); if (AppsFilter().isIdenticalTo(filter!)) { filter = null; } @@ -740,20 +741,20 @@ class AppsFilter { this.includeUptodate = true, this.includeNonInstalled = true}); - List toValuesArray() { - return [ - nameFilter, - authorFilter, - includeUptodate ? 'true' : '', - includeNonInstalled ? 'true' : '' - ]; + Map toValuesMap() { + return { + 'appName': nameFilter, + 'author': authorFilter, + 'upToDateApps': includeUptodate ? 'true' : '', + 'nonInstalledApps': includeNonInstalled ? 'true' : '' + }; } - AppsFilter.fromValuesArray(List values) { - nameFilter = values[0]; - authorFilter = values[1]; - includeUptodate = values[2] == 'true'; - includeNonInstalled = values[3] == 'true'; + AppsFilter.fromValuesMap(Map values) { + nameFilter = values['appName']!; + authorFilter = values['author']!; + includeUptodate = values['upToDateApps'] == 'true'; + includeNonInstalled = values['nonInstalledApps'] == 'true'; } bool isIdenticalTo(AppsFilter other) => diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 889969b..db69b7f 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -145,7 +145,7 @@ class _ImportExportPageState extends State { title: tr('importFromURLList'), items: [ [ - GeneratedFormItem( + GeneratedFormItem('appURLList', label: tr('appURLList'), max: 7, additionalValidators: [ @@ -172,7 +172,6 @@ class _ImportExportPageState extends State { ]) ] ], - defaultValues: const [], ); }).then((values) { if (values != null) { @@ -237,11 +236,11 @@ class _ImportExportPageState extends State { items: [ [ GeneratedFormItem( + 'searchQuery', label: tr( 'searchQuery')) ] ], - defaultValues: const [], ); }); if (values != null && @@ -346,10 +345,10 @@ class _ImportExportPageState extends State { .requiredArgs .map( (e) => [ - GeneratedFormItem(label: e) + GeneratedFormItem(e, + label: e) ]) .toList(), - defaultValues: const [], ); }); if (values != null) { diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 8f8befb..279f6f9 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -143,16 +143,11 @@ class _SettingsPageState extends State { .toList(), onValueChanges: (values, valid, isBuilding) { if (valid) { - for (var i = 0; i < values.length; i++) { - settingsProvider.setSettingString( - e.additionalSourceSpecificSettingFormItems[i].id, - values[i]); - } + values.forEach((key, value) { + settingsProvider.setSettingString(key, value); + }); } - }, - defaultValues: e.additionalSourceSpecificSettingFormItems.map((e) { - return settingsProvider.getSettingString(e.id) ?? ''; - }).toList()); + }); } else { return Container(); } diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index ec605e2..570a3dd 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -313,7 +313,8 @@ class AppsProvider with ChangeNotifier { throw ObtainiumError(tr('appNotFound')); } String? apkUrl; - if (!apps[id]!.app.trackOnly) { + var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == 'true'; + if (!trackOnly) { apkUrl = await confirmApkUrl(apps[id]!.app, context); } if (apkUrl != null) { @@ -326,7 +327,7 @@ class AppsProvider with ChangeNotifier { appsToInstall.add(id); } } - if (apps[id]!.app.trackOnly) { + if (trackOnly) { trackOnlyAppsToUpdate.add(id); } } @@ -451,9 +452,10 @@ class AppsProvider with ChangeNotifier { // Don't save changes, just return the object if changes were made (else null) App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { var modded = false; - if (installedInfo == null && - app.installedVersion != null && - !app.trackOnly) { + var trackOnly = app.additionalSettings['trackOnly'] == 'true'; + var noVersionDetection = + app.additionalSettings['noVersionDetection'] == 'true'; + if (installedInfo == null && app.installedVersion != null && !trackOnly) { app.installedVersion = null; modded = true; } else if (installedInfo?.versionName != null && @@ -461,7 +463,8 @@ class AppsProvider with ChangeNotifier { app.installedVersion = installedInfo!.versionName; modded = true; } else if (installedInfo?.versionName != null && - installedInfo!.versionName != app.installedVersion) { + installedInfo!.versionName != app.installedVersion && + !noVersionDetection) { String? correctedInstalledVersion = reconcileRealAndInternalVersions( installedInfo.versionName!, app.installedVersion!); if (correctedInstalledVersion != null) { @@ -470,7 +473,8 @@ class AppsProvider with ChangeNotifier { } } if (app.installedVersion != null && - app.installedVersion != app.latestVersion) { + app.installedVersion != app.latestVersion && + !noVersionDetection) { app.installedVersion = reconcileRealAndInternalVersions( app.installedVersion!, app.latestVersion, matchMode: true) ?? @@ -623,12 +627,8 @@ class AppsProvider with ChangeNotifier { App newApp = await sourceProvider.getApp( sourceProvider.getSource(currentApp.url), currentApp.url, - currentApp.additionalData, - name: currentApp.name, - id: currentApp.id, - pinned: currentApp.pinned, - trackOnly: currentApp.trackOnly, - installedVersion: currentApp.installedVersion); + currentApp.additionalSettings, + currentApp: currentApp); if (currentApp.preferredApkIndex < newApp.apkUrls.length) { newApp.preferredApkIndex = currentApp.preferredApkIndex; } diff --git a/lib/providers/notifications_provider.dart b/lib/providers/notifications_provider.dart index 9e62c5a..b68a669 100644 --- a/lib/providers/notifications_provider.dart +++ b/lib/providers/notifications_provider.dart @@ -80,11 +80,11 @@ class DownloadNotification extends ObtainiumNotification { DownloadNotification(String appName, int progPercent) : super( appName.hashCode, - 'Downloading $appName', + tr('downloadingX', args: [appName]), '', 'APP_DOWNLOADING', - 'Downloading App', - 'Notifies the user of the progress in downloading an App', + tr('downloadingX', args: [tr('app')]), + tr('downloadNotifDescription'), Importance.low, onlyAlertOnce: true, progPercent: progPercent); diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index a623477..5966456 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -44,10 +44,9 @@ class App { late String latestVersion; List apkUrls = []; late int preferredApkIndex; - late List additionalData; + late Map additionalSettings; late DateTime? lastUpdateCheck; bool pinned = false; - bool trackOnly = false; App( this.id, this.url, @@ -57,39 +56,59 @@ class App { this.latestVersion, this.apkUrls, this.preferredApkIndex, - this.additionalData, + this.additionalSettings, this.lastUpdateCheck, - this.pinned, - this.trackOnly); + this.pinned); @override String toString() { - return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned'; + return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned'; } - factory App.fromJson(Map json) => App( - json['id'] as String, - json['url'] as String, - json['author'] as String, - json['name'] as String, - json['installedVersion'] == null - ? null - : json['installedVersion'] as String, - json['latestVersion'] as String, - json['apkUrls'] == null - ? [] - : List.from(jsonDecode(json['apkUrls'])), - json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int, - json['additionalData'] == null - ? SourceProvider() - .getSource(json['url']) - .additionalSourceAppSpecificDefaults - : List.from(jsonDecode(json['additionalData'])), - json['lastUpdateCheck'] == null - ? null - : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), - json['pinned'] ?? false, - json['trackOnly'] ?? false); + factory App.fromJson(Map json) { + var source = SourceProvider().getSource(json['url']); + var formItems = source.combinedAppSpecificSettingFormItems + .reduce((value, element) => [...value, ...element]); + Map additionalSettings = + getDefaultValuesFromFormItems([formItems]); + if (json['additionalSettings'] != null) { + additionalSettings.addEntries( + Map.from(jsonDecode(json['additionalSettings'])) + .entries); + } + // If needed, migrate old-style additionalData to new-style additionalSettings + if (json['additionalData'] != null) { + List temp = List.from(jsonDecode(json['additionalData'])); + temp.asMap().forEach((i, value) { + if (i < formItems.length) { + additionalSettings[formItems[i].key] = value; + } + }); + additionalSettings['trackOnly'] = (json['trackOnly'] ?? false).toString(); + additionalSettings['noVersionDetection'] = + (json['noVersionDetection'] ?? false).toString(); + } + return App( + json['id'] as String, + json['url'] as String, + json['author'] as String, + json['name'] as String, + json['installedVersion'] == null + ? null + : json['installedVersion'] as String, + json['latestVersion'] as String, + json['apkUrls'] == null + ? [] + : List.from(jsonDecode(json['apkUrls'])), + json['preferredApkIndex'] == null + ? 0 + : json['preferredApkIndex'] as int, + additionalSettings, + json['lastUpdateCheck'] == null + ? null + : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), + json['pinned'] ?? false); + } Map toJson() => { 'id': id, @@ -100,10 +119,9 @@ class App { 'latestVersion': latestVersion, 'apkUrls': jsonEncode(apkUrls), 'preferredApkIndex': preferredApkIndex, - 'additionalData': jsonEncode(additionalData), + 'additionalSettings': jsonEncode(additionalSettings), 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, - 'pinned': pinned, - 'trackOnly': trackOnly + 'pinned': pinned }; } @@ -124,7 +142,7 @@ preStandardizeUrl(String url) { return url; } -const String noAPKFound = 'No APK found'; +String noAPKFound = tr('noAPKFound'); List getLinksFromParsedHTML( Document dom, RegExp hrefPattern, String prependToLinks) => @@ -137,6 +155,13 @@ List getLinksFromParsedHTML( .map((e) => '$prependToLinks${e.attributes['href']!}') .toList(); +Map getDefaultValuesFromFormItems( + List> items) { + return Map.fromEntries(items + .map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? ''))) + .reduce((value, element) => [...value, ...element])); +} + class AppSource { String? host; late String name; @@ -151,23 +176,37 @@ class AppSource { } Future getLatestAPKDetails( - String standardUrl, List additionalData, - {bool trackOnly = false}) { + String standardUrl, Map additionalSettings) { throw NotImplementedError(); } // Different Sources may need different kinds of additional data for Apps - List> additionalSourceAppSpecificFormItems = []; - List additionalSourceAppSpecificDefaults = []; + List> additionalSourceAppSpecificSettingFormItems = + []; // Some additional data may be needed for Apps regardless of Source - final List additionalAppSpecificSourceAgnosticFormItems = [ - GeneratedFormItem( + final List> + additionalAppSpecificSourceAgnosticSettingFormItems = [ + [ + GeneratedFormItem( + 'trackOnly', label: tr('trackOnly'), type: FormItemType.bool, - key: 'trackOnlyFormItemKey') + ) + ], + [ + GeneratedFormItem('noVersionDetection', + label: tr('noVersionDetection'), type: FormItemType.bool) + ] ]; - final List additionalAppSpecificSourceAgnosticDefaults = ['']; + + // Previous 2 variables combined into one at runtime for convenient usage + List> get combinedAppSpecificSettingFormItems { + return [ + ...additionalSourceAppSpecificSettingFormItems, + ...additionalAppSpecificSourceAgnosticSettingFormItems + ]; + } // Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider List additionalSourceSpecificSettingFormItems = []; @@ -186,7 +225,7 @@ class AppSource { } String? tryInferringAppId(String standardUrl, - {List additionalData = const []}) { + {Map additionalSettings = const {}}) { return null; } } @@ -246,8 +285,8 @@ class SourceProvider { return source; } - bool ifSourceAppsRequireAdditionalData(AppSource source) { - for (var row in source.additionalSourceAppSpecificFormItems) { + bool ifRequiredAppSpecificSettingsExist(AppSource source) { + for (var row in source.combinedAppSpecificSettingFormItems) { for (var element in row) { if (element.required && element.opts == null) { return true; @@ -274,37 +313,44 @@ class SourceProvider { return true; } - Future getApp(AppSource source, String url, List additionalData, - {String name = '', - String? id, - bool pinned = false, - bool trackOnly = false, - String? installedVersion}) async { + Future getApp( + AppSource source, String url, Map additionalSettings, + {App? currentApp, + bool trackOnlyOverride = false, + noVersionDetectionOverride = false}) async { + if (trackOnlyOverride) { + additionalSettings['trackOnly'] = 'true'; + } + if (noVersionDetectionOverride) { + additionalSettings['noVersionDetection'] = 'true'; + } + var trackOnly = currentApp?.additionalSettings['trackOnly'] == 'true'; String standardUrl = source.standardizeURL(preStandardizeUrl(url)); - APKDetails apk = await source - .getLatestAPKDetails(standardUrl, additionalData, trackOnly: trackOnly); + APKDetails apk = + await source.getLatestAPKDetails(standardUrl, additionalSettings); if (apk.apkUrls.isEmpty && !trackOnly) { throw NoAPKError(); } String apkVersion = apk.version.replaceAll('/', '-'); + var name = currentApp?.name.trim() ?? + apk.names.name[0].toUpperCase() + apk.names.name.substring(1); return App( - id ?? + currentApp?.id ?? source.tryInferringAppId(standardUrl, - additionalData: additionalData) ?? + additionalSettings: additionalSettings) ?? generateTempID(apk.names, source), standardUrl, apk.names.author[0].toUpperCase() + apk.names.author.substring(1), name.trim().isNotEmpty ? name : apk.names.name[0].toUpperCase() + apk.names.name.substring(1), - installedVersion, + currentApp?.installedVersion, apkVersion, apk.apkUrls, apk.apkUrls.length - 1, - additionalData, + additionalSettings, DateTime.now(), - pinned, - trackOnly); + currentApp?.pinned ?? false); } // Returns errors in [results, errors] instead of throwing them @@ -316,7 +362,10 @@ class SourceProvider { try { var source = getSource(url); apps.add(await getApp( - source, url, source.additionalSourceAppSpecificDefaults)); + source, + url, + getDefaultValuesFromFormItems( + source.combinedAppSpecificSettingFormItems))); } catch (e) { errors.addAll({url: e}); }