diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85166e6..d4bd632 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: for apk in ./build/app/outputs/flutter-apk/*-release*.apk; do unsignedFn=${apk/-release/-unsigned} mv "$apk" "$unsignedFn" - ${ANDROID_HOME}/build-tools/30.0.2/apksigner sign --ks apksign.keystore --ks-pass pass:"${KEYSTORE_PASSWORD}" --out "${apk}" "${unsignedFn}" + ${ANDROID_HOME}/build-tools/$(ls ${ANDROID_HOME}/build-tools/ | tail -1)/apksigner sign --ks apksign.keystore --ks-pass pass:"${KEYSTORE_PASSWORD}" --out "${apk}" "${unsignedFn}" sha256sum ${apk} | cut -d " " -f 1 > "$apk".sha256 gpg --batch --pinentry-mode loopback --passphrase "${PGP_PASSPHRASE}" --sign --detach-sig "$apk".sha256 done diff --git a/README.md b/README.md index 648bf53..ea52c19 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Currently supported App sources: - [SourceForge](https://sourceforge.net/) - [SourceHut](https://git.sr.ht/) - Other - General: - - [APKPure](https://apkpure.com/) + - [APKPure](https://apkpure.net/) - [Aptoide](https://aptoide.com/) - [Uptodown](https://uptodown.com/) - [APKMirror](https://apkmirror.com/) (Track-Only) diff --git a/assets/translations/bs.json b/assets/translations/bs.json index 90d0737..fa1fcba 100644 --- a/assets/translations/bs.json +++ b/assets/translations/bs.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Želite li ukloniti aplikaciju?", "other": "Želite li ukloniti aplikacije?" diff --git a/assets/translations/cs.json b/assets/translations/cs.json index 13bf6d7..18b0a67 100644 --- a/assets/translations/cs.json +++ b/assets/translations/cs.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Správce", "shizukuBinderNotFound": "Shizuku neběží", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Odstranit Apku?", "other": "Odstranit Apky?" diff --git a/assets/translations/de.json b/assets/translations/de.json index 28ef8fb..3c924b4 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku läuft nicht", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "App entfernen?", "other": "Apps entfernen?" diff --git a/assets/translations/en.json b/assets/translations/en.json index ed5a898..7f2daf1 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -289,6 +289,8 @@ "shizukuBinderNotFound": "Сompatible Shizuku service wasn't found", "useSystemFont": "Use the system font", "systemFontError": "Error loading the system font: {}", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Remove App?", "other": "Remove Apps?" diff --git a/assets/translations/es.json b/assets/translations/es.json index 82c00a4..abe6417 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku no está operativo", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "¿Eliminar Aplicación?", "other": "¿Eliminar Aplicaciones?" diff --git a/assets/translations/fa.json b/assets/translations/fa.json index 9cf7264..feea14f 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "برنامه حذف شود؟", "other": "برنامه ها حذف شوند؟" diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 70aac00..0a64673 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Supprimer l'application ?", "other": "Supprimer les applications ?" diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 61ebca0..df48201 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Eltávolítja az alkalmazást?", "other": "Eltávolítja az alkalmazást?" diff --git a/assets/translations/it.json b/assets/translations/it.json index 55cecc8..91dff86 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku non è in esecuzione", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Rimuovere l'app?", "other": "Rimuovere le app?" diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 4a0494f..43101c1 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -287,6 +287,10 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizukuが起動していません", + "useSystemFont": "システムフォントを使用する", + "systemFontError": "システムフォントの読み込みエラー: {}", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "アプリを削除しますか?", "other": "アプリを削除しますか?" diff --git a/assets/translations/nl.json b/assets/translations/nl.json index cb5f434..88ae1b4 100644 --- a/assets/translations/nl.json +++ b/assets/translations/nl.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "App verwijderen?", "other": "Apps verwijderen?" diff --git a/assets/translations/pl.json b/assets/translations/pl.json index 23b1c1a..c602475 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Usunąć aplikację?", "few": "Usunąć aplikacje?", diff --git a/assets/translations/pt.json b/assets/translations/pt.json index 809a850..2fe1cb2 100644 --- a/assets/translations/pt.json +++ b/assets/translations/pt.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku não está rodando", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Remover aplicativo?", "other": "Remover aplicativos?" diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 67c424d..ab25718 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -289,6 +289,8 @@ "shizukuBinderNotFound": "Совместимый сервис Shizuku не найден", "useSystemFont": "Использовать системный шрифт", "systemFontError": "Ошибка загрузки системного шрифта: {}", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Удалить приложение?", "other": "Удалить приложения?" diff --git a/assets/translations/sv.json b/assets/translations/sv.json index 73fd8ea..6886fdc 100644 --- a/assets/translations/sv.json +++ b/assets/translations/sv.json @@ -273,6 +273,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Ta Bort App?", "other": "Ta Bort Appar?" diff --git a/assets/translations/tr.json b/assets/translations/tr.json index c3a96f1..35f8c16 100644 --- a/assets/translations/tr.json +++ b/assets/translations/tr.json @@ -287,6 +287,8 @@ "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku is not running", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "Uygulamayı Kaldır?", "other": "Uygulamaları Kaldır?" diff --git a/assets/translations/vi.json b/assets/translations/vi.json index 7587e73..9d65204 100644 --- a/assets/translations/vi.json +++ b/assets/translations/vi.json @@ -11,7 +11,7 @@ "unexpectedError": "Lỗi không mong đợi", "ok": "OK", "and": "và", - "githubPATLabel": "Mã thông báo truy cập cá nhân GitHub (Tăng tốc độ giới hạn)", + "githubPATLabel": "GitHub Token (Tăng tốc độ, giới hạn)", "includePrereleases": "Bao gồm các bản phát hành trước", "fallbackToOlderReleases": "Dự phòng về bản phát hành cũ hơn", "filterReleaseTitlesByRegEx": "Lọc tiêu đề bản phát hành theo biểu thức chính quy", @@ -34,7 +34,7 @@ "cancelled": "Đã hủy", "appAlreadyAdded": "Ứng dụng được thêm rồi", "alreadyUpToDateQuestion": "Ứng dụng đã được cập nhật?", - "addApp": "Thêm ứng dụng", + "addApp": "Thêm", "appSourceURL": "URL nguồn ứng dụng", "error": "Lỗi", "add": "Thêm", @@ -48,7 +48,7 @@ "noApps": "Không có ứng dụng", "noAppsForFilter": "Không có ứng dụng cho bộ lọc", "byX": "Bởi {}", - "percentProgress": "Tiến triển: {}%", + "percentProgress": "Đang tải {}%", "pleaseWait": "Vui lòng chờ", "updateAvailable": "Có sẵn bản cập nhật", "estimateInBracketsShort": "(Ước lượng.)", @@ -88,10 +88,10 @@ "importExport": "Nhập/Xuất", "settings": "Cài đặt", "exportedTo": "Đã xuất sang {}", - "obtainiumExport": "Xuất Obtainium", + "obtainiumExport": "Xuất", "invalidInput": "Đầu vào không hợp lệ", "importedX": "Đã nhập {}", - "obtainiumImport": "Nhập Obtainium", + "obtainiumImport": "Nhập", "importFromURLList": "Nhập từ danh sách URL", "searchQuery": "Truy vấn tìm kiếm", "appURLList": "Danh sách URL ứng dụng", @@ -120,13 +120,13 @@ "appSortOrder": "Thứ tự sắp xếp", "ascending": "Tăng dần", "descending": "Giảm dần", - "bgUpdateCheckInterval": "Khoảng thời gian kiểm tra cập nhật nền", - "neverManualOnly": "Không bao giờ - Chỉ thủ công", + "bgUpdateCheckInterval": "Thời gian tự động kiểm tra cập nhật", + "neverManualOnly": "Không bao giờ", "appearance": "Hiển thị", - "showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem Ứng dụng", - "pinUpdates": "Ghim nội dung cập nhật lên đầu chế độ xem Ứng dụng", + "showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem chi tiết Ứng dụng", + "pinUpdates": "Chuyển ứng dụng có phiên bản mới lên đầu danh sách", "updates": "Cập nhật", - "sourceSpecific": "Nguồn cụ thể", + "sourceSpecific": "Cài đặt Nguồn", "appSource": "Nguồn ứng dụng", "noLogs": "Không có nhật ký", "appLogs": "Nhật ký ứng dụng", @@ -219,8 +219,8 @@ "dontShowAgain": "Đừng hiển thị thông tin này nữa", "dontShowTrackOnlyWarnings": "Không hiển thị cảnh báo 'Chỉ-Theo dõi'", "dontShowAPKOriginWarnings": "Không hiển thị cảnh báo nguồn gốc APK", - "moveNonInstalledAppsToBottom": "Di chuyển Ứng dụng chưa được cài đặt xuống cuối chế độ xem Ứng dụng", - "gitlabPATLabel": "Mã thông báo truy cập cá nhân GitLab\n(Cho phép tìm kiếm và khám phá APK tốt hơn)", + "moveNonInstalledAppsToBottom": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách", + "gitlabPATLabel": "GitLab Token\n(Cho phép tìm kiếm và lọc APK tốt hơn)", "about": "Giới thiệu", "requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Cài đặt)", "checkOnStart": "Kiểm tra các bản cập nhật khi khởi động", @@ -241,9 +241,9 @@ "appsPossiblyUpdated": "Đã cố gắng cập nhật ứng dụng", "appsPossiblyUpdatedNotifDescription": "Thông báo cho người dùng rằng các bản cập nhật cho một hoặc nhiều Ứng dụng có khả năng được áp dụng trong nền", "xWasPossiblyUpdatedToY": "{} có thể đã được cập nhật thành {}.", - "enableBackgroundUpdates": "Bật cập nhật nền", - "backgroundUpdateReqsExplanation": "Có thể không thực hiện được cập nhật nền cho tất cả ứng dụng.", - "backgroundUpdateLimitsExplanation": "Sự thành công của cài đặt nền chỉ có thể được xác định khi mở Obtainium.", + "enableBackgroundUpdates": "Tự động cập nhật trong nền", + "backgroundUpdateReqsExplanation": "Có thể không thực hiện được cập nhật trong nền cho tất cả ứng dụng.", + "backgroundUpdateLimitsExplanation": "Sự thành công của cài đặt trong nền chỉ có thể được xác định khi mở Obtainium.", "verifyLatestTag": "Xác minh thẻ 'mới nhất'", "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit", "filterByLinkText": "Filter links by link text", @@ -256,8 +256,8 @@ "matchGroupToUse": "Nhóm đối sánh để sử dụng cho Regex trích xuất phiên bản", "highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn", "pickExportDir": "Chọn thư mục xuất", - "autoExportOnChanges": "Tự động xuất khi thay đổi", - "includeSettings": "Include settings", + "autoExportOnChanges": "Tự động xuất", + "includeSettings": "Bao gồm cài đặt ứng dụng", "filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy", "trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất", "dontSortReleasesList": "Giữ lại thứ tự phát hành từ API", @@ -278,12 +278,12 @@ "downloadingXNotifChannel": "Đang tải xuống {}", "completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng", "checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật", - "onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra các ứng dụng đã cài đặt và Chỉ-Theo dõi để biết các bản cập nhật", + "onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra cập nhật các ứng dụng đã cài đặt và Chỉ-Theo dõi", "supportFixedAPKURL": "Support fixed APK URLs", "selectX": "Select {}", - "parallelDownloads": "Allow parallel downloads", - "installMethod": "Installation method", - "normal": "Normal", + "parallelDownloads": "Cho phép tải đa luồng", + "installMethod": "Phương thức cài đặt", + "normal": "Mặc định", "shizuku": "Shizuku", "root": "Root", "shizukuBinderNotFound": "Shizuku chưa khởi động", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 3a92d78..094c032 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -289,6 +289,8 @@ "shizukuBinderNotFound": "未发现兼容的 Shizuku 服务", "useSystemFont": "使用系统字体", "systemFontError": "加载系统字体出错:{}", + "useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version", + "requestHeader": "Request header", "removeAppQuestion": { "one": "是否删除应用?", "other": "是否删除应用?" diff --git a/lib/app_sources/apkcombo.dart b/lib/app_sources/apkcombo.dart index 802373c..1a58847 100644 --- a/lib/app_sources/apkcombo.dart +++ b/lib/app_sources/apkcombo.dart @@ -5,17 +5,19 @@ import 'package:obtainium/providers/source_provider.dart'; class APKCombo extends AppSource { APKCombo() { - host = 'apkcombo.com'; + hosts = ['apkcombo.com']; } @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+'); - var match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+', + caseSensitive: false); + var match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -26,18 +28,19 @@ class APKCombo extends AppSource { @override Future?> getRequestHeaders( - {Map additionalSettings = const {}, - bool forAPKDownload = false}) async { + Map additionalSettings, + {bool forAPKDownload = false}) async { return { "User-Agent": "curl/8.0.1", "Accept": "*/*", "Connection": "keep-alive", - "Host": "$host" + "Host": hosts[0] }; } - Future>> getApkUrls(String standardUrl) async { - var res = await sourceRequest('$standardUrl/download/apk'); + Future>> getApkUrls( + String standardUrl, Map additionalSettings) async { + var res = await sourceRequest('$standardUrl/download/apk', {}); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -70,9 +73,9 @@ class APKCombo extends AppSource { } @override - Future apkUrlPrefetchModifier( - String apkUrl, String standardUrl) async { - var freshURLs = await getApkUrls(standardUrl); + Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, + Map additionalSettings) async { + var freshURLs = await getApkUrls(standardUrl, additionalSettings); var path2Match = Uri.parse(apkUrl).path; for (var url in freshURLs) { if (Uri.parse(url.value).path == path2Match) { @@ -88,7 +91,7 @@ class APKCombo extends AppSource { Map additionalSettings, ) async { String appId = (await tryInferringAppId(standardUrl))!; - var preres = await sourceRequest(standardUrl); + var preres = await sourceRequest(standardUrl, additionalSettings); if (preres.statusCode != 200) { throw getObtainiumHttpError(preres); } @@ -112,7 +115,9 @@ class APKCombo extends AppSource { } } return APKDetails( - version, await getApkUrls(standardUrl), AppNames(author, appName), + version, + await getApkUrls(standardUrl, additionalSettings), + AppNames(author, appName), releaseDate: releaseDate); } } diff --git a/lib/app_sources/apkmirror.dart b/lib/app_sources/apkmirror.dart index f1b903b..4e3cd1f 100644 --- a/lib/app_sources/apkmirror.dart +++ b/lib/app_sources/apkmirror.dart @@ -9,7 +9,7 @@ import 'package:obtainium/providers/source_provider.dart'; class APKMirror extends AppSource { APKMirror() { - host = 'apkmirror.com'; + hosts = ['apkmirror.com']; enforceTrackOnly = true; additionalSourceAppSpecificSettingFormItems = [ @@ -32,13 +32,14 @@ class APKMirror extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = - RegExp('^https?://(www\\.)?$host/apk/[^/]+/[^/]+'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -58,7 +59,7 @@ class APKMirror extends AppSource { true ? additionalSettings['filterReleaseTitlesByRegEx'] : null; - Response res = await sourceRequest('$standardUrl/feed'); + Response res = await sourceRequest('$standardUrl/feed', additionalSettings); if (res.statusCode == 200) { var items = parse(res.body).querySelectorAll('item'); dynamic targetRelease; @@ -84,7 +85,7 @@ class APKMirror extends AppSource { dateString != null ? HttpDate.parse('$dateString GMT') : null; String? version = titleString ?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0, - RegExp(' by ').firstMatch(titleString)?.start ?? 0) + RegExp(' by ').allMatches(titleString).last.start) .trim(); if (version == null || version.isEmpty) { version = titleString; diff --git a/lib/app_sources/apkpure.dart b/lib/app_sources/apkpure.dart index 0fe3548..f4c0026 100644 --- a/lib/app_sources/apkpure.dart +++ b/lib/app_sources/apkpure.dart @@ -20,26 +20,28 @@ parseDateTimeMMMddCommayyyy(String? dateString) { class APKPure extends AppSource { APKPure() { - host = 'apkpure.com'; + hosts = ['apkpure.net', 'apkpure.com']; allowSubDomains = true; naiveStandardVersionDetection = true; } @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegExB = - RegExp('^https?://m.$host/+[^/]+/+[^/]+(/+[^/]+)?'); - RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExB = RegExp( + '^https?://m.${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?', + caseSensitive: false); + RegExpMatch? match = standardUrlRegExB.firstMatch(url); if (match != null) { - url = 'https://$host${Uri.parse(url).path}'; + url = 'https://${getSourceRegex(hosts)}${Uri.parse(url).path}'; } - RegExp standardUrlRegExA = - RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+(/+[^/]+)?'); - match = standardUrlRegExA.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExA = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?', + caseSensitive: false); + match = standardUrlRegExA.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -55,8 +57,8 @@ class APKPure extends AppSource { ) async { String appId = (await tryInferringAppId(standardUrl))!; String host = Uri.parse(standardUrl).host; - var res = await sourceRequest('$standardUrl/download'); - var resChangelog = await sourceRequest(standardUrl); + var res = await sourceRequest('$standardUrl/download', additionalSettings); + var resChangelog = await sourceRequest(standardUrl, additionalSettings); if (res.statusCode == 200 && resChangelog.statusCode == 200) { var html = parse(res.body); var htmlChangelog = parse(resChangelog.body); @@ -69,7 +71,8 @@ class APKPure extends AppSource { DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString); String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; List> apkUrls = [ - MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') + MapEntry('$appId.apk', + 'https://d.${hosts.contains(host) ? 'cdnpure.com' : host}/b/$type/$appId?version=latest') ]; String author = html .querySelector('span.info-sdk') diff --git a/lib/app_sources/aptoide.dart b/lib/app_sources/aptoide.dart index ea162df..a2795d6 100644 --- a/lib/app_sources/aptoide.dart +++ b/lib/app_sources/aptoide.dart @@ -6,7 +6,7 @@ import 'package:obtainium/providers/source_provider.dart'; class Aptoide extends AppSource { Aptoide() { - host = 'aptoide.com'; + hosts = ['aptoide.com']; name = 'Aptoide'; allowSubDomains = true; naiveStandardVersionDetection = true; @@ -14,22 +14,26 @@ class Aptoide extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override Future tryInferringAppId(String standardUrl, {Map additionalSettings = const {}}) async { - return (await getAppDetailsJSON(standardUrl))['package']; + return (await getAppDetailsJSON( + standardUrl, additionalSettings))['package']; } - Future> getAppDetailsJSON(String standardUrl) async { - var res = await sourceRequest(standardUrl); + Future> getAppDetailsJSON( + String standardUrl, Map additionalSettings) async { + var res = await sourceRequest(standardUrl, additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -40,8 +44,8 @@ class Aptoide extends AppSource { } else { throw NoReleasesError(); } - var res2 = - await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id'); + var res2 = await sourceRequest( + 'https://ws2.aptoide.com/api/7/getApp/app_id/$id', additionalSettings); if (res2.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -53,7 +57,7 @@ class Aptoide extends AppSource { String standardUrl, Map additionalSettings, ) async { - var appDetails = await getAppDetailsJSON(standardUrl); + var appDetails = await getAppDetailsJSON(standardUrl, additionalSettings); String appName = appDetails['name'] ?? tr('app'); String author = appDetails['developer']?['name'] ?? name; String? dateStr = appDetails['updated']; diff --git a/lib/app_sources/codeberg.dart b/lib/app_sources/codeberg.dart index 44a7bdf..bf01701 100644 --- a/lib/app_sources/codeberg.dart +++ b/lib/app_sources/codeberg.dart @@ -5,7 +5,7 @@ import 'package:obtainium/providers/source_provider.dart'; class Codeberg extends AppSource { GitHub gh = GitHub(); Codeberg() { - host = 'codeberg.org'; + hosts = ['codeberg.org']; additionalSourceAppSpecificSettingFormItems = gh.additionalSourceAppSpecificSettingFormItems; @@ -16,12 +16,14 @@ class Codeberg extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -35,7 +37,7 @@ class Codeberg extends AppSource { ) async { return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings, (bool useTagUrl) async { - return 'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100'; + return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100'; }, null); } @@ -50,7 +52,7 @@ class Codeberg extends AppSource { {Map querySettings = const {}}) async { return gh.searchCommon( query, - 'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100', + 'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100', 'data', querySettings: querySettings); } diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index a9e1707..9129f3c 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -9,7 +9,7 @@ import 'package:obtainium/providers/source_provider.dart'; class FDroid extends AppSource { FDroid() { - host = 'f-droid.org'; + hosts = ['f-droid.org']; name = tr('fdroid'); naiveStandardVersionDetection = true; canSearch = true; @@ -37,20 +37,22 @@ class FDroid extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegExB = - RegExp('^https?://(www\\.)?$host/+[^/]+/+packages/+[^/]+'); - RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExB = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegExB.firstMatch(url); if (match != null) { url = - 'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}'; + 'https://${Uri.parse(match.group(0)!).host}/packages/${Uri.parse(url).pathSegments.last}'; } - RegExp standardUrlRegExA = - RegExp('^https?://(www\\.)?$host/+packages/+[^/]+'); - match = standardUrlRegExA.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExA = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+', + caseSensitive: false); + match = standardUrlRegExA.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -67,7 +69,8 @@ class FDroid extends AppSource { String? appId = await tryInferringAppId(standardUrl); String host = Uri.parse(standardUrl).host; var details = getAPKUrlsFromFDroidPackagesAPIResponse( - await sourceRequest('https://$host/api/v1/packages/$appId'), + await sourceRequest( + 'https://$host/api/v1/packages/$appId', additionalSettings), 'https://$host/repo/$appId', standardUrl, name, @@ -84,29 +87,30 @@ class FDroid extends AppSource { if (!hostChanged) { try { var res = await sourceRequest( - 'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml'); + 'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml', + additionalSettings); var lines = res.body.split('\n'); - String author = lines - .where((l) => l.startsWith('AuthorName: ')) - .first - .split(': ') - .sublist(1) - .join(': '); - details.names.author = author; + var authorLines = lines.where((l) => l.startsWith('AuthorName: ')); + if (authorLines.isNotEmpty) { + details.names.author = + authorLines.first.split(': ').sublist(1).join(': '); + } var changelogUrls = lines.where((l) => l.startsWith('Changelog: ')); if (changelogUrls.isNotEmpty) { details.changeLog = changelogUrls.first; - details.changeLog = (await sourceRequest(details.changeLog! - .split(': ') - .sublist(1) - .join(': ') - .replaceFirst('/blob/', '/raw/'))) + details.changeLog = (await sourceRequest( + details.changeLog! + .split(': ') + .sublist(1) + .join(': ') + .replaceFirst('/blob/', '/raw/'), + additionalSettings)) .body; } } catch (e) { // Fail silently } - if ((details.changeLog?.length ?? 0) > 1000) { + if ((details.changeLog?.length ?? 0) > 2048) { details.changeLog = '${details.changeLog!.substring(0, 2048)}...'; } } @@ -117,7 +121,7 @@ class FDroid extends AppSource { Future>> search(String query, {Map querySettings = const {}}) async { Response res = await sourceRequest( - 'https://search.$host/?q=${Uri.encodeQueryComponent(query)}'); + 'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {}); if (res.statusCode == 200) { Map> urlsWithDescriptions = {}; parse(res.body).querySelectorAll('.package-header').forEach((e) { diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index f988a4f..a1c571b 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -59,7 +59,7 @@ class FDroidRepo extends AppSource { throw NoReleasesError(); } url = removeQueryParamsFromUrl(standardizeUrl(url)); - var res = await sourceRequest('$url/index.xml'); + var res = await sourceRequest('$url/index.xml', {}); if (res.statusCode == 200) { var body = parse(res.body); Map> results = {}; @@ -117,7 +117,8 @@ class FDroidRepo extends AppSource { throw NoReleasesError(); } var res = await sourceRequest( - '$standardUrl${standardUrl.endsWith('/index.xml') ? '' : '/index.xml'}'); + '$standardUrl${standardUrl.endsWith('/index.xml') ? '' : '/index.xml'}', + additionalSettings); if (res.statusCode == 200) { var body = parse(res.body); var foundApps = body.querySelectorAll('application').where((element) { diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index 754de12..7c0e19d 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -14,7 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class GitHub extends AppSource { GitHub() { - host = 'github.com'; + hosts = ['github.com']; appIdInferIsOptional = true; sourceConfigSettingFormItems = [ @@ -108,7 +108,8 @@ class GitHub extends AppSource { for (var path in possibleBuildGradleLocations) { try { var res = await sourceRequest( - '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path'); + '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path', + additionalSettings); if (res.statusCode == 200) { try { var body = jsonDecode(res.body); @@ -149,18 +150,20 @@ class GitHub extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override Future?> getRequestHeaders( - {Map additionalSettings = const {}, - bool forAPKDownload = false}) async { + Map additionalSettings, + {bool forAPKDownload = false}) async { var token = await getTokenIfAny(additionalSettings); var headers = {}; if (token != null) { @@ -203,11 +206,11 @@ class GitHub extends AppSource { } Future getAPIHost(Map additionalSettings) async => - 'https://api.$host'; + 'https://api.${hosts[0]}'; Future convertStandardUrlToAPIUrl( String standardUrl, Map additionalSettings) async => - '${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://$host'.length)}'; + '${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}'; @override String? changeLogPageFromStandardUrl(String standardUrl) => @@ -238,7 +241,8 @@ class GitHub extends AppSource { if (verifyLatestTag) { var temp = requestUrl.split('?'); Response res = await sourceRequest( - '${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}'); + '${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}', + additionalSettings); if (res.statusCode != 200) { if (onHttpErrorCode != null) { onHttpErrorCode(res); @@ -247,7 +251,7 @@ class GitHub extends AppSource { } latestRelease = jsonDecode(res.body); } - Response res = await sourceRequest(requestUrl); + Response res = await sourceRequest(requestUrl, additionalSettings); if (res.statusCode == 200) { var releases = jsonDecode(res.body) as List; if (latestRelease != null) { @@ -424,7 +428,7 @@ class GitHub extends AppSource { String query, String requestUrl, String rootProp, {Function(Response)? onHttpErrorCode, Map querySettings = const {}}) async { - Response res = await sourceRequest(requestUrl); + Response res = await sourceRequest(requestUrl, {}); if (res.statusCode == 200) { int minStarCount = querySettings['minStarCount'] != null ? int.parse(querySettings['minStarCount']) diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index a0e8f14..0b7776f 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -13,7 +13,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class GitLab extends AppSource { GitLab() { - host = 'gitlab.com'; + hosts = ['gitlab.com']; canSearch = true; sourceConfigSettingFormItems = [ @@ -52,12 +52,14 @@ class GitLab extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } Future getPATIfAny(Map additionalSettings) async { @@ -81,15 +83,15 @@ class GitLab extends AppSource { Future>> search(String query, {Map querySettings = const {}}) async { var url = - 'https://$host/api/v4/projects?search=${Uri.encodeQueryComponent(query)}'; - var res = await sourceRequest(url); + 'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}'; + var res = await sourceRequest(url, {}); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } var json = jsonDecode(res.body) as List; Map> results = {}; for (var element in json) { - results['https://$host/${element['path_with_namespace']}'] = [ + results['https://${hosts[0]}/${element['path_with_namespace']}'] = [ element['name_with_namespace'], element['description'] ?? tr('noDescription') ]; @@ -113,7 +115,8 @@ class GitLab extends AppSource { if (PAT != null) { var names = GitHub().getAppNames(standardUrl); Response res = await sourceRequest( - 'https://$host/api/v4/projects/${names.author}%2F${names.name}/releases?private_token=$PAT'); + 'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/releases?private_token=$PAT', + additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -148,7 +151,8 @@ class GitLab extends AppSource { releaseDate: releaseDate); }); } else { - Response res = await sourceRequest('$standardUrl/-/tags?format=atom'); + Response res = await sourceRequest( + '$standardUrl/-/tags?format=atom', additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index e6e45ed..1e8b1e9 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -19,6 +19,8 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) { .toList(); String absoluteUrl; if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) { + absoluteUrl = '${referenceAbsoluteUrl.origin}$ambiguousUrl'; + } else if (currPathSegments.isEmpty) { absoluteUrl = '${referenceAbsoluteUrl.origin}/$ambiguousUrl'; } else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) { absoluteUrl = @@ -139,7 +141,37 @@ class HTML extends AppSource { ], finalStepFormitems[0], ...commonFormItems, - ...finalStepFormitems.sublist(1) + ...finalStepFormitems.sublist(1), + [ + GeneratedFormSubForm( + 'requestHeader', + [ + [ + GeneratedFormTextField('requestHeader', + label: tr('requestHeader'), + additionalValidators: [ + (value) { + if ((value ?? 'empty:valid') + .split(':') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .length < + 2) { + return tr('invalidInput'); + } + return null; + } + ]) + ] + ], + label: tr('requestHeader'), + defaultValue: [ + { + 'requestHeader': + 'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36' + } + ]) + ] ]; overrideVersionDetectionFormDefault('noVersionDetection', disableStandard: false, disableRelDate: true); @@ -147,12 +179,25 @@ class HTML extends AppSource { @override Future?> getRequestHeaders( - {Map additionalSettings = const {}, - bool forAPKDownload = false}) async { - return { - "User-Agent": - "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" - }; + Map additionalSettings, + {bool forAPKDownload = false}) async { + if (additionalSettings.isNotEmpty) { + if (additionalSettings['requestHeader']?.isNotEmpty != true) { + additionalSettings['requestHeader'] = []; + } + additionalSettings['requestHeader'] = additionalSettings['requestHeader'] + .where((l) => l['requestHeader'].isNotEmpty == true) + .toList(); + Map requestHeaders = {}; + for (int i = 0; i < (additionalSettings['requestHeader'].length); i++) { + var temp = + (additionalSettings['requestHeader'][i]['requestHeader'] as String) + .split(':'); + requestHeaders[temp[0].trim()] = temp.sublist(1).join(':').trim(); + } + return requestHeaders; + } + return null; } @override @@ -233,7 +278,8 @@ class HTML extends AppSource { .where((l) => l['customLinkFilterRegex'].isNotEmpty == true) .toList(); for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) { - var intLinks = await grabLinksCommon(await sourceRequest(currentUrl), + var intLinks = await grabLinksCommon( + await sourceRequest(currentUrl, additionalSettings), additionalSettings['intermediateLink'][i]); if (intLinks.isEmpty) { throw NoReleasesError(); @@ -243,7 +289,7 @@ class HTML extends AppSource { } var uri = Uri.parse(currentUrl); - Response res = await sourceRequest(currentUrl); + Response res = await sourceRequest(currentUrl, additionalSettings); var links = await grabLinksCommon(res, additionalSettings); if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true) { diff --git a/lib/app_sources/huaweiappgallery.dart b/lib/app_sources/huaweiappgallery.dart index a073d2a..54ab2ec 100644 --- a/lib/app_sources/huaweiappgallery.dart +++ b/lib/app_sources/huaweiappgallery.dart @@ -6,26 +6,30 @@ import 'package:obtainium/providers/source_provider.dart'; class HuaweiAppGallery extends AppSource { HuaweiAppGallery() { name = 'Huawei AppGallery'; - host = 'appgallery.huawei.com'; + hosts = ['appgallery.huawei.com']; overrideVersionDetectionFormDefault('releaseDateAsVersion', disableStandard: true); } @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/app/[^/]+'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/app/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } getDlUrl(String standardUrl) => - 'https://${host!.replaceAll('appgallery.', 'appgallery.cloud.')}/appdl/${standardUrl.split('/').last}'; + 'https://${hosts[0].replaceAll('appgallery.', 'appgallery.cloud.')}/appdl/${standardUrl.split('/').last}'; - requestAppdlRedirect(String dlUrl) async { - Response res = await sourceRequest(dlUrl, followRedirects: false); + requestAppdlRedirect( + String dlUrl, Map additionalSettings) async { + Response res = + await sourceRequest(dlUrl, additionalSettings, followRedirects: false); if (res.statusCode == 200 || res.statusCode == 302 || res.statusCode == 304) { @@ -52,7 +56,7 @@ class HuaweiAppGallery extends AppSource { Future tryInferringAppId(String standardUrl, {Map additionalSettings = const {}}) async { String dlUrl = getDlUrl(standardUrl); - Response res = await requestAppdlRedirect(dlUrl); + Response res = await requestAppdlRedirect(dlUrl, additionalSettings); return res.headers['location'] != null ? appIdFromRedirectDlUrl(res.headers['location']!) : null; @@ -64,7 +68,7 @@ class HuaweiAppGallery extends AppSource { Map additionalSettings, ) async { String dlUrl = getDlUrl(standardUrl); - Response res = await requestAppdlRedirect(dlUrl); + Response res = await requestAppdlRedirect(dlUrl, additionalSettings); if (res.headers['location'] == null) { throw NoReleasesError(); } diff --git a/lib/app_sources/izzyondroid.dart b/lib/app_sources/izzyondroid.dart index dd301b3..ace8ed1 100644 --- a/lib/app_sources/izzyondroid.dart +++ b/lib/app_sources/izzyondroid.dart @@ -6,7 +6,7 @@ class IzzyOnDroid extends AppSource { late FDroid fd; IzzyOnDroid() { - host = 'izzysoft.de'; + hosts = ['izzysoft.de']; fd = FDroid(); additionalSourceAppSpecificSettingFormItems = fd.additionalSourceAppSpecificSettingFormItems; @@ -15,17 +15,20 @@ class IzzyOnDroid extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegExA = RegExp('^https?://android.$host/repo/apk/[^/]+'); - RegExpMatch? match = standardUrlRegExA.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExA = RegExp( + '^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegExA.firstMatch(url); if (match == null) { - RegExp standardUrlRegExB = - RegExp('^https?://apt.$host/fdroid/index/apk/[^/]+'); - match = standardUrlRegExB.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExB = RegExp( + '^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+', + caseSensitive: false); + match = standardUrlRegExB.firstMatch(url); } if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -42,7 +45,8 @@ class IzzyOnDroid extends AppSource { String? appId = await tryInferringAppId(standardUrl); return fd.getAPKUrlsFromFDroidPackagesAPIResponse( await sourceRequest( - 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'), + 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId', + additionalSettings), 'https://android.izzysoft.de/frepo/$appId', standardUrl, name, diff --git a/lib/app_sources/jenkins.dart b/lib/app_sources/jenkins.dart index 9e5af8d..8e817db 100644 --- a/lib/app_sources/jenkins.dart +++ b/lib/app_sources/jenkins.dart @@ -8,6 +8,7 @@ class Jenkins extends AppSource { Jenkins() { overrideVersionDetectionFormDefault('releaseDateAsVersion', disableStandard: true); + neverAutoSelect = true; } String trimJobUrl(String url) { @@ -16,7 +17,7 @@ class Jenkins extends AppSource { if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -29,8 +30,8 @@ class Jenkins extends AppSource { Map additionalSettings, ) async { standardUrl = trimJobUrl(standardUrl); - Response res = - await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json'); + Response res = await sourceRequest( + '$standardUrl/lastSuccessfulBuild/api/json', additionalSettings); if (res.statusCode == 200) { var json = jsonDecode(res.body); var releaseDate = json['timestamp'] == null diff --git a/lib/app_sources/mullvad.dart b/lib/app_sources/mullvad.dart index ff9cefb..3b0439d 100644 --- a/lib/app_sources/mullvad.dart +++ b/lib/app_sources/mullvad.dart @@ -6,17 +6,19 @@ import 'package:obtainium/providers/source_provider.dart'; class Mullvad extends AppSource { Mullvad() { - host = 'mullvad.net'; + hosts = ['mullvad.net']; } @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -28,7 +30,8 @@ class Mullvad extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await sourceRequest('$standardUrl/en/download/android'); + Response res = await sourceRequest( + '$standardUrl/en/download/android', additionalSettings); if (res.statusCode == 200) { var versions = parse(res.body) .querySelectorAll('p') diff --git a/lib/app_sources/neutroncode.dart b/lib/app_sources/neutroncode.dart index 9ce0fdd..b0f2d93 100644 --- a/lib/app_sources/neutroncode.dart +++ b/lib/app_sources/neutroncode.dart @@ -5,18 +5,19 @@ import 'package:obtainium/providers/source_provider.dart'; class NeutronCode extends AppSource { NeutronCode() { - host = 'neutroncode.com'; + hosts = ['neutroncode.com']; } @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = - RegExp('^https?://(www\\.)?$host/downloads/file/[^/]+'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -79,7 +80,7 @@ class NeutronCode extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await sourceRequest(standardUrl); + Response res = await sourceRequest(standardUrl, additionalSettings); if (res.statusCode == 200) { var http = parse(res.body); var name = http.querySelector('.pd-title')?.innerHtml; @@ -92,7 +93,7 @@ class NeutronCode extends AppSource { if (version == null) { throw NoVersionError(); } - String? apkUrl = 'https://$host/download/$filename'; + String? apkUrl = 'https://${hosts[0]}/download/$filename'; var dateStringOriginal = http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml; var dateString = dateStringOriginal != null diff --git a/lib/app_sources/signal.dart b/lib/app_sources/signal.dart index f7bda25..8e91939 100644 --- a/lib/app_sources/signal.dart +++ b/lib/app_sources/signal.dart @@ -5,12 +5,12 @@ import 'package:obtainium/providers/source_provider.dart'; class Signal extends AppSource { Signal() { - host = 'signal.org'; + hosts = ['signal.org']; } @override String sourceSpecificStandardizeURL(String url) { - return 'https://$host'; + return 'https://${hosts[0]}'; } @override @@ -18,8 +18,8 @@ class Signal extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = - await sourceRequest('https://updates.$host/android/latest.json'); + Response res = await sourceRequest( + 'https://updates.${hosts[0]}/android/latest.json', additionalSettings); if (res.statusCode == 200) { var json = jsonDecode(res.body); String? apkUrl = json['url']; diff --git a/lib/app_sources/sourceforge.dart b/lib/app_sources/sourceforge.dart index 885a9cc..52f6aca 100644 --- a/lib/app_sources/sourceforge.dart +++ b/lib/app_sources/sourceforge.dart @@ -5,24 +5,27 @@ import 'package:obtainium/providers/source_provider.dart'; class SourceForge extends AppSource { SourceForge() { - host = 'sourceforge.net'; + hosts = ['sourceforge.net']; } @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegExB = RegExp('^https?://(www\\.)?$host/p/[^/]+'); - RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExB = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/p/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegExB.firstMatch(url); if (match != null) { url = - 'https://${Uri.parse(url.substring(0, match.end)).host}/projects/${url.substring(Uri.parse(url.substring(0, match.end)).host.length + '/projects/'.length + 1)}'; + 'https://${Uri.parse(match.group(0)!).host}/projects/${url.substring(Uri.parse(match.group(0)!).host.length + '/projects/'.length + 1)}'; } - RegExp standardUrlRegExA = - RegExp('^https?://(www\\.)?$host/projects/[^/]+'); - match = standardUrlRegExA.firstMatch(url.toLowerCase()); + RegExp standardUrlRegExA = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/projects/[^/]+', + caseSensitive: false); + match = standardUrlRegExA.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -30,7 +33,8 @@ class SourceForge extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await sourceRequest('$standardUrl/rss?path=/'); + Response res = + await sourceRequest('$standardUrl/rss?path=/', additionalSettings); if (res.statusCode == 200) { var parsedHtml = parse(res.body); var allDownloadLinks = diff --git a/lib/app_sources/sourcehut.dart b/lib/app_sources/sourcehut.dart index 05b9959..bb19b96 100644 --- a/lib/app_sources/sourcehut.dart +++ b/lib/app_sources/sourcehut.dart @@ -8,7 +8,7 @@ import 'package:easy_localization/easy_localization.dart'; class SourceHut extends AppSource { SourceHut() { - host = 'git.sr.ht'; + hosts = ['git.sr.ht']; additionalSourceAppSpecificSettingFormItems = [ [ @@ -20,12 +20,14 @@ class SourceHut extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return url.substring(0, match.end); + return match.group(0)!; } @override @@ -40,7 +42,8 @@ class SourceHut extends AppSource { String appName = standardUri.pathSegments.last; bool fallbackToOlderReleases = additionalSettings['fallbackToOlderReleases'] == true; - Response res = await sourceRequest('$standardUrl/refs/rss.xml'); + Response res = + await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings); if (res.statusCode == 200) { var parsedHtml = parse(res.body); List apkDetailsList = []; @@ -69,7 +72,7 @@ class SourceHut extends AppSource { } catch (e) { // ignore } - var res2 = await sourceRequest(releasePage); + var res2 = await sourceRequest(releasePage, additionalSettings); List> apkUrls = []; if (res2.statusCode == 200) { apkUrls = getApkUrlsFromUrls(parse(res2.body) diff --git a/lib/app_sources/steammobile.dart b/lib/app_sources/steammobile.dart index 65c518b..c70a6f7 100644 --- a/lib/app_sources/steammobile.dart +++ b/lib/app_sources/steammobile.dart @@ -7,7 +7,7 @@ import 'package:obtainium/providers/source_provider.dart'; class SteamMobile extends AppSource { SteamMobile() { - host = 'store.steampowered.com'; + hosts = ['store.steampowered.com']; name = tr('steam'); additionalSourceAppSpecificSettingFormItems = [ [ @@ -21,7 +21,7 @@ class SteamMobile extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - return 'https://$host'; + return 'https://${hosts[0]}'; } @override @@ -29,7 +29,8 @@ class SteamMobile extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await sourceRequest('https://$host/mobile'); + Response res = + await sourceRequest('https://${hosts[0]}/mobile', additionalSettings); if (res.statusCode == 200) { var apkNamePrefix = additionalSettings['app'] as String?; if (apkNamePrefix == null) { diff --git a/lib/app_sources/telegramapp.dart b/lib/app_sources/telegramapp.dart index 44042e0..651d00f 100644 --- a/lib/app_sources/telegramapp.dart +++ b/lib/app_sources/telegramapp.dart @@ -6,13 +6,13 @@ import 'package:obtainium/providers/source_provider.dart'; class TelegramApp extends AppSource { TelegramApp() { - host = 'telegram.org'; + hosts = ['telegram.org']; name = 'Telegram ${tr('app')}'; } @override String sourceSpecificStandardizeURL(String url) { - return 'https://$host'; + return 'https://${hosts[0]}'; } @override @@ -20,7 +20,8 @@ class TelegramApp extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await sourceRequest('https://t.me/s/TAndroidAPK'); + Response res = + await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings); if (res.statusCode == 200) { var http = parse(res.body); var messages = diff --git a/lib/app_sources/uptodown.dart b/lib/app_sources/uptodown.dart index 7a0b41d..fe3d589 100644 --- a/lib/app_sources/uptodown.dart +++ b/lib/app_sources/uptodown.dart @@ -6,29 +6,33 @@ import 'package:obtainium/providers/source_provider.dart'; class Uptodown extends AppSource { Uptodown() { - host = 'uptodown.com'; + hosts = ['uptodown.com']; allowSubDomains = true; naiveStandardVersionDetection = true; } @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); - RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExp standardUrlRegEx = RegExp( + '^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}', + caseSensitive: false); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } - return '${url.substring(0, match.end)}/android/download'; + return '${match.group(0)!}/android/download'; } @override Future tryInferringAppId(String standardUrl, {Map additionalSettings = const {}}) async { - return (await getAppDetailsFromPage(standardUrl))['appId']; + return (await getAppDetailsFromPage( + standardUrl, additionalSettings))['appId']; } - Future> getAppDetailsFromPage(String standardUrl) async { - var res = await sourceRequest(standardUrl); + Future> getAppDetailsFromPage( + String standardUrl, Map additionalSettings) async { + var res = await sourceRequest(standardUrl, additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -56,7 +60,8 @@ class Uptodown extends AppSource { String standardUrl, Map additionalSettings, ) async { - var appDetails = await getAppDetailsFromPage(standardUrl); + var appDetails = + await getAppDetailsFromPage(standardUrl, additionalSettings); var version = appDetails['version']; var apkUrl = appDetails['apkUrl']; var appId = appDetails['appId']; @@ -82,9 +87,9 @@ class Uptodown extends AppSource { } @override - Future apkUrlPrefetchModifier( - String apkUrl, String standardUrl) async { - var res = await sourceRequest(apkUrl); + Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, + Map additionalSettings) async { + var res = await sourceRequest(apkUrl, additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -94,6 +99,6 @@ class Uptodown extends AppSource { if (finalUrlKey == null) { throw NoAPKError(); } - return 'https://dw.$host/dwn/$finalUrlKey'; + return 'https://dw.${hosts[0]}/dwn/$finalUrlKey'; } } diff --git a/lib/app_sources/vlc.dart b/lib/app_sources/vlc.dart index 091411a..7d16870 100644 --- a/lib/app_sources/vlc.dart +++ b/lib/app_sources/vlc.dart @@ -1,31 +1,33 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:html/parser.dart'; import 'package:http/http.dart'; -import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; class VLC extends AppSource { VLC() { - host = 'videolan.org'; + hosts = ['videolan.org']; } - get dwUrlBase => 'https://get.$host/vlc-android/'; + get dwUrlBase => 'https://get.${hosts[0]}/vlc-android/'; @override Future?> getRequestHeaders( - {Map additionalSettings = const {}, - bool forAPKDownload = false}) => - HTML().getRequestHeaders( - additionalSettings: additionalSettings, - forAPKDownload: forAPKDownload); + Map additionalSettings, + {bool forAPKDownload = false}) async { + return { + "User-Agent": + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" + }; + } @override String sourceSpecificStandardizeURL(String url) { - return 'https://$host'; + return 'https://${hosts[0]}'; } - Future getLatestVersion(String standardUrl) async { - Response res = await sourceRequest(dwUrlBase); + Future getLatestVersion( + String standardUrl, Map additionalSettings) async { + Response res = await sourceRequest(dwUrlBase, additionalSettings); if (res.statusCode == 200) { var dwLinks = parse(res.body) .querySelectorAll('a') @@ -77,9 +79,9 @@ class VLC extends AppSource { } @override - Future apkUrlPrefetchModifier( - String apkUrl, String standardUrl) async { - Response res = await sourceRequest(apkUrl); + Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, + Map additionalSettings) async { + Response res = await sourceRequest(apkUrl, additionalSettings); if (res.statusCode == 200) { String? apkUrl = parse(res.body).querySelector('#alt_link')?.attributes['href']; diff --git a/lib/app_sources/whatsapp.dart b/lib/app_sources/whatsapp.dart index 702b52a..e9ad7c5 100644 --- a/lib/app_sources/whatsapp.dart +++ b/lib/app_sources/whatsapp.dart @@ -5,20 +5,21 @@ import 'package:obtainium/providers/source_provider.dart'; class WhatsApp extends AppSource { WhatsApp() { - host = 'whatsapp.com'; + hosts = ['whatsapp.com']; overrideVersionDetectionFormDefault('noVersionDetection', disableStandard: true, disableRelDate: true); } @override String sourceSpecificStandardizeURL(String url) { - return 'https://$host'; + return 'https://${hosts[0]}'; } @override - Future apkUrlPrefetchModifier( - String apkUrl, String standardUrl) async { - Response res = await sourceRequest('$standardUrl/android'); + Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, + Map additionalSettings) async { + Response res = + await sourceRequest('$standardUrl/android', additionalSettings); if (res.statusCode == 200) { var targetLinks = parse(res.body) .querySelectorAll('a') @@ -42,8 +43,8 @@ class WhatsApp extends AppSource { ) async { // This is a CDN link that is consistent per version // But it has query params that change constantly - Uri apkUri = - Uri.parse(await apkUrlPrefetchModifier(standardUrl, standardUrl)); + Uri apkUri = Uri.parse(await apkUrlPrefetchModifier( + standardUrl, standardUrl, additionalSettings)); var unusableApkUrl = '${apkUri.origin}/${apkUri.path}'; // So we use the param-less URL is a pseudo-version to add the app and check for updates // See #357 for why we can't scrape the version number directly diff --git a/lib/components/generated_form.dart b/lib/components/generated_form.dart index 02ecc29..5ac21f8 100644 --- a/lib/components/generated_form.dart +++ b/lib/components/generated_form.dart @@ -510,9 +510,10 @@ class _GeneratedFormState extends State { ]); } else if (widget.items[r][e] is GeneratedFormSubForm) { List subformColumn = []; + var formItems = (widget.items[r][e] as GeneratedFormSubForm).items; + var compact = formItems.length == 1 && formItems[0].length == 1; for (int i = 0; i < values[fieldKey].length; i++) { - var items = (widget.items[r][e] as GeneratedFormSubForm) - .items + var items = formItems .map((x) => x.map((y) { y.defaultValue = values[fieldKey]?[i]?[y.key]; return y; @@ -525,14 +526,15 @@ class _GeneratedFormState extends State { subformColumn.add(Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Divider(), - const SizedBox( - height: 16, - ), - Text( - '${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})', - style: const TextStyle(fontWeight: FontWeight.bold), - ), + if (!compact) + const SizedBox( + height: 16, + ), + if (!compact) + Text( + '${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})', + style: const TextStyle(fontWeight: FontWeight.bold), + ), GeneratedForm( key: internalFormKey, items: items, @@ -567,13 +569,12 @@ class _GeneratedFormState extends State { Icons.delete_outline_rounded, )) ], - ), + ) ], )); } subformColumn.add(Padding( - padding: EdgeInsets.only( - bottom: values[fieldKey].length > 0 ? 24 : 0, top: 8), + padding: const EdgeInsets.only(bottom: 0, top: 8), child: Row( children: [ Expanded( @@ -591,9 +592,6 @@ class _GeneratedFormState extends State { ], ), )); - if (values[fieldKey].length > 0) { - subformColumn.add(const Divider()); - } formInputs[r][e] = Column(children: subformColumn); } } diff --git a/lib/main.dart b/lib/main.dart index 1e4b1db..df577ff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,12 +19,10 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.15.7'; +const String currentVersion = '0.15.9'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES -const int bgUpdateCheckAlarmId = 666; - List> supportedLocales = const [ MapEntry(Locale('en'), 'English'), MapEntry(Locale('zh'), '简体中文'), diff --git a/lib/mass_app_sources/githubstars.dart b/lib/mass_app_sources/githubstars.dart index b32cc77..4a7ac61 100644 --- a/lib/mass_app_sources/githubstars.dart +++ b/lib/mass_app_sources/githubstars.dart @@ -18,7 +18,7 @@ class GitHubStars implements MassAppUrlSource { Response res = await get( Uri.parse( 'https://api.github.com/users/$username/starred?per_page=100&page=$page'), - headers: await GitHub().getRequestHeaders()); + headers: await GitHub().getRequestHeaders({})); if (res.statusCode == 200) { Map> urlsWithDescriptions = {}; for (var e in (jsonDecode(res.body) as List)) { diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 6c4b9a3..41deae2 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -59,7 +59,9 @@ class AddAppPageState extends State { if (updateUrlInput) { urlInputKey++; } - var prevHost = pickedSource?.host; + var prevHost = pickedSource?.hosts.isNotEmpty == true + ? pickedSource?.hosts[0] + : null; try { var naturalSource = valid ? sourceProvider.getSource(userInput) : null; @@ -77,7 +79,7 @@ class AddAppPageState extends State { overrideSource: pickedSourceOverride) : null; if (pickedSource.runtimeType != source.runtimeType || - (prevHost != null && prevHost != source?.host)) { + (prevHost != null && prevHost != source?.hosts[0])) { pickedSource = source; additionalSettings = source != null ? getDefaultValuesFromFormItems( @@ -508,16 +510,16 @@ class AddAppPageState extends State { height: 16, ), ...sourceProvider.sources.map((e) => GestureDetector( - onTap: e.host != null + onTap: e.hosts.isNotEmpty ? () { - launchUrlString('https://${e.host}', + launchUrlString('https://${e.hosts[0]}', mode: LaunchMode.externalApplication); } : null, child: Text( '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', style: TextStyle( - decoration: e.host != null + decoration: e.hosts.isNotEmpty ? TextDecoration.underline : TextDecoration.none, fontStyle: FontStyle.italic), diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 2605089..18a61dd 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -199,10 +199,11 @@ class _ImportExportPageState extends State { ...source.searchQuerySettingFormItems.map((e) => [e]), [ GeneratedFormTextField('url', - label: source.host != null + label: source.hosts.isNotEmpty ? tr('overrideSource') : plural('url', 1).substring(2), - defaultValue: source.host ?? '', + defaultValue: + source.hosts.isNotEmpty ? source.hosts[0] : '', required: true) ], ], @@ -212,7 +213,7 @@ class _ImportExportPageState extends State { setState(() { importInProgress = true; }); - if (values['url'] != source.host) { + if (values['url'] != source.hosts[0]) { source = sourceProvider.getSource(values['url'], overrideSource: source.runtimeType.toString()); } diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 3a53819..071947d 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -326,13 +326,15 @@ class AppsProvider with ChangeNotifier { AppSource source = SourceProvider() .getSource(app.url, overrideSource: app.overrideSource); String downloadUrl = await source.apkUrlPrefetchModifier( - app.apkUrls[app.preferredApkIndex].value, app.url); + app.apkUrls[app.preferredApkIndex].value, + app.url, + app.additionalSettings); var notif = DownloadNotification(app.finalName, 100); notificationsProvider?.cancel(notif.id); int? prevProg; var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; - var headers = await source.getRequestHeaders( - additionalSettings: app.additionalSettings, forAPKDownload: true); + var headers = await source.getRequestHeaders(app.additionalSettings, + forAPKDownload: true); var downloadedFile = await downloadFileWithRetry( downloadUrl, fileNameNoExt, headers: headers, (double? progress) { @@ -796,13 +798,17 @@ class AppsProvider with ChangeNotifier { SourceProvider() .getSource(app.app.url, overrideSource: app.app.overrideSource) .naiveStandardVersionDetection; + String? realInstalledVersion = + app.app.additionalSettings['useVersionCodeAsOSVersion'] == true + ? app.installedInfo?.versionCode.toString() + : app.installedInfo?.versionName; return app.app.additionalSettings['trackOnly'] != true && app.app.additionalSettings['versionDetection'] != 'releaseDateAsVersion' && - app.installedInfo?.versionName != null && + realInstalledVersion != null && app.app.installedVersion != null && - (reconcileVersionDifferences(app.installedInfo!.versionName!, - app.app.installedVersion!) != + (reconcileVersionDifferences( + realInstalledVersion, app.app.installedVersion!) != null || naiveStandardVersionDetection); } @@ -821,30 +827,33 @@ class AppsProvider with ChangeNotifier { SourceProvider() .getSource(app.url, overrideSource: app.overrideSource) .naiveStandardVersionDetection; + String? realInstalledVersion = + app.additionalSettings['useVersionCodeAsOSVersion'] == true + ? installedInfo?.versionCode.toString() + : installedInfo?.versionName; // FIRST, COMPARE THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE ONE IS NULL if (installedInfo == null && app.installedVersion != null && !trackOnly) { // App says it's installed but isn't really (and isn't track only) - set to not installed app.installedVersion = null; modded = true; - } else if (installedInfo?.versionName != null && - app.installedVersion == null) { - // App says it's not installed but really is - set to installed and use real package versionName - app.installedVersion = installedInfo!.versionName; + } else if (realInstalledVersion != null && app.installedVersion == null) { + // App says it's not installed but really is - set to installed and use real package versionName (or versionCode if chosen) + app.installedVersion = realInstalledVersion; modded = true; } // SECOND, RECONCILE DIFFERENCES BETWEEN THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE NEITHER IS NULL - if (installedInfo?.versionName != null && - installedInfo!.versionName != app.installedVersion && + if (realInstalledVersion != null && + realInstalledVersion != app.installedVersion && versionDetectionIsStandard) { // App's reported version and real version don't match (and it uses standard version detection) // If they share a standard format (and are still different under it), update the reported version accordingly var correctedInstalledVersion = reconcileVersionDifferences( - installedInfo.versionName!, app.installedVersion!); + realInstalledVersion, app.installedVersion!); if (correctedInstalledVersion?.key == false) { app.installedVersion = correctedInstalledVersion!.value; modded = true; } else if (naiveStandardVersionDetection) { - app.installedVersion = installedInfo.versionName; + app.installedVersion = realInstalledVersion; modded = true; } } @@ -1289,8 +1298,11 @@ class AppsProvider with ChangeNotifier { await Future.delayed(const Duration(microseconds: 1)); } for (App a in importedApps) { + var installedInfo = await getInstalledInfo(a.id, printErr: false); a.installedVersion = - (await getInstalledInfo(a.id, printErr: false))?.versionName; + a.additionalSettings['useVersionCodeAsOSVersion'] == true + ? installedInfo?.versionCode.toString() + : installedInfo?.versionName; } await saveApps(importedApps, onlyIfExists: false); notifyListeners(); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 3935f9c..c649fb7 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -14,7 +14,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_storage/shared_storage.dart' as saf; -String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; +String obtainiumTempId = 'imranr98_obtainium_${GitHub().hosts[0]}'; String obtainiumId = 'dev.imranr.obtainium'; enum InstallMethodSettings { normal, shizuku, root } diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index aef2e00..57ea4dd 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -19,7 +19,6 @@ import 'package:obtainium/app_sources/huaweiappgallery.dart'; import 'package:obtainium/app_sources/izzyondroid.dart'; import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/app_sources/jenkins.dart'; -import 'package:obtainium/app_sources/mullvad.dart'; import 'package:obtainium/app_sources/neutroncode.dart'; import 'package:obtainium/app_sources/signal.dart'; import 'package:obtainium/app_sources/sourceforge.dart'; @@ -366,8 +365,12 @@ List> getApkUrlsFromUrls(List urls) => return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e); }).toList(); +getSourceRegex(List hosts) { + return '(${hosts.join('|').replaceAll('.', '\\.')})'; +} + abstract class AppSource { - String? host; + List hosts = []; bool hostChanged = false; late String name; bool enforceTrackOnly = false; @@ -413,8 +416,8 @@ abstract class AppSource { } Future?> getRequestHeaders( - {Map additionalSettings = const {}, - bool forAPKDownload = false}) async { + Map additionalSettings, + {bool forAPKDownload = false}) async { return null; } @@ -422,12 +425,10 @@ abstract class AppSource { return app; } - Future sourceRequest(String url, - {bool followRedirects = true, - Map additionalSettings = - const {}}) async { - var requestHeaders = - await getRequestHeaders(additionalSettings: additionalSettings); + Future sourceRequest( + String url, Map additionalSettings, + {bool followRedirects = true}) async { + var requestHeaders = await getRequestHeaders(additionalSettings); if (requestHeaders != null || followRedirects == false) { var req = Request('GET', Uri.parse(url)); req.followRedirects = followRedirects; @@ -484,6 +485,10 @@ abstract class AppSource { label: tr('versionDetection'), defaultValue: 'standardVersionDetection') ], + [ + GeneratedFormSwitch('useVersionCodeAsOSVersion', + label: tr('useVersionCodeAsOSVersion'), defaultValue: false) + ], [ GeneratedFormTextField('apkFilterRegEx', label: tr('filterAPKsByRegEx'), @@ -544,8 +549,8 @@ abstract class AppSource { return null; } - Future apkUrlPrefetchModifier( - String apkUrl, String standardUrl) async { + Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, + Map additionalSettings) async { return apkUrl; } @@ -676,7 +681,6 @@ class SourceProvider { APKMirror(), HuaweiAppGallery(), Jenkins(), - Mullvad(), Signal(), VLC(), WhatsApp(), @@ -697,14 +701,14 @@ class SourceProvider { throw UnsupportedURLError(); } var res = srcs.first; - res.host = Uri.parse(url).host; + res.hosts = [Uri.parse(url).host]; res.hostChanged = true; return srcs.first; } AppSource? source; - for (var s in sources.where((element) => element.host != null)) { + for (var s in sources.where((element) => element.hosts.isNotEmpty)) { if (RegExp( - '://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}${s.host}(/|\\z)?') + '://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})(/|\\z)?') .hasMatch(url)) { source = s; break; @@ -712,7 +716,7 @@ class SourceProvider { } if (source == null) { for (var s in sources.where( - (element) => element.host == null && !element.neverAutoSelect)) { + (element) => element.hosts.isEmpty && !element.neverAutoSelect)) { try { s.sourceSpecificStandardizeURL(url); source = s; diff --git a/pubspec.lock b/pubspec.lock index 894a4a9..8743138 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,10 +22,10 @@ packages: dependency: "direct main" description: name: android_package_manager - sha256: b873fe5856f7c442aca9751dac05d117285be9e4de08eb15d1ffb811fd1b688d + sha256: e52ca607b9f19f95d5dae4211ed8fa93e67093f22ac570db47489c5bca512940 url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.0" animations: dependency: "direct main" description: @@ -70,10 +70,10 @@ packages: dependency: "direct main" description: name: background_fetch - sha256: f70b28a0f7a3156195e9742229696f004ea3bf10f74039b7bf4c78a74fbda8a4 + sha256: "34550cf9b383e5a1844e7d22119aa500508c7df9421fa967c9fb4430d6cb2878" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" boolean_selector: dependency: transitive description: @@ -258,6 +258,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -506,10 +514,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: @@ -538,10 +546,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -690,10 +698,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: @@ -823,26 +831,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 + sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.3" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: c0766a55ab42cefaa728cabc951e82919ab41a3a4fee0aaa96176ca82da8cc51 + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.2" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "46b81e3109cbb2d6b81702ad3077540789a3e74e22795eb9f0b7d494dbaa72ea" + sha256: cdb7b6da34483f9b2c9f8b2b29bc468fa7271d92e2021607ca0c4d3bcb04cdd4 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.3" url_launcher_linux: dependency: transitive description: @@ -863,10 +871,10 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" url_launcher_web: dependency: transitive description: @@ -887,10 +895,10 @@ packages: dependency: transitive description: name: uuid - sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.3.3" vector_math: dependency: transitive description: @@ -911,10 +919,10 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: "60e23976834e995c404c0b21d3b9db37ecd77d3303ef74f8b8d7a7b19947fc04" + sha256: "71e1bfaef41016c8d5954291df5e9f8c6172f1f6ff3af01b5656456ddb11f94c" url: "https://pub.dev" source: hosted - version: "4.4.3" + version: "4.4.4" webview_flutter_android: dependency: transitive description: @@ -927,10 +935,10 @@ packages: dependency: transitive description: name: webview_flutter_platform_interface - sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943 + sha256: "80b40ae4fb959957eef9fa8970b6c9accda9f49fc45c2b75154696a8e8996cfe" url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.9.1" webview_flutter_wkwebview: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5936dee..287afec 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.15.7+243 # When changing this, update the tag in main() accordingly +version: 0.15.9+245 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0' @@ -55,7 +55,7 @@ dependencies: git: url: https://github.com/ImranR98/android_package_installer ref: main - android_package_manager: ^0.6.0 + android_package_manager: ^0.7.0 share_plus: ^7.0.0 sqflite: ^2.2.0+3 easy_localization: ^3.0.1