mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +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