diff --git a/assets/translations/bs.json b/assets/translations/bs.json index a3e935a..62b5b32 100644 --- a/assets/translations/bs.json +++ b/assets/translations/bs.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Onemogući detekciju verzije", "noVersionDetectionExplanation": "Ova opcija bi se trebala koristiti samo za aplikacije gdje detekcija verzije ne radi ispravno.", "downloadingX": "Preuzimanje {}", + "downloadX": "Download {}", + "downloadedX": "Downloaded {}", + "releaseAsset": "Release Asset", "downloadNotifDescription": "Obavještava korisnika o napretku u preuzimanju aplikacije", "noAPKFound": "APK nije pronađen", "noVersionDetection": "Nema detekcije verzije", diff --git a/assets/translations/cs.json b/assets/translations/cs.json index 33fa372..52d23b4 100644 --- a/assets/translations/cs.json +++ b/assets/translations/cs.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Deaktivovat detekci verze", "noVersionDetectionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně.", "downloadingX": "Stáhnout {}", + "downloadX": "Stáhnout {}", + "downloadedX": "Staženo {}", + "releaseAsset": "Vydání aktiva", "downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace", "noAPKFound": "Žádná APK nebyla nalezena", "noVersionDetection": "Žádná detekce verze", diff --git a/assets/translations/de.json b/assets/translations/de.json index 4d711bf..58f6873 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Versionsermittlung deaktivieren", "noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.", "downloadingX": "Lade {} herunter", + "downloadX": "Herunterladen {}", + "downloadedX": "Heruntergeladen {}", + "releaseAsset": "Asset freigeben", "downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App", "noAPKFound": "Keine APK gefunden", "noVersionDetection": "Keine Versionserkennung", diff --git a/assets/translations/en.json b/assets/translations/en.json index 8794475..167fa06 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Disable Version Detection", "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", "downloadingX": "Downloading {}", + "downloadX": "Download {}", + "downloadedX": "Downloaded {}", + "releaseAsset": "Release Asset", "downloadNotifDescription": "Notifies the user of the progress in downloading an App", "noAPKFound": "No APK found", "noVersionDetection": "No version detection", diff --git a/assets/translations/es.json b/assets/translations/es.json index 873a1d4..e0020fb 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Desactivar la detección de versiones", "noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda que no funcionar correctamente.", "downloadingX": "Descargando {}", + "downloadX": "Descargar {}", + "downloadedX": "Descargado {}", + "releaseAsset": "Liberar activos", "downloadNotifDescription": "Notifica al usuario del progreso de descarga de una aplicación", "noAPKFound": "No se encontró el paquete de instalación APK", "noVersionDetection": "Sin detección de versiones", diff --git a/assets/translations/fa.json b/assets/translations/fa.json index 10731d6..3b785eb 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -183,6 +183,9 @@ "disableVersionDetection": "غیرفعال کردن تشخیص نسخه", "noVersionDetectionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند.", "downloadingX": "در حال دانلود {}", + "downloadX": "Download {}", + "downloadedX": "Downloaded {}", + "releaseAsset": "Release Asset", "downloadNotifDescription": "کاربر را از پیشرفت دانلود یک برنامه مطلع می کند", "noAPKFound": "APK پیدا نشد فایل", "noVersionDetection": "بدون تشخیص نسخه", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 61be34e..9e60af6 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Désactiver la détection de version", "noVersionDetectionExplanation": "Cette option ne doit être utilisée que pour les applications où la détection de version ne fonctionne pas correctement.", "downloadingX": "Téléchargement {}", + "downloadX": "Télécharger {}", + "downloadedX": "Téléchargé {}", + "releaseAsset": "Actif libéré", "downloadNotifDescription": "Avertit l'utilisateur de la progression du téléchargement d'une application", "noAPKFound": "Aucun APK trouvé", "noVersionDetection": "Pas de détection de version", diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 5e9c669..a5dc4bc 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Verzió érzékelés letiltása", "noVersionDetectionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzióérzékelés nem működik megfelelően.", "downloadingX": "{} letöltés", + "downloadX": "Letöltés {}", + "downloadedX": "Letöltés {}", + "releaseAsset": "Release Asset", "downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról", "noAPKFound": "Nem található APK", "noVersionDetection": "Nincs verzió érzékelés", diff --git a/assets/translations/it.json b/assets/translations/it.json index e6aa2a4..e0b6af0 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Disattiva il rilevamento della versione", "noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le app la cui versione non viene rilevata correttamente.", "downloadingX": "Scaricamento di {} in corso", + "downloadX": "Scarica {}", + "downloadedX": "Scaricato {}", + "releaseAsset": "Rilascio Asset", "downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'app", "noAPKFound": "Nessun APK trovato", "noVersionDetection": "Disattiva rilevamento di versione", diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 171167e..dba7734 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -183,6 +183,9 @@ "disableVersionDetection": "バージョン検出を無効にする", "noVersionDetectionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリにのみ使用する必要があります。", "downloadingX": "{} をダウンロード中", + "downloadX": "ダウンロード", + "downloadedX": "ダウンロード", + "releaseAsset": "リリース資産", "downloadNotifDescription": "アプリのダウンロード状況を通知する", "noAPKFound": "APKが見つかりません", "noVersionDetection": "バージョン検出を行わない", diff --git a/assets/translations/nl.json b/assets/translations/nl.json index 845676d..417ec3b 100644 --- a/assets/translations/nl.json +++ b/assets/translations/nl.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Versieherkenning uitschakelen", "noVersionDetectionExplanation": "Deze optie moet alleen worden gebruikt voor apps waar versieherkenning niet correct werkt.", "downloadingX": "Downloaden {}", + "downloadX": "Downloaden", + "downloadedX": "Gedownload {}", + "releaseAsset": "Release Activa", "downloadNotifDescription": "Stelt de gebruiker op de hoogte van de voortgang bij het downloaden van een app", "noAPKFound": "Geen APK gevonden", "noVersionDetection": "Geen versieherkenning", diff --git a/assets/translations/pl.json b/assets/translations/pl.json index c017f93..3886a0c 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Wyłącz wykrywanie wersji", "noVersionDetectionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie.", "downloadingX": "Pobieranie {}", + "downloadX": "Pobierz {}", + "downloadedX": "Pobrano {}", + "releaseAsset": "Release Asset", "downloadNotifDescription": "Informuje o postępach w pobieraniu aplikacji", "noAPKFound": "Nie znaleziono pakietu APK", "noVersionDetection": "Bez wykrywania wersji", diff --git a/assets/translations/pt.json b/assets/translations/pt.json index c2281d9..a8b170f 100644 --- a/assets/translations/pt.json +++ b/assets/translations/pt.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Desativar detecção de versão", "noVersionDetectionExplanation": "Essa opção deve apenas ser usada por aplicativos onde a detecção de versão não funciona corretamente.", "downloadingX": "Baixando {}", + "downloadX": "Descarregar {}", + "downloadedX": "Descarregado {}", + "releaseAsset": "Libertação de activos", "downloadNotifDescription": "Notifica o usuário o progresso do download de um aplicativo", "noAPKFound": "APK não encontrado", "noVersionDetection": "Sem detecção de versão", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 1c6ec24..0dffae4 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Отключить обнаружение версии", "noVersionDetectionExplanation": "Эта настройка должна использоваться только для приложений, где обнаружение версии не работает корректно", "downloadingX": "Загрузка {}", + "downloadX": "Скачать {}", + "downloadedX": "Загружено {}", + "releaseAsset": "Освобождение актива", "downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения", "noAPKFound": "APK не найден", "noVersionDetection": "Обнаружение версий отключено", diff --git a/assets/translations/sv.json b/assets/translations/sv.json index 5713536..f77c5d3 100644 --- a/assets/translations/sv.json +++ b/assets/translations/sv.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Inaktivera versionsdetektering", "noVersionDetectionExplanation": "Det här alternativet bör endast användas för appar där versionsidentifiering inte fungerar korrekt.", "downloadingX": "Laddar ner {}", + "downloadX": "Ladda ner {}", + "downloadedX": "Nedladdad {}", + "releaseAsset": "Frigör tillgång", "downloadNotifDescription": "Meddelar användaren om framstegen med att ladda ner en app", "noAPKFound": "Ingen APK funnen", "noVersionDetection": "Ingen versiondetektering", diff --git a/assets/translations/tr.json b/assets/translations/tr.json index 176663a..db1df8e 100644 --- a/assets/translations/tr.json +++ b/assets/translations/tr.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Sürüm Algılama Devre Dışı", "noVersionDetectionExplanation": "Bu seçenek, sürüm algılamanın doğru çalışmadığı uygulamalar için kullanılmalıdır.", "downloadingX": "{} İndiriliyor", + "downloadX": "İndir {}", + "downloadedX": "İndirildi {}", + "releaseAsset": "Varlık Serbest Bırakma", "downloadNotifDescription": "Bir uygulamanın indirme sürecinde ilerlemeyi bildiren bir bildirim", "noAPKFound": "APK bulunamadı", "noVersionDetection": "Sürüm Algılanamıyor", diff --git a/assets/translations/uk.json b/assets/translations/uk.json index 327d5fa..b2de35c 100644 --- a/assets/translations/uk.json +++ b/assets/translations/uk.json @@ -183,6 +183,9 @@ "disableVersionDetection": "Вимкнути визначення версії", "noVersionDetectionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно.", "downloadingX": "Завантаження {}", + "downloadX": "Завантажити {}", + "downloadedX": "Завантажено {}", + "releaseAsset": "Звільнити актив", "downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку", "noAPKFound": "APK не знайдено", "noVersionDetection": "Визначення версії відключено", diff --git a/assets/translations/vi.json b/assets/translations/vi.json index 951f209..4259cf0 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": "GitHub Token (Tăng tốc độ, giới hạn)", + "githubPATLabel": "Token truy cập cá nhân GitHub (Cải thiện 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", @@ -183,6 +183,9 @@ "disableVersionDetection": "Tắt tính năng phát hiện phiên bản", "noVersionDetectionExplanation": "Chỉ nên sử dụng tùy chọn này cho Ứng dụng mà tính năng phát hiện phiên bản không hoạt động chính xác.", "downloadingX": "Đang tải xuống {}", + "downloadX": "Download {}", + "downloadedX": "Downloaded {}", + "releaseAsset": "Release Asset", "downloadNotifDescription": "Thông báo cho người dùng về tiến trình tải xuống Ứng dụng", "noAPKFound": "Không tìm thấy APK", "noVersionDetection": "Không phát hiện phiên bản", @@ -218,7 +221,7 @@ "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": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách", - "gitlabPATLabel": "GitLab Token", + "gitlabPATLabel": "Token truy cập cá nhân GitLab", "about": "Giới thiệu", "requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Thiết đặt)", "checkOnStart": "Kiểm tra các bản cập nhật khi khởi động", @@ -299,8 +302,8 @@ "note": "Ghi chú", "selfHostedNote": "Trình đơn thả xuống \"{}\" có thể được dùng để tiếp cận các phiên bản tự lưu trữ/tùy chỉnh của bất kỳ nguồn nào.", "badDownload": "Không thể phân tích cú pháp APK (tải xuống một phần hoặc không tương thích)", - "beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)", - "appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.", + "beforeNewInstallsShareToAppVerifier": "Chia sẻ ứng dụng mới với AppVerifier (nếu có)", + "appVerifierInstructionToast": "Chia sẻ lên AppVerifier, sau đó quay lại đây khi sẵn sàng.", "removeAppQuestion": { "one": "Gỡ ứng dụng?", "other": "Gỡ ứng dụng?" diff --git a/assets/translations/zh.json b/assets/translations/zh.json index ed1482c..8ee9682 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -183,6 +183,9 @@ "disableVersionDetection": "禁用版本检测", "noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。", "downloadingX": "正在下载“{}”", + "downloadX": "下载 {}", + "downloadedX": "下载 {}", + "releaseAsset": "释放资产", "downloadNotifDescription": "提示应用的下载进度", "noAPKFound": "未找到 APK 文件", "noVersionDetection": "禁用版本检测", diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index f6e2615..c7aa120 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -271,17 +271,14 @@ class GitHub extends AppSource { } } - List> getReleaseAPKUrls(dynamic release) => - (release['assets'] as List?) - ?.map((e) { - return (e['name'] != null) && - ((e['url'] ?? e['browser_download_url']) != null) - ? MapEntry(e['name'] as String, - (e['url'] ?? e['browser_download_url']) as String) - : const MapEntry('', ''); - }) - .where((element) => element.key.toLowerCase().endsWith('.apk')) - .toList() ?? + List> getReleaseAssetUrls(dynamic release) => + (release['assets'] as List?)?.map((e) { + return (e['name'] != null) && + ((e['url'] ?? e['browser_download_url']) != null) + ? MapEntry(e['name'] as String, + (e['url'] ?? e['browser_download_url']) as String) + : const MapEntry('', ''); + }).toList() ?? []; DateTime? getPublishDateFromRelease(dynamic rel) => @@ -383,7 +380,11 @@ class GitHub extends AppSource { .hasMatch(((releases[i]['body'] as String?) ?? '').trim())) { continue; } - var apkUrls = getReleaseAPKUrls(releases[i]); + var allAssetUrls = getReleaseAssetUrls(releases[i]); + List> apkUrls = allAssetUrls + .where((element) => element.key.toLowerCase().endsWith('.apk')) + .toList(); + apkUrls = filterApks(apkUrls, additionalSettings['apkFilterRegEx'], additionalSettings['invertAPKFilter']); if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) { @@ -391,12 +392,25 @@ class GitHub extends AppSource { } targetRelease = releases[i]; targetRelease['apkUrls'] = apkUrls; + targetRelease['version'] = + targetRelease['tag_name'] ?? targetRelease['name']; + if (targetRelease['tarball_url'] != null) { + allAssetUrls.add(MapEntry( + (targetRelease['version'] ?? 'source') + '.tar.gz', + targetRelease['tarball_url'])); + } + if (targetRelease['zipball_url'] != null) { + allAssetUrls.add(MapEntry( + (targetRelease['version'] ?? 'source') + '.zip', + targetRelease['zipball_url'])); + } + targetRelease['allAssetUrls'] = allAssetUrls; break; } if (targetRelease == null) { throw NoReleasesError(); } - String? version = targetRelease['tag_name'] ?? targetRelease['name']; + String? version = targetRelease['version']; DateTime? releaseDate = getReleaseDateFromRelease( targetRelease, useLatestAssetDateAsReleaseDate); if (version == null) { @@ -408,7 +422,9 @@ class GitHub extends AppSource { targetRelease['apkUrls'] as List>, getAppNames(standardUrl), releaseDate: releaseDate, - changeLog: changeLog.isEmpty ? null : changeLog); + changeLog: changeLog.isEmpty ? null : changeLog, + allAssetUrls: + targetRelease['allAssetUrls'] as List>); } else { if (onHttpErrorCode != null) { onHttpErrorCode(res); diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 53e0689..49f7246 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -180,6 +180,16 @@ class GitLab extends AppSource { throw NoAPKError(); } - return apkDetailsList.first; + finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) { + if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+\$') + .hasMatch(apkUrl.value)) { + return MapEntry( + apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/')); + } else { + return apkUrl; + } + }).toList(); + + return finalResult; } } diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index e280519..5dc17b0 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -155,7 +155,8 @@ class AddAppPageState extends State { // Only download the APK here if you need to for the package ID if (isTempId(app) && app.additionalSettings['trackOnly'] != true) { // ignore: use_build_context_synchronously - var apkUrl = await appsProvider.confirmApkUrl(app, context); + var apkUrl = + await appsProvider.confirmAppFileUrl(app, context, false); if (apkUrl == null) { throw ObtainiumError(tr('cancelled')); } diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 6970cb3..a0ff893 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -158,6 +158,29 @@ class _AppPageState extends State { textAlign: TextAlign.center, style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), ), + if (app?.app.apkUrls.isNotEmpty == true || + app?.app.otherAssetUrls.isNotEmpty == true) + GestureDetector( + onTap: app?.app == null || updating + ? null + : () async { + try { + await appsProvider + .downloadAppAssets([app!.app.id], context); + } catch (e) { + showError(e, context); + } + }, + child: Text( + tr('downloadX', args: [tr('releaseAsset').toLowerCase()]), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + decoration: + changeLogFn != null ? TextDecoration.underline : null, + fontStyle: changeLogFn != null ? FontStyle.italic : null, + ), + ), + ), const SizedBox( height: 48, ), diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 52125ed..7da0e2b 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -854,69 +854,78 @@ class AppsPageState extends State { scrollable: true, content: Padding( padding: const EdgeInsets.only(top: 6), - child: Row( + child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - IconButton( + TextButton( + onPressed: pinSelectedApps, + child: Text(selectedApps + .where((element) => element.pinned) + .isEmpty + ? tr('pinToTop') + : tr('unpinFromTop'))), + const Divider(), + TextButton( + onPressed: () { + String urls = ''; + for (var a in selectedApps) { + urls += '${a.url}\n'; + } + urls = urls.substring(0, urls.length - 1); + Share.share(urls, + subject: 'Obtainium - ${tr('appsString')}'); + Navigator.of(context).pop(); + }, + child: Text(tr('shareSelectedAppURLs'))), + const Divider(), + TextButton( + onPressed: selectedAppIds.isEmpty + ? null + : () { + String urls = + '

${tr('customLinkMessage')}:

\n\n
    \n'; + for (var a in selectedApps) { + urls += + '
  • ${a.name}
  • \n'; + } + urls += + '
\n\n

${tr('about')}

'; + Share.share(urls, + subject: + 'Obtainium - ${tr('appsString')}'); + }, + child: Text(tr('shareAppConfigLinks'))), + const Divider(), + TextButton( + onPressed: () { + appsProvider + .downloadAppAssets( + selectedApps.map((e) => e.id).toList(), + globalNavigatorKey.currentContext ?? + context) + .catchError((e) => showError( + e, + globalNavigatorKey.currentContext ?? + context)); + Navigator.of(context).pop(); + }, + child: Text(tr('downloadX', + args: [tr('releaseAsset').toLowerCase()]))), + const Divider(), + TextButton( onPressed: appsProvider.areDownloadsRunning() ? null : showMassMarkDialog, - tooltip: tr('markSelectedAppsUpdated'), - icon: const Icon(Icons.done)), - IconButton( - onPressed: pinSelectedApps, - tooltip: selectedApps - .where((element) => element.pinned) - .isEmpty - ? tr('pinToTop') - : tr('unpinFromTop'), - icon: Icon(selectedApps - .where((element) => element.pinned) - .isEmpty - ? Icons.bookmark_outline_rounded - : Icons.bookmark_remove_outlined), - ), - IconButton( - onPressed: () { - String urls = ''; - for (var a in selectedApps) { - urls += '${a.url}\n'; - } - urls = urls.substring(0, urls.length - 1); - Share.share(urls, - subject: 'Obtainium - ${tr('appsString')}'); - Navigator.of(context).pop(); - }, - tooltip: tr('shareSelectedAppURLs'), - icon: const Icon(Icons.share_rounded), - ), - IconButton( - onPressed: selectedAppIds.isEmpty - ? null - : () { - String urls = - '

${tr('customLinkMessage')}:

\n\n
    \n'; - for (var a in selectedApps) { - urls += - '
  • ${a.name}
  • \n'; - } - urls += - '
\n\n

${tr('about')}

'; - Share.share(urls, - subject: 'Obtainium - ${tr('appsString')}'); - }, - tooltip: tr('shareAppConfigLinks'), - icon: const Icon(Icons.ios_share), - ), + child: Text(tr('markSelectedAppsUpdated'))), ]), ), ); diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 2068d22..ee8364f 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -360,7 +360,7 @@ class AppsProvider with ChangeNotifier { foregroundStream = FGBGEvents.stream.asBroadcastStream(); foregroundSubscription = foregroundStream?.listen((event) async { isForeground = event == FGBGType.foreground; - if (isForeground) await loadApps(); + if (isForeground) loadApps(); }); () async { await settingsProvider.initializeSettings(); @@ -698,23 +698,28 @@ class AppsProvider with ChangeNotifier { await intent.launch(); } - Future?> confirmApkUrl( - App app, BuildContext? context) async { + Future?> confirmAppFileUrl( + App app, BuildContext? context, bool pickAnyAsset) async { + var urlsToSelectFrom = app.apkUrls; + if (pickAnyAsset) { + urlsToSelectFrom = [...urlsToSelectFrom, ...app.otherAssetUrls]; + } // If the App has more than one APK, the user should pick one (if context provided) - MapEntry? apkUrl = - app.apkUrls[app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0]; + MapEntry? appFileUrl = urlsToSelectFrom[ + app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0]; // get device supported architecture List archs = (await DeviceInfoPlugin().androidInfo).supportedAbis; - if (app.apkUrls.length > 1 && context != null) { + if (urlsToSelectFrom.length > 1 && context != null) { // ignore: use_build_context_synchronously - apkUrl = await showDialog( + appFileUrl = await showDialog( context: context, builder: (BuildContext ctx) { - return APKPicker( + return AppFilePicker( app: app, - initVal: apkUrl, + initVal: appFileUrl, archs: archs, + pickAnyAsset: pickAnyAsset, ); }); } @@ -724,8 +729,8 @@ class AppsProvider with ChangeNotifier { } // If the picked APK comes from an origin different from the source, get user confirmation (if context provided) - if (apkUrl != null && - getHost(apkUrl.value) != getHost(app.url) && + if (appFileUrl != null && + getHost(appFileUrl.value) != getHost(app.url) && context != null) { // ignore: use_build_context_synchronously if (!(settingsProvider.hideAPKOriginWarning) && @@ -734,13 +739,13 @@ class AppsProvider with ChangeNotifier { context: context, builder: (BuildContext ctx) { return APKOriginWarningDialog( - sourceUrl: app.url, apkUrl: apkUrl!.value); + sourceUrl: app.url, apkUrl: appFileUrl!.value); }) != true) { - apkUrl = null; + appFileUrl = null; } } - return apkUrl; + return appFileUrl; } // Given a list of AppIds, uses stored info about the apps to download APKs and install them @@ -767,7 +772,7 @@ class AppsProvider with ChangeNotifier { var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true; if (!trackOnly) { // ignore: use_build_context_synchronously - apkUrl = await confirmApkUrl(apps[id]!.app, context); + apkUrl = await confirmAppFileUrl(apps[id]!.app, context, false); } if (apkUrl != null) { int urlInd = apps[id]! @@ -898,6 +903,85 @@ class AppsProvider with ChangeNotifier { return installedIds; } + Future> downloadAppAssets( + List appIds, BuildContext context, + {bool forceParallelDownloads = false}) async { + NotificationsProvider notificationsProvider = + context.read(); + List, App>> filesToDownload = []; + for (var id in appIds) { + if (apps[id] == null) { + throw ObtainiumError(tr('appNotFound')); + } + MapEntry? fileUrl; + if (apps[id]!.app.apkUrls.isNotEmpty || + apps[id]!.app.otherAssetUrls.isNotEmpty) { + // ignore: use_build_context_synchronously + fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true); + } + if (fileUrl != null) { + filesToDownload.add(MapEntry(fileUrl, apps[id]!.app)); + } + } + + // Prepare to download+install Apps + MultiAppMultiError errors = MultiAppMultiError(); + List downloadedIds = []; + + Future downloadFn(MapEntry fileUrl, App app) async { + try { + var exportDir = await settingsProvider.getExportDir(); + String downloadPath = '/storage/emulated/0/Download'; + bool downloadsAccessible = false; + try { + downloadsAccessible = Directory(downloadPath).existsSync(); + } catch (e) { + // + } + if (!downloadsAccessible && exportDir != null) { + downloadPath = exportDir.path; + } + await downloadFile( + fileUrl.value, + fileUrl.key + .split('.') + .reversed + .toList() + .sublist(1) + .reversed + .join('.'), (double? progress) { + notificationsProvider + .notify(DownloadNotification(fileUrl.key, progress?.ceil() ?? 0)); + }, downloadPath, + headers: await SourceProvider() + .getSource(app.url, overrideSource: app.overrideSource) + .getRequestHeaders(app.additionalSettings, + forAPKDownload: + fileUrl.key.endsWith('.apk') ? true : false), + useExisting: false); + notificationsProvider + .notify(DownloadedNotification(fileUrl.key, fileUrl.value)); + } catch (e) { + errors.add(fileUrl.key, e); + } finally { + notificationsProvider.cancel(DownloadNotification(fileUrl.key, 0).id); + } + } + + if (forceParallelDownloads || !settingsProvider.parallelDownloads) { + for (var urlWithApp in filesToDownload) { + await downloadFn(urlWithApp.key, urlWithApp.value); + } + } else { + await Future.wait(filesToDownload + .map((urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value))); + } + if (errors.idsByErrorString.isNotEmpty) { + throw errors; + } + return downloadedIds; + } + Future getAppsDir() async { Directory appsDir = Directory('${(await getExternalStorageDirectory())!.path}/app_data'); @@ -1469,38 +1553,49 @@ class AppsProvider with ChangeNotifier { } } -class APKPicker extends StatefulWidget { - const APKPicker({super.key, required this.app, this.initVal, this.archs}); +class AppFilePicker extends StatefulWidget { + const AppFilePicker( + {super.key, + required this.app, + this.initVal, + this.archs, + this.pickAnyAsset = false}); final App app; final MapEntry? initVal; final List? archs; + final bool pickAnyAsset; @override - State createState() => _APKPickerState(); + State createState() => _AppFilePickerState(); } -class _APKPickerState extends State { - MapEntry? apkUrl; +class _AppFilePickerState extends State { + MapEntry? fileUrl; @override Widget build(BuildContext context) { - apkUrl ??= widget.initVal; + fileUrl ??= widget.initVal; + var urlsToSelectFrom = widget.app.apkUrls; + if (widget.pickAnyAsset) { + urlsToSelectFrom = [...urlsToSelectFrom, ...widget.app.otherAssetUrls]; + } return AlertDialog( scrollable: true, - title: Text(tr('pickAnAPK')), + title: Text(widget.pickAnyAsset + ? tr('selectX', args: [tr('releaseAsset').toLowerCase()]) + : tr('pickAnAPK')), content: Column(children: [ Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])), const SizedBox(height: 16), - ...widget.app.apkUrls.map( + ...urlsToSelectFrom.map( (u) => RadioListTile( title: Text(u.key), value: u.value, - groupValue: apkUrl!.value, + groupValue: fileUrl!.value, onChanged: (String? val) { setState(() { - apkUrl = - widget.app.apkUrls.where((e) => e.value == val).first; + fileUrl = urlsToSelectFrom.where((e) => e.value == val).first; }); }), ), @@ -1527,7 +1622,7 @@ class _APKPickerState extends State { TextButton( onPressed: () { HapticFeedback.selectionClick(); - Navigator.of(context).pop(apkUrl); + Navigator.of(context).pop(fileUrl); }, child: Text(tr('continue'))) ], diff --git a/lib/providers/notifications_provider.dart b/lib/providers/notifications_provider.dart index 851fb18..898126b 100644 --- a/lib/providers/notifications_provider.dart +++ b/lib/providers/notifications_provider.dart @@ -120,6 +120,18 @@ class DownloadNotification extends ObtainiumNotification { progPercent: progPercent); } +class DownloadedNotification extends ObtainiumNotification { + DownloadedNotification(String fileName, String downloadUrl) + : super( + downloadUrl.hashCode, + tr('downloadedX', args: [fileName]), + '', + 'FILE_DOWNLOADED', + tr('downloadedXNotifChannel', args: [tr('app')]), + tr('downloadedX', args: [tr('app')]), + Importance.defaultImportance); +} + final completeInstallationNotification = ObtainiumNotification( 1, tr('completeAppInstallation'), diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 7761f61..d69b33e 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -47,9 +47,10 @@ class APKDetails { late AppNames names; late DateTime? releaseDate; late String? changeLog; + late List> allAssetUrls; APKDetails(this.version, this.apkUrls, this.names, - {this.releaseDate, this.changeLog}); + {this.releaseDate, this.changeLog, this.allAssetUrls = const []}); } stringMapListTo2DList(List> mapList) => @@ -223,6 +224,7 @@ class App { String? installedVersion; late String latestVersion; List> apkUrls = []; + List> otherAssetUrls = []; late int preferredApkIndex; late Map additionalSettings; late DateTime? lastUpdateCheck; @@ -248,7 +250,8 @@ class App { this.releaseDate, this.changeLog, this.overrideSource, - this.allowIdChange = false}); + this.allowIdChange = false, + this.otherAssetUrls = const []}); @override String toString() { @@ -280,41 +283,44 @@ class App { changeLog: changeLog, releaseDate: releaseDate, overrideSource: overrideSource, - allowIdChange: allowIdChange); + allowIdChange: allowIdChange, + otherAssetUrls: otherAssetUrls); factory App.fromJson(Map json) { json = appJSONCompatibilityModifiers(json); return App( - json['id'] as String, - json['url'] as String, - json['author'] as String, - json['name'] as String, - json['installedVersion'] == null - ? null - : json['installedVersion'] as String, - (json['latestVersion'] ?? tr('unknown')) as String, - assumed2DlistToStringMapList(jsonDecode( - (json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))), - (json['preferredApkIndex'] ?? -1) as int, - jsonDecode(json['additionalSettings']) as Map, - json['lastUpdateCheck'] == null - ? null - : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), - json['pinned'] ?? false, - categories: json['categories'] != null - ? (json['categories'] as List) - .map((e) => e.toString()) - .toList() - : json['category'] != null - ? [json['category'] as String] - : [], - releaseDate: json['releaseDate'] == null - ? null - : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']), - changeLog: - json['changeLog'] == null ? null : json['changeLog'] as String, - overrideSource: json['overrideSource'], - allowIdChange: json['allowIdChange'] ?? false); + json['id'] as String, + json['url'] as String, + json['author'] as String, + json['name'] as String, + json['installedVersion'] == null + ? null + : json['installedVersion'] as String, + (json['latestVersion'] ?? tr('unknown')) as String, + assumed2DlistToStringMapList( + jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))), + (json['preferredApkIndex'] ?? -1) as int, + jsonDecode(json['additionalSettings']) as Map, + json['lastUpdateCheck'] == null + ? null + : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), + json['pinned'] ?? false, + categories: json['categories'] != null + ? (json['categories'] as List) + .map((e) => e.toString()) + .toList() + : json['category'] != null + ? [json['category'] as String] + : [], + releaseDate: json['releaseDate'] == null + ? null + : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']), + changeLog: json['changeLog'] == null ? null : json['changeLog'] as String, + overrideSource: json['overrideSource'], + allowIdChange: json['allowIdChange'] ?? false, + otherAssetUrls: assumed2DlistToStringMapList( + jsonDecode((json['otherAssetUrls'] ?? '[]'))), + ); } Map toJson() => { @@ -325,6 +331,7 @@ class App { 'installedVersion': installedVersion, 'latestVersion': latestVersion, 'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)), + 'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)), 'preferredApkIndex': preferredApkIndex, 'additionalSettings': jsonEncode(additionalSettings), 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, @@ -892,8 +899,10 @@ class SourceProvider { allowIdChange: currentApp?.allowIdChange ?? trackOnly || (source.appIdInferIsOptional && - inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install - ); + inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install + otherAssetUrls: apk.allAssetUrls + .where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0) + .toList()); return source.endOfGetAppChanges(finalApp); } diff --git a/pubspec.lock b/pubspec.lock index 262dad8..56163f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -345,10 +345,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "87e11b9df25a42e2db315b8b7a51fae8e66f57a4b2f50ec4b822d0fa155e6b52" + sha256: "31c12de79262b5431c5492e9c89948aa789158435f707d3519a7fdef6af28af7" url: "https://pub.dev" source: hosted - version: "0.6.22" + version: "0.6.22+1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -667,10 +667,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.8.0" provider: dependency: "direct main" description: @@ -784,10 +784,10 @@ packages: dependency: "direct main" description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" sqflite_common: dependency: transitive description: @@ -992,10 +992,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.4.0" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3c52c57..f9bc3e1 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: 1.1.0+2257 +version: 1.1.2+2259 environment: sdk: '>=3.0.0 <4.0.0'