From 392554123b38e7fda5eacbba7e5a59519e284841 Mon Sep 17 00:00:00 2001 From: Imran Remtulla <30463115+ImranR98@users.noreply.github.com> Date: Sat, 29 Apr 2023 23:50:12 -0400 Subject: [PATCH] Added an (experimental) Source override option for URLs that work with an existing Source but use a custom host (#271, #393) (#502) --- README.md | 1 - assets/translations/de.json | 2 +- assets/translations/en.json | 3 +- assets/translations/fa.json | 2 +- assets/translations/fr.json | 2 +- assets/translations/hu.json | 2 +- assets/translations/it.json | 2 +- assets/translations/ja.json | 2 +- assets/translations/zh.json | 2 +- lib/app_sources/apkmirror.dart | 2 +- lib/app_sources/codeberg.dart | 2 +- lib/app_sources/fdroid.dart | 7 +- lib/app_sources/fdroidrepo.dart | 11 -- lib/app_sources/github.dart | 2 +- lib/app_sources/gitlab.dart | 2 +- lib/app_sources/html.dart | 2 +- lib/app_sources/izzyondroid.dart | 2 +- lib/app_sources/mullvad.dart | 2 +- lib/app_sources/neutroncode.dart | 2 +- lib/app_sources/signal.dart | 2 +- lib/app_sources/sourceforge.dart | 2 +- lib/app_sources/steammobile.dart | 2 +- lib/app_sources/telegramapp.dart | 2 +- lib/app_sources/vlc.dart | 2 +- lib/app_sources/whatsapp.dart | 2 +- lib/main.dart | 2 +- lib/pages/add_app.dart | 69 +++++++- lib/pages/app.dart | 5 +- lib/pages/apps.dart | 11 +- lib/providers/apps_provider.dart | 7 +- lib/providers/source_provider.dart | 268 ++++++++++++++++++----------- pubspec.yaml | 2 +- 32 files changed, 270 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 107e70f..0b30e18 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ Currently supported App sources: - [SourceForge](https://sourceforge.net/) - [APKMirror](https://apkmirror.com/) (Track-Only) - Third Party F-Droid Repos - - Any URLs ending with `/fdroid/`, where `` can be anything - most often `repo` - [Steam](https://store.steampowered.com/mobile) - [Telegram App](https://telegram.org) - [VLC](https://www.videolan.org/vlc/download-android.html) diff --git a/assets/translations/de.json b/assets/translations/de.json index 8d7eee6..c89f6db 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}", "remove": "Entfernen", "yesMarkUpdated": "Ja, als aktualisiert markieren", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "App ID oder Name", "appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden", "reposHaveMultipleApps": "Repos können mehrere Apps enthalten", diff --git a/assets/translations/en.json b/assets/translations/en.json index a9b074d..8654d1d 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "Last Update Check: {}", "remove": "Remove", "yesMarkUpdated": "Yes, Mark as Updated", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "App ID or Name", "appWithIdOrNameNotFound": "No App was found with that ID or Name", "reposHaveMultipleApps": "Repos may contain multiple Apps", @@ -224,6 +224,7 @@ "standardVersionDetection": "Standard version detection", "groupByCategory": "Group by Category", "autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible", + "overrideSource": "Override Source", "removeAppQuestion": { "one": "Remove App?", "other": "Remove Apps?" diff --git a/assets/translations/fa.json b/assets/translations/fa.json index dbf4123..6aeee69 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "بررسی آخرین به‌روزرسانی: {}", "remove": "حذف", "yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "شناسه یا نام برنامه", "appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد", "reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 4e7a5ea..95a73ee 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "Vérification de la dernière mise à jour : {}", "remove": "Retirer", "yesMarkUpdated": "Oui, marquer comme mis à jour", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "ID ou nom de l'application", "appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom", "reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications", diff --git a/assets/translations/hu.json b/assets/translations/hu.json index e10d709..30b3418 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "Frissítés ellenőrizve: {}", "remove": "Eltávolítás", "yesMarkUpdated": "Igen, megjelölés frissítettként", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "App ID vagy név", "appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel", "reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak", diff --git a/assets/translations/it.json b/assets/translations/it.json index b3ef7ec..e212184 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}", "remove": "Rimuovi", "yesMarkUpdated": "Sì, contrassegna come aggiornato", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "ID o nome dell'App", "appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome", "reposHaveMultipleApps": "I repository possono contenere più App", diff --git a/assets/translations/ja.json b/assets/translations/ja.json index ea5f56e..2de9a3c 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "最終アップデート確認: {}", "remove": "削除", "yesMarkUpdated": "はい、アップデート済みとしてマークします", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "アプリのIDまたは名前", "appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした", "reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index b7a1780..4a555a9 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -179,7 +179,7 @@ "lastUpdateCheckX": "上次更新检查:{}", "remove": "删除", "yesMarkUpdated": "是的,标记为已更新", - "fdroid": "F-Droid", + "fdroid": "F-Droid Official", "appIdOrName": "应用 ID 或名称", "appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用", "reposHaveMultipleApps": "存储库中可能包含多个应用", diff --git a/lib/app_sources/apkmirror.dart b/lib/app_sources/apkmirror.dart index 6905023..396e599 100644 --- a/lib/app_sources/apkmirror.dart +++ b/lib/app_sources/apkmirror.dart @@ -31,7 +31,7 @@ class APKMirror extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/codeberg.dart b/lib/app_sources/codeberg.dart index dc71fd6..382c7f9 100644 --- a/lib/app_sources/codeberg.dart +++ b/lib/app_sources/codeberg.dart @@ -39,7 +39,7 @@ class Codeberg extends AppSource { var gh = GitHub(); @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index 3d87419..c9c84e6 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -12,16 +12,15 @@ class FDroid extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegExB = - RegExp('^https?://(cloudflare\\.)?$host/+[^/]+/+packages/+[^/]+'); + RegExp('^https?://$host/+[^/]+/+packages/+[^/]+'); RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); if (match != null) { url = 'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}'; } - RegExp standardUrlRegExA = - RegExp('^https?://(cloudflare\\.)?$host/+packages/+[^/]+'); + RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+'); match = standardUrlRegExA.firstMatch(url.toLowerCase()); if (match == null) { throw InvalidURLError(name); diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index 8523257..33c8edf 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -19,17 +19,6 @@ class FDroidRepo extends AppSource { ]; } - @override - String standardizeURL(String url) { - RegExp standardUrlRegExp = - RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)'); - RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase()); - if (match == null) { - throw InvalidURLError(name); - } - return url.substring(0, match.end); - } - @override Future getLatestAPKDetails( String standardUrl, diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index 5b6f2cf..55e1d8b 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -75,7 +75,7 @@ class GitHub extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 5b8db8c..150cb04 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -19,7 +19,7 @@ class GitLab extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index 8067b26..ece1796 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -6,7 +6,7 @@ import 'package:obtainium/providers/source_provider.dart'; class HTML extends AppSource { @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { return url; } diff --git a/lib/app_sources/izzyondroid.dart b/lib/app_sources/izzyondroid.dart index a3f388c..0fbceb9 100644 --- a/lib/app_sources/izzyondroid.dart +++ b/lib/app_sources/izzyondroid.dart @@ -9,7 +9,7 @@ class IzzyOnDroid extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/mullvad.dart b/lib/app_sources/mullvad.dart index 3f43def..64ef092 100644 --- a/lib/app_sources/mullvad.dart +++ b/lib/app_sources/mullvad.dart @@ -10,7 +10,7 @@ class Mullvad extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/neutroncode.dart b/lib/app_sources/neutroncode.dart index 881cc31..45374a9 100644 --- a/lib/app_sources/neutroncode.dart +++ b/lib/app_sources/neutroncode.dart @@ -9,7 +9,7 @@ class NeutronCode extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/signal.dart b/lib/app_sources/signal.dart index 4258d0c..40d6953 100644 --- a/lib/app_sources/signal.dart +++ b/lib/app_sources/signal.dart @@ -9,7 +9,7 @@ class Signal extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { return 'https://$host'; } diff --git a/lib/app_sources/sourceforge.dart b/lib/app_sources/sourceforge.dart index e7b9ec5..2d976ea 100644 --- a/lib/app_sources/sourceforge.dart +++ b/lib/app_sources/sourceforge.dart @@ -9,7 +9,7 @@ class SourceForge extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { diff --git a/lib/app_sources/steammobile.dart b/lib/app_sources/steammobile.dart index 5478a8f..305c89a 100644 --- a/lib/app_sources/steammobile.dart +++ b/lib/app_sources/steammobile.dart @@ -20,7 +20,7 @@ class SteamMobile extends AppSource { final apks = {'steam': tr('steamMobile'), 'steam-chat-app': tr('steamChat')}; @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { return 'https://$host'; } diff --git a/lib/app_sources/telegramapp.dart b/lib/app_sources/telegramapp.dart index dd59e50..3c02156 100644 --- a/lib/app_sources/telegramapp.dart +++ b/lib/app_sources/telegramapp.dart @@ -11,7 +11,7 @@ class TelegramApp extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { return 'https://$host'; } diff --git a/lib/app_sources/vlc.dart b/lib/app_sources/vlc.dart index 56adf8d..b105068 100644 --- a/lib/app_sources/vlc.dart +++ b/lib/app_sources/vlc.dart @@ -10,7 +10,7 @@ class VLC extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { return 'https://$host'; } diff --git a/lib/app_sources/whatsapp.dart b/lib/app_sources/whatsapp.dart index 9512158..85deb84 100644 --- a/lib/app_sources/whatsapp.dart +++ b/lib/app_sources/whatsapp.dart @@ -9,7 +9,7 @@ class WhatsApp extends AppSource { } @override - String standardizeURL(String url) { + String sourceSpecificStandardizeURL(String url) { return 'https://$host'; } diff --git a/lib/main.dart b/lib/main.dart index 0939e5e..e17f836 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.11.36'; +const String currentVersion = '0.12.0'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 27d37c2..625e5dd 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form_modal.dart'; @@ -28,6 +29,7 @@ class _AddAppPageState extends State { String userInput = ''; String searchQuery = ''; + String? pickedSourceOverride; AppSource? pickedSource; Map additionalSettings = {}; bool additionalSettingsValid = true; @@ -49,8 +51,13 @@ class _AddAppPageState extends State { if (isSearch) { searchnum++; } - var source = valid ? sourceProvider.getSource(userInput) : null; - if (pickedSource.runtimeType != source.runtimeType) { + var prevHost = pickedSource?.host; + 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( @@ -115,7 +122,8 @@ class _AddAppPageState extends State { var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; app = await sourceProvider.getApp( pickedSource!, userInput, additionalSettings, - trackOnlyOverride: trackOnly); + trackOnlyOverride: trackOnly, + overrideSource: pickedSourceOverride); if (!trackOnly) { await settingsProvider.getInstallPermission(); } @@ -173,9 +181,9 @@ class _AddAppPageState extends State { (value) { try { sourceProvider - .getSource(value ?? '') - .standardizeURL( - preStandardizeUrl(value ?? '')); + .getSource(value ?? '', + overrideSource: pickedSourceOverride) + .standardizeUrl(value ?? ''); } catch (e) { return e is String ? e @@ -260,6 +268,48 @@ class _AddAppPageState extends State { } } + Widget getHTMLSourceOverrideDropdown() => Column(children: [ + Row( + children: [ + Expanded( + child: GeneratedForm( + items: [ + [ + GeneratedFormDropdown( + 'overrideSource', + defaultValue: HTML().runtimeType.toString(), + [ + ...sourceProvider.sources.map( + (s) => MapEntry(s.runtimeType.toString(), s.name)) + ], + label: tr('overrideSource')) + ] + ], + onValueChanges: (values, valid, isBuilding) { + fn() { + pickedSourceOverride = (values['overrideSource'] == null || + values['overrideSource'] == '') + ? null + : values['overrideSource']; + } + + if (!isBuilding) { + setState(() { + fn(); + }); + } else { + fn(); + } + changeUserInput(userInput, valid, isBuilding); + }, + )) + ], + ), + const SizedBox( + height: 25, + ), + ]); + bool shouldShowSearchBar() => sourceProvider.sources.where((e) => e.canSearch).isNotEmpty && pickedSource == null && @@ -309,6 +359,10 @@ class _AddAppPageState extends State { const SizedBox( height: 16, ), + if (pickedSourceOverride != null || + pickedSource.runtimeType.toString() == + HTML().runtimeType.toString()) + getHTMLSourceOverrideDropdown(), GeneratedForm( key: Key(pickedSource.runtimeType.toString()), items: pickedSource!.combinedAppSpecificSettingFormItems, @@ -379,6 +433,9 @@ class _AddAppPageState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ getUrlInputRow(), + const SizedBox( + height: 16, + ), if (shouldShowSearchBar()) const SizedBox( height: 16, diff --git a/lib/pages/app.dart b/lib/pages/app.dart index f105470..3e4c3aa 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -39,7 +39,10 @@ class _AppPageState extends State { var sourceProvider = SourceProvider(); AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy(); - var source = app != null ? sourceProvider.getSource(app.app.url) : null; + var source = app != null + ? sourceProvider.getSource(app.app.url, + overrideSource: app.app.overrideSource) + : null; if (!areDownloadsRunning && prevApp == null && app != null) { prevApp = app; getUpdate(app.app.id); diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index fd1c5e0..e00dae2 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -111,7 +111,11 @@ class AppsPageState extends State { return false; } if (filter.sourceFilter.isNotEmpty && - sourceProvider.getSource(app.app.url).runtimeType.toString() != + sourceProvider + .getSource(app.app.url, + overrideSource: app.app.overrideSource) + .runtimeType + .toString() != filter.sourceFilter) { return false; } @@ -306,8 +310,9 @@ class AppsPageState extends State { } getChangeLogFn(int appIndex) { - AppSource appSource = - SourceProvider().getSource(listedApps[appIndex].app.url); + AppSource appSource = SourceProvider().getSource( + listedApps[appIndex].app.url, + overrideSource: listedApps[appIndex].app.overrideSource); String? changesUrl = appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url); String? changeLog = listedApps[appIndex].app.changeLog; diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 02dd719..46d57c4 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -172,7 +172,7 @@ class AppsProvider with ChangeNotifier { } try { String downloadUrl = await SourceProvider() - .getSource(app.url) + .getSource(app.url, overrideSource: app.overrideSource) .apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value); var fileName = '${app.id}-${downloadUrl.hashCode}.apk'; var notif = DownloadNotification(app.finalName, 100); @@ -647,7 +647,7 @@ class AppsProvider with ChangeNotifier { for (int i = 0; i < newApps.length; i++) { var info = await getInstalledInfo(newApps[i].id); try { - sp.getSource(newApps[i].url); + sp.getSource(newApps[i].url, overrideSource: newApps[i].overrideSource); apps[newApps[i].id] = AppInMemory(newApps[i], null, info); } catch (e) { errors.add([newApps[i].id, newApps[i].finalName, e.toString()]); @@ -787,7 +787,8 @@ class AppsProvider with ChangeNotifier { App? currentApp = apps[appId]!.app; SourceProvider sourceProvider = SourceProvider(); App newApp = await sourceProvider.getApp( - sourceProvider.getSource(currentApp.url), + sourceProvider.getSource(currentApp.url, + overrideSource: currentApp.overrideSource), currentApp.url, currentApp.additionalSettings, currentApp: currentApp); diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 8670216..367e559 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -44,6 +44,106 @@ class APKDetails { {this.releaseDate, this.changeLog}); } +stringMapListTo2DList(List> mapList) => + mapList.map((e) => [e.key, e.value]).toList(); + +assumed2DlistToStringMapList(List arr) => + arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList(); + +// App JSON schema has changed multiple times over the many versions of Obtainium +// This function takes an App JSON and modifies it if needed to conform to the latest (current) version +appJSONCompatibilityModifiers(Map json) { + var source = SourceProvider() + .getSource(json['url'], overrideSource: json['overrideSource']); + 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 newer-style additionalSettings (V1) + if (json['additionalData'] != null) { + List temp = List.from(jsonDecode(json['additionalData'])); + temp.asMap().forEach((i, value) { + if (i < formItems.length) { + if (formItems[i] is GeneratedFormSwitch) { + additionalSettings[formItems[i].key] = value == 'true'; + } else { + additionalSettings[formItems[i].key] = value; + } + } + }); + additionalSettings['trackOnly'] = + json['trackOnly'] == 'true' || json['trackOnly'] == true; + additionalSettings['noVersionDetection'] = + json['noVersionDetection'] == 'true' || json['trackOnly'] == true; + } + // Convert bool style version detection options to dropdown style + if (additionalSettings['noVersionDetection'] == true) { + additionalSettings['versionDetection'] = 'noVersionDetection'; + if (additionalSettings['releaseDateAsVersion'] == true) { + additionalSettings['versionDetection'] = 'releaseDateAsVersion'; + additionalSettings.remove('releaseDateAsVersion'); + } + if (additionalSettings['noVersionDetection'] != null) { + additionalSettings.remove('noVersionDetection'); + } + if (additionalSettings['releaseDateAsVersion'] != null) { + additionalSettings.remove('releaseDateAsVersion'); + } + } + // Ensure additionalSettings are correctly typed + for (var item in formItems) { + if (additionalSettings[item.key] != null) { + additionalSettings[item.key] = + item.ensureType(additionalSettings[item.key]); + } + } + int preferredApkIndex = + json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int; + if (preferredApkIndex < 0) { + preferredApkIndex = 0; + } + json['preferredApkIndex'] = preferredApkIndex; + // apkUrls can either be old list or new named list apkUrls + List> apkUrls = []; + if (json['apkUrls'] != null) { + var apkUrlJson = jsonDecode(json['apkUrls']); + try { + apkUrls = getApkUrlsFromUrls(List.from(apkUrlJson)); + } catch (e) { + apkUrls = assumed2DlistToStringMapList(List.from(apkUrlJson)); + apkUrls = List.from(apkUrlJson) + .map((e) => MapEntry(e[0] as String, e[1] as String)) + .toList(); + } + json['apkUrls'] = jsonEncode(stringMapListTo2DList(apkUrls)); + } + // Arch based APK filter option should be disabled if it previously did not exist + if (additionalSettings['autoApkFilterByArch'] == null) { + additionalSettings['autoApkFilterByArch'] = false; + } + json['additionalSettings'] = jsonEncode(additionalSettings); + // F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately + // This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid) + // While not causing problems for existing apps from that source that were added in a previous version + var overrideSourceWasUndefined = !json.keys.contains('overrideSource'); + if ((json['url'] as String).startsWith('https://cloudflare.f-droid.org')) { + json['overrideSource'] = FDroid().runtimeType.toString(); + } else if (overrideSourceWasUndefined) { + // Similar to above, but for third-party F-Droid repos + RegExpMatch? match = RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)') + .firstMatch(json['url'] as String); + if (match != null) { + json['overrideSource'] = FDroidRepo().runtimeType.toString(); + } + } + return json; +} + class App { late String id; late String url; @@ -59,6 +159,7 @@ class App { List categories; late DateTime? releaseDate; late String? changeLog; + late String? overrideSource; App( this.id, this.url, @@ -73,7 +174,8 @@ class App { this.pinned, {this.categories = const [], this.releaseDate, - this.changeLog}); + this.changeLog, + this.overrideSource}); @override String toString() { @@ -103,80 +205,11 @@ class App { pinned, categories: categories, changeLog: changeLog, - releaseDate: releaseDate); + releaseDate: releaseDate, + overrideSource: overrideSource); 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 newer-style additionalSettings (V1) - if (json['additionalData'] != null) { - List temp = List.from(jsonDecode(json['additionalData'])); - temp.asMap().forEach((i, value) { - if (i < formItems.length) { - if (formItems[i] is GeneratedFormSwitch) { - additionalSettings[formItems[i].key] = value == 'true'; - } else { - additionalSettings[formItems[i].key] = value; - } - } - }); - additionalSettings['trackOnly'] = - json['trackOnly'] == 'true' || json['trackOnly'] == true; - additionalSettings['noVersionDetection'] = - json['noVersionDetection'] == 'true' || json['trackOnly'] == true; - } - // Convert bool style version detection options to dropdown style - if (additionalSettings['noVersionDetection'] == true) { - additionalSettings['versionDetection'] = 'noVersionDetection'; - if (additionalSettings['releaseDateAsVersion'] == true) { - additionalSettings['versionDetection'] = 'releaseDateAsVersion'; - additionalSettings.remove('releaseDateAsVersion'); - } - if (additionalSettings['noVersionDetection'] != null) { - additionalSettings.remove('noVersionDetection'); - } - if (additionalSettings['releaseDateAsVersion'] != null) { - additionalSettings.remove('releaseDateAsVersion'); - } - } - // Ensure additionalSettings are correctly typed - for (var item in formItems) { - if (additionalSettings[item.key] != null) { - additionalSettings[item.key] = - item.ensureType(additionalSettings[item.key]); - } - } - int preferredApkIndex = json['preferredApkIndex'] == null - ? 0 - : json['preferredApkIndex'] as int; - if (preferredApkIndex < 0) { - preferredApkIndex = 0; - } - // apkUrls can either be old list or new named list apkUrls - List> apkUrls = []; - if (json['apkUrls'] != null) { - var apkUrlJson = jsonDecode(json['apkUrls']); - try { - apkUrls = getApkUrlsFromUrls(List.from(apkUrlJson)); - } catch (e) { - apkUrls = List.from(apkUrlJson) - .map((e) => MapEntry(e[0] as String, e[1] as String)) - .toList(); - } - } - // Arch based APK filter option should be disabled if it previously did not exist - if (json['additionalSettings'] != null && - jsonDecode(json['additionalSettings'])['autoApkFilterByArch'] == null) { - additionalSettings['autoApkFilterByArch'] = false; - } + json = appJSONCompatibilityModifiers(json); return App( json['id'] as String, json['url'] as String, @@ -186,9 +219,9 @@ class App { ? null : json['installedVersion'] as String, json['latestVersion'] as String, - apkUrls, - preferredApkIndex, - additionalSettings, + assumed2DlistToStringMapList(jsonDecode(json['apkUrls'])), + json['preferredApkIndex'] as int, + jsonDecode(json['additionalSettings']) as Map, json['lastUpdateCheck'] == null ? null : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), @@ -204,7 +237,8 @@ class App { ? null : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']), changeLog: - json['changeLog'] == null ? null : json['changeLog'] as String); + json['changeLog'] == null ? null : json['changeLog'] as String, + overrideSource: json['overrideSource']); } Map toJson() => { @@ -214,14 +248,15 @@ class App { 'name': name, 'installedVersion': installedVersion, 'latestVersion': latestVersion, - 'apkUrls': jsonEncode(apkUrls.map((e) => [e.key, e.value]).toList()), + 'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)), 'preferredApkIndex': preferredApkIndex, 'additionalSettings': jsonEncode(additionalSettings), 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'pinned': pinned, 'categories': categories, 'releaseDate': releaseDate?.microsecondsSinceEpoch, - 'changeLog': changeLog + 'changeLog': changeLog, + 'overrideSource': overrideSource }; } @@ -273,8 +308,9 @@ List> getApkUrlsFromUrls(List urls) => return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e); }).toList(); -class AppSource { +abstract class AppSource { String? host; + bool hostChanged = false; late String name; bool enforceTrackOnly = false; bool changeLogIfAnyIsMarkDown = true; @@ -283,7 +319,15 @@ class AppSource { name = runtimeType.toString(); } - String standardizeURL(String url) { + String standardizeUrl(String url) { + url = preStandardizeUrl(url); + if (!hostChanged) { + url = sourceSpecificStandardizeURL(url); + } + return url; + } + + String sourceSpecificStandardizeURL(String url) { throw NotImplementedError(); } @@ -389,33 +433,44 @@ regExValidator(String? value) { class SourceProvider { // Add more source classes here so they are available via the service - List sources = [ - GitHub(), - GitLab(), - Codeberg(), - FDroid(), - IzzyOnDroid(), - FDroidRepo(), - SourceForge(), - APKMirror(), - Mullvad(), - Signal(), - VLC(), - // WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date - TelegramApp(), - SteamMobile(), - NeutronCode(), - HTML() // This should ALWAYS be the last option as they are tried in order - ]; + List get sources => [ + GitHub(), + GitLab(), + Codeberg(), + FDroid(), + IzzyOnDroid(), + FDroidRepo(), + SourceForge(), + APKMirror(), + Mullvad(), + Signal(), + VLC(), + // WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date + TelegramApp(), + SteamMobile(), + NeutronCode(), + HTML() // This should ALWAYS be the last option as they are tried in order + ]; // Add more mass url source classes here so they are available via the service List massUrlSources = [GitHubStars()]; - AppSource getSource(String url) { + AppSource getSource(String url, {String? overrideSource}) { url = preStandardizeUrl(url); + if (overrideSource != null) { + var srcs = + sources.where((e) => e.runtimeType.toString() == overrideSource); + if (srcs.isEmpty) { + throw UnsupportedURLError(); + } + var res = srcs.first; + res.host = Uri.parse(url).host; + res.hostChanged = true; + return srcs.first; + } AppSource? source; for (var s in sources.where((element) => element.host != null)) { - if (RegExp('://(.+\\.)?${s.host}').hasMatch(url)) { + if (RegExp('://${s.host}').hasMatch(url)) { source = s; break; } @@ -423,7 +478,7 @@ class SourceProvider { if (source == null) { for (var s in sources.where((element) => element.host == null)) { try { - s.standardizeURL(url); + s.sourceSpecificStandardizeURL(url); source = s; break; } catch (e) { @@ -459,12 +514,14 @@ class SourceProvider { Future getApp( AppSource source, String url, Map additionalSettings, - {App? currentApp, bool trackOnlyOverride = false}) async { + {App? currentApp, + bool trackOnlyOverride = false, + String? overrideSource}) async { if (trackOnlyOverride || source.enforceTrackOnly) { additionalSettings['trackOnly'] = true; } var trackOnly = additionalSettings['trackOnly'] == true; - String standardUrl = source.standardizeURL(preStandardizeUrl(url)); + String standardUrl = source.standardizeUrl(url); APKDetails apk = await source.getLatestAPKDetails(standardUrl, additionalSettings); if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' && @@ -514,7 +571,8 @@ class SourceProvider { currentApp?.pinned ?? false, categories: currentApp?.categories ?? const [], releaseDate: apk.releaseDate, - changeLog: apk.changeLog); + changeLog: apk.changeLog, + overrideSource: overrideSource ?? currentApp?.overrideSource); } // Returns errors in [results, errors] instead of throwing them diff --git a/pubspec.yaml b/pubspec.yaml index 9fd310d..e62f283 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.11.36+158 # When changing this, update the tag in main() accordingly +version: 0.12.0+159 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0'