mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-28 12:03:45 +01:00 
			
		
		
		
	Added an (experimental) Source override option for URLs that work with an existing Source but use a custom host (#271, #393) (#502)
This commit is contained in:
		| @@ -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/<word>`, where `<word>` 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) | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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?" | ||||
|   | ||||
| @@ -179,7 +179,7 @@ | ||||
|     "lastUpdateCheckX": "بررسی آخرین بهروزرسانی: {}", | ||||
|     "remove": "حذف", | ||||
|     "yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده", | ||||
|     "fdroid": "F-Droid", | ||||
|     "fdroid": "F-Droid Official", | ||||
|     "appIdOrName": "شناسه یا نام برنامه", | ||||
|     "appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد", | ||||
|     "reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -179,7 +179,7 @@ | ||||
|     "lastUpdateCheckX": "最終アップデート確認: {}", | ||||
|     "remove": "削除", | ||||
|     "yesMarkUpdated": "はい、アップデート済みとしてマークします", | ||||
|     "fdroid": "F-Droid", | ||||
|     "fdroid": "F-Droid Official", | ||||
|     "appIdOrName": "アプリのIDまたは名前", | ||||
|     "appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした", | ||||
|     "reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります", | ||||
|   | ||||
| @@ -179,7 +179,7 @@ | ||||
|     "lastUpdateCheckX": "上次更新检查:{}", | ||||
|     "remove": "删除", | ||||
|     "yesMarkUpdated": "是的,标记为已更新", | ||||
|     "fdroid": "F-Droid", | ||||
|     "fdroid": "F-Droid Official", | ||||
|     "appIdOrName": "应用 ID 或名称", | ||||
|     "appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用", | ||||
|     "reposHaveMultipleApps": "存储库中可能包含多个应用", | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ class Signal extends AppSource { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String standardizeURL(String url) { | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     return 'https://$host'; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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'; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class TelegramApp extends AppSource { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String standardizeURL(String url) { | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     return 'https://$host'; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ class VLC extends AppSource { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String standardizeURL(String url) { | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     return 'https://$host'; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ class WhatsApp extends AppSource { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String standardizeURL(String url) { | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     return 'https://$host'; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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<AddAppPage> { | ||||
|  | ||||
|   String userInput = ''; | ||||
|   String searchQuery = ''; | ||||
|   String? pickedSourceOverride; | ||||
|   AppSource? pickedSource; | ||||
|   Map<String, dynamic> additionalSettings = {}; | ||||
|   bool additionalSettingsValid = true; | ||||
| @@ -49,8 +51,13 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|           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<AddAppPage> { | ||||
|           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<AddAppPage> { | ||||
|                               (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<AddAppPage> { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     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<AddAppPage> { | ||||
|             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<AddAppPage> { | ||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                     children: [ | ||||
|                       getUrlInputRow(), | ||||
|                       const SizedBox( | ||||
|                         height: 16, | ||||
|                       ), | ||||
|                       if (shouldShowSearchBar()) | ||||
|                         const SizedBox( | ||||
|                           height: 16, | ||||
|   | ||||
| @@ -39,7 +39,10 @@ class _AppPageState extends State<AppPage> { | ||||
|  | ||||
|     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); | ||||
|   | ||||
| @@ -111,7 +111,11 @@ class AppsPageState extends State<AppsPage> { | ||||
|         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<AppsPage> { | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -44,6 +44,106 @@ class APKDetails { | ||||
|       {this.releaseDate, this.changeLog}); | ||||
| } | ||||
|  | ||||
| stringMapListTo2DList(List<MapEntry<String, String>> mapList) => | ||||
|     mapList.map((e) => [e.key, e.value]).toList(); | ||||
|  | ||||
| assumed2DlistToStringMapList(List<dynamic> 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<String, dynamic> json) { | ||||
|   var source = SourceProvider() | ||||
|       .getSource(json['url'], overrideSource: json['overrideSource']); | ||||
|   var formItems = source.combinedAppSpecificSettingFormItems | ||||
|       .reduce((value, element) => [...value, ...element]); | ||||
|   Map<String, dynamic> additionalSettings = | ||||
|       getDefaultValuesFromFormItems([formItems]); | ||||
|   if (json['additionalSettings'] != null) { | ||||
|     additionalSettings.addEntries( | ||||
|         Map<String, dynamic>.from(jsonDecode(json['additionalSettings'])) | ||||
|             .entries); | ||||
|   } | ||||
|   // If needed, migrate old-style additionalData to newer-style additionalSettings (V1) | ||||
|   if (json['additionalData'] != null) { | ||||
|     List<String> temp = List<String>.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<MapEntry<String, String>> apkUrls = []; | ||||
|   if (json['apkUrls'] != null) { | ||||
|     var apkUrlJson = jsonDecode(json['apkUrls']); | ||||
|     try { | ||||
|       apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson)); | ||||
|     } catch (e) { | ||||
|       apkUrls = assumed2DlistToStringMapList(List<dynamic>.from(apkUrlJson)); | ||||
|       apkUrls = List<dynamic>.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<String> 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<String, dynamic> json) { | ||||
|     var source = SourceProvider().getSource(json['url']); | ||||
|     var formItems = source.combinedAppSpecificSettingFormItems | ||||
|         .reduce((value, element) => [...value, ...element]); | ||||
|     Map<String, dynamic> additionalSettings = | ||||
|         getDefaultValuesFromFormItems([formItems]); | ||||
|     if (json['additionalSettings'] != null) { | ||||
|       additionalSettings.addEntries( | ||||
|           Map<String, dynamic>.from(jsonDecode(json['additionalSettings'])) | ||||
|               .entries); | ||||
|     } | ||||
|     // If needed, migrate old-style additionalData to newer-style additionalSettings (V1) | ||||
|     if (json['additionalData'] != null) { | ||||
|       List<String> temp = List<String>.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<MapEntry<String, String>> apkUrls = []; | ||||
|     if (json['apkUrls'] != null) { | ||||
|       var apkUrlJson = jsonDecode(json['apkUrls']); | ||||
|       try { | ||||
|         apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson)); | ||||
|       } catch (e) { | ||||
|         apkUrls = List<dynamic>.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<String, dynamic>, | ||||
|         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<String, dynamic> 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<MapEntry<String, String>> getApkUrlsFromUrls(List<String> 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<AppSource> 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<AppSource> 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<MassAppUrlSource> 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<App> getApp( | ||||
|       AppSource source, String url, Map<String, dynamic> 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 | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user