diff --git a/android/app/build.gradle b/android/app/build.gradle index e8d09aa..90500f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -71,9 +71,17 @@ android { } } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } buildTypes { release { - + signingConfig signingConfigs.release } } } diff --git a/assets/translations/bs.json b/assets/translations/bs.json index d3bf65a..eadd9b6 100644 --- a/assets/translations/bs.json +++ b/assets/translations/bs.json @@ -55,7 +55,7 @@ "notInstalled": "Nije instalirano", "estimateInBrackets": "(Procjena)", "selectAll": "Označi sve", - "deselectN": "Poništi odabir {}", + "deselectX": "Poništi odabir {}", "xWillBeRemovedButRemainInstalled": "{} će biti uklonjen iz Obtainiuma, ali će ostati instaliran na uređaju.", "removeSelectedAppsQuestion": "Želite li ukloniti odabrane aplikacije?", "removeSelectedApps": "Ukloni odabrane aplikacije", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Premjesti neinstalirane aplikacije na dno prikaza aplikacija", "gitlabPATLabel": "GitLab token za lični pristup\n(Omogućava pretraživanje i bolje otkrivanje APK-a)", "about": "O nama", - "requiresCredentialsInSettings": "Za ovo su potrebni dodatni akreditivi (u Postavkama)", + "requiresCredentialsInSettings": "{}: Za ovo su potrebni dodatni akreditivi (u Postavkama)", "checkOnStart": "Provjerite ima li novosti pri pokretanju", "tryInferAppIdFromCode": "Pokušati otkriti ID aplikacije iz izvornog koda", "removeOnExternalUninstall": "Automatski ukloni eksterno deinstalirane aplikacije", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije", "checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Želite li ukloniti aplikaciju?", "other": "Želite li ukloniti aplikacije?" diff --git a/assets/translations/cs.json b/assets/translations/cs.json index b4b052e..0af18b7 100644 --- a/assets/translations/cs.json +++ b/assets/translations/cs.json @@ -55,7 +55,7 @@ "notInstalled": "Není nainstalováno", "estimateInBrackets": "(přibližně)", "selectAll": "Vybrat Vše", - "deselectN": "{} deselected", + "deselectX": "{} deselected", "xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.", "removeSelectedAppsQuestion": "Odebrat vybrané aplikace?", "removeSelectedApps": "Odebrat vybrané aplikace", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace", "gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)", "about": "About", - "requiresCredentialsInSettings": "Vyžaduje další pověření (v nastavení)", + "requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)", "checkOnStart": "Zkontrolovat jednou při spuštění", "tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu", "removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Dokončit instalaci aplikace", "checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Odstranit Apku?", "other": "Odstranit Apky?" diff --git a/assets/translations/de.json b/assets/translations/de.json index af5cc15..e5f979b 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -55,7 +55,7 @@ "notInstalled": "Nicht installiert", "estimateInBrackets": "(Ungefähr)", "selectAll": "Alle auswählen", - "deselectN": "{} abgewählt", + "deselectX": "{} abgewählt", "xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.", "removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?", "removeSelectedApps": "Ausgewählte Apps entfernen", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben", "gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)", "about": "Über", - "requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)", + "requiresCredentialsInSettings": "{}: Benötigt zusätzliche Anmeldedaten (in den Einstellungen)", "checkOnStart": "Überprüfe einmalig beim Start", "tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln", "removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "App Installation abschließen", "checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen", "onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "App entfernen?", "other": "Apps entfernen?" @@ -327,4 +329,4 @@ "one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.", "other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert." } -} +} \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index 4e4626a..d81785a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -55,7 +55,7 @@ "notInstalled": "Not Installed", "estimateInBrackets": "(Estimate)", "selectAll": "Select All", - "deselectN": "Deselect {}", + "deselectX": "Deselect {}", "xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.", "removeSelectedAppsQuestion": "Remove Selected Apps?", "removeSelectedApps": "Remove Selected Apps", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "about": "About", - "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", + "requiresCredentialsInSettings": "{} needs additional credentials (in Settings)", "checkOnStart": "Check for updates on startup", "tryInferAppIdFromCode": "Try inferring App ID from source code", "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Complete App Installation", "checkingForUpdatesNotifChannel": "Checking for Updates", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Remove App?", "other": "Remove Apps?" diff --git a/assets/translations/es.json b/assets/translations/es.json index 481838a..fff2e8b 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -55,7 +55,7 @@ "notInstalled": "No Instalado", "estimateInBrackets": "(Aproximado)", "selectAll": "Seleccionar Todo", - "deselectN": "Deseleccionar {}", + "deselectX": "Deseleccionar {}", "xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.", "removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?", "removeSelectedApps": "Borrar Aplicaciones Seleccionadas", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "about": "About", - "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", + "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)", "checkOnStart": "Check for updates on startup", "tryInferAppIdFromCode": "Try inferring App ID from source code", "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación", "checkingForUpdatesNotifChannel": "Buscando Actualizaciones", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "¿Eliminar Aplicación?", "other": "¿Eliminar Aplicaciones?" diff --git a/assets/translations/fa.json b/assets/translations/fa.json index 013ee34..5b0d281 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -55,7 +55,7 @@ "notInstalled": "نصب نشده", "estimateInBrackets": "(تخمین زدن)", "selectAll": "انتخاب همه", - "deselectN": "لغو انتخاب {}", + "deselectX": "لغو انتخاب {}", "xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف می‌شود اما روی دستگاه نصب می‌ماند.", "removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟", "removeSelectedApps": "حذف برنامه های انتخاب شده", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید", "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)", "about": "درباره", - "requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)", + "requiresCredentialsInSettings": "{}: این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)", "checkOnStart": "بررسی در شروع", "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید", "removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "نصب کامل برنامه", "checkingForUpdatesNotifChannel": "بررسی به‌روزرسانی‌ها", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "برنامه حذف شود؟", "other": "برنامه ها حذف شوند؟" diff --git a/assets/translations/fr.json b/assets/translations/fr.json index e5d3bba..1940b2d 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -55,7 +55,7 @@ "notInstalled": "Pas installé", "estimateInBrackets": "(Estimation)", "selectAll": "Tout sélectionner", - "deselectN": "Déselectionner {}", + "deselectX": "Déselectionner {}", "xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.", "removeSelectedAppsQuestion": "Supprimer les applications sélectionnées ?", "removeSelectedApps": "Supprimer les applications sélectionnées", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "about": "About", - "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", + "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)", "checkOnStart": "Check for updates on startup", "tryInferAppIdFromCode": "Try inferring App ID from source code", "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Installation complète de l'application", "checkingForUpdatesNotifChannel": "Vérification des mises à jour", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Supprimer l'application ?", "other": "Supprimer les applications ?" diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 37f9e30..de1402e 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -55,7 +55,7 @@ "notInstalled": "Nem telepített", "estimateInBrackets": "(Becslés)", "selectAll": "Mindet kiválaszt", - "deselectN": "Törölje {} kijelölését", + "deselectX": "Törölje {} kijelölését", "xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.", "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?", "removeSelectedApps": "Távolítsa el a kiválasztott appokat", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára", "gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)", "about": "Rólunk", - "requiresCredentialsInSettings": "Ehhez további hitelesítő adatokra van szükség (a Beállításokban)", + "requiresCredentialsInSettings": "{}: Ehhez további hitelesítő adatokra van szükség (a Beállításokban)", "checkOnStart": "Egyszer az alkalmazás indításakor is", "tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból", "removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Teljes app telepítés", "checkingForUpdatesNotifChannel": "Frissítések keresése", "onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "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 945dfc0..25d1788 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -55,7 +55,7 @@ "notInstalled": "Non installato", "estimateInBrackets": "(stimato)", "selectAll": "Seleziona tutto", - "deselectN": "Deseleziona {}", + "deselectX": "Deseleziona {}", "xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.", "removeSelectedAppsQuestion": "Rimuovere le app selezionate?", "removeSelectedApps": "Rimuovi le app selezionate", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista", "gitlabPATLabel": "GitLab Personal Access Token\n(attiva la ricerca e migliora la rilevazione di apk)", "about": "Informazioni", - "requiresCredentialsInSettings": "Servono credenziali aggiuntive (in Impostazioni)", + "requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)", "checkOnStart": "Controlla una volta all'avvio", "tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente", "removeOnExternalUninstall": "Rimuovi automaticamente app disinstallate esternamente", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Completa l'installazione dell'app", "checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso", "onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Rimuovere l'app?", "other": "Rimuovere le app?" diff --git a/assets/translations/ja.json b/assets/translations/ja.json index e176194..b81d093 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -55,7 +55,7 @@ "notInstalled": "未インストール", "estimateInBrackets": "(推定)", "selectAll": "すべて選択", - "deselectN": "{}件の選択を解除", + "deselectX": "{}件の選択を解除", "xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。", "removeSelectedAppsQuestion": "選択したアプリを削除しますか?", "removeSelectedApps": "選択したアプリを削除する", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる", "gitlabPATLabel": "GitLab パーソナルアクセストークン\n(検索とより良いAPK検出の有効化)", "about": "概要", - "requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)", + "requiresCredentialsInSettings": "{}: これには追加の認証が必要です (設定にて)", "checkOnStart": "起動時にアップデートを確認する", "tryInferAppIdFromCode": "ソースコードからApp IDを推測する", "removeOnExternalUninstall": "外部でアンインストールされたアプリを自動的に削除する", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "アプリのインストールを完了する", "checkingForUpdatesNotifChannel": "アップデートを確認中", "onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "アプリを削除しますか?", "other": "アプリを削除しますか?" diff --git a/assets/translations/nl.json b/assets/translations/nl.json index 7c953da..aa33f61 100644 --- a/assets/translations/nl.json +++ b/assets/translations/nl.json @@ -55,7 +55,7 @@ "notInstalled": "Niet geinstalleerd", "estimateInBrackets": "(Ongeveer)", "selectAll": "Selecteer alles", - "deselectN": "Deselecteer {}", + "deselectX": "Deselecteer {}", "xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.", "removeSelectedAppsQuestion": "Geselecteerde apps verwijderen??", "removeSelectedApps": "Geselecteerde apps verwijderen", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave", "gitlabPATLabel": "GitLab Personal Access Token\n(Maakt het mogelijk beter te zoeken naar APK's)", "about": "Over", - "requiresCredentialsInSettings": "Dit vereist aanvullende referenties (in Instellingen)", + "requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)", "checkOnStart": "Controleren op updates bij opstarten", "tryInferAppIdFromCode": "Probeer de app-ID af te leiden uit de broncode", "removeOnExternalUninstall": "Automatisch extern verwijderde apps verwijderen", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Voltooien van de app-installatie", "checkingForUpdatesNotifChannel": "Controleren op updates", "onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "App verwijderen?", "other": "Apps verwijderen?" diff --git a/assets/translations/pl.json b/assets/translations/pl.json index e36ef9c..3da01ab 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -55,7 +55,7 @@ "notInstalled": "Nie zainstalowano", "estimateInBrackets": "(Szacunkowo)", "selectAll": "Zaznacz wszystkie", - "deselectN": "Odznacz {}", + "deselectX": "Odznacz {}", "xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.", "removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?", "removeSelectedApps": "Usuń wybrane aplikacje", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji", "gitlabPATLabel": "Osobisty token dostępu GitLab\n(Umożliwia wyszukiwanie i lepsze wykrywanie APK)", "about": "Więcej informacji", - "requiresCredentialsInSettings": "Wymaga to dodatkowych poświadczeń (w Ustawieniach)", + "requiresCredentialsInSettings": "{}: Wymaga to dodatkowych poświadczeń (w Ustawieniach)", "checkOnStart": "Sprawdź aktualizacje przy uruchomieniu", "tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego", "removeOnExternalUninstall": "Automatyczne usuń odinstalowane zewnętrznie aplikacje", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji", "checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji", "onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Usunąć aplikację?", "few": "Usunąć aplikacje?", diff --git a/assets/translations/pt.json b/assets/translations/pt.json index 9611468..878d9f5 100644 --- a/assets/translations/pt.json +++ b/assets/translations/pt.json @@ -55,7 +55,7 @@ "notInstalled": "Não Instalado", "estimateInBrackets": "(Aproximado)", "selectAll": "Selecionar All", - "deselectN": "Deselecionar {}", + "deselectX": "Deselecionar {}", "xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.", "removeSelectedAppsQuestion": "Remover Apps Selecionados?", "removeSelectedApps": "Remover Apps Selecionados", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps", "gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)", "about": "Sobre", - "requiresCredentialsInSettings": "Isso requer credenciais adicionais (em Configurações)", + "requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)", "checkOnStart": "Checar por atualizações ao iniciar ", "tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte", "removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Instalação completa do App", "checkingForUpdatesNotifChannel": "Checando por Atualizações", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Remover App?", "other": "Remover Apps?" diff --git a/assets/translations/ru.json b/assets/translations/ru.json index f02a7da..b5682ea 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -55,7 +55,7 @@ "notInstalled": "Не установлено", "estimateInBrackets": "(Оценка)", "selectAll": "Выбрать всё", - "deselectN": "Отменить выбор {}", + "deselectX": "Отменить выбор {}", "xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве", "removeSelectedAppsQuestion": "Удалить выбранные приложения?", "removeSelectedApps": "Удалить выбранные приложения", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка", "gitlabPATLabel": "Персональный токен доступа GitLab\n(включает поиск и улучшает обнаружение APK)", "about": "Описание", - "requiresCredentialsInSettings": "Для этого требуются дополнительные учетные данные (в настройках)", + "requiresCredentialsInSettings": "{}: Для этого требуются дополнительные учетные данные (в настройках)", "checkOnStart": "Проверять наличие обновлений при запуске", "tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода", "removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Завершение установки приложения", "checkingForUpdatesNotifChannel": "Проверка обновлений", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Удалить приложение?", "other": "Удалить приложения?" diff --git a/assets/translations/sv.json b/assets/translations/sv.json index 06aeabb..6df5314 100644 --- a/assets/translations/sv.json +++ b/assets/translations/sv.json @@ -55,7 +55,7 @@ "notInstalled": "Inte Installerad", "estimateInBrackets": "(Uppskattning)", "selectAll": "Välj Alla", - "deselectN": "Avmarkera {}", + "deselectX": "Avmarkera {}", "xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.", "removeSelectedAppsQuestion": "Ta bort markerade Appar?", "removeSelectedApps": "Ta bort markerade Appar", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "about": "Om", - "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", + "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)", "checkOnStart": "Kolla efter uppdateringar vid start", "tryInferAppIdFromCode": "Try inferring App ID from source code", "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", diff --git a/assets/translations/tr.json b/assets/translations/tr.json index a969e57..a463ae6 100644 --- a/assets/translations/tr.json +++ b/assets/translations/tr.json @@ -55,7 +55,7 @@ "notInstalled": "Yüklenmedi", "estimateInBrackets": "(Tahmini)", "selectAll": "Hepsini Seç", - "deselectN": "{}'yi Seçimden Kaldır", + "deselectX": "{}'yi Seçimden Kaldır", "xWillBeRemovedButRemainInstalled": "{} Obtainium'dan kaldırılacak ancak cihazınızda yüklü kalacaktır.", "removeSelectedAppsQuestion": "Seçilen Uygulamaları Kaldırmak İstiyor musunuz?", "removeSelectedApps": "Seçilen Uygulamaları Kaldır", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "Yüklenmemiş Uygulamaları Uygulamalar Görünümünün Altına Taşı", "gitlabPATLabel": "GitLab Kişisel Erişim Belirteci\n(Arama ve Daha İyi APK Keşfi İçin)", "about": "Hakkında", - "requiresCredentialsInSettings": "Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)", + "requiresCredentialsInSettings": "{}: Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)", "checkOnStart": "Başlangıçta güncellemeleri kontrol et", "tryInferAppIdFromCode": "Uygulama kimliğini kaynak kodundan çıkarma girişimi", "removeOnExternalUninstall": "Harici kaldırmada otomatik olarak kaldırılan uygulamalar", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "Uygulama Kurulumu Tamamlandı", "checkingForUpdatesNotifChannel": "Güncellemeler Kontrol Ediliyor", "onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "Uygulamayı Kaldır?", "other": "Uygulamaları Kaldır?" diff --git a/assets/translations/vi.json b/assets/translations/vi.json index 4b66a9e..5f649f5 100644 --- a/assets/translations/vi.json +++ b/assets/translations/vi.json @@ -55,7 +55,7 @@ "notInstalled": "Chưa cài đặt", "estimateInBrackets": "(Ước lượng)", "selectAll": "Chọn tất cả", - "deselectN": "Bỏ chọn {}", + "deselectX": "Bỏ chọn {}", "xWillBeRemovedButRemainInstalled": "{} sẽ bị xóa khỏi Obtainium nhưng vẫn còn cài đặt trên thiết bị.", "removeSelectedAppsQuestion": "Xóa ứng dụng đã chọn?", "removeSelectedApps": "Xóa ứng dụng đã chọn", @@ -223,7 +223,7 @@ "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)", "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)", + "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", "tryInferAppIdFromCode": "Thử suy ra ID ứng dụng từ mã nguồn", "removeOnExternalUninstall": "Tự động xóa ứng dụng đã gỡ cài đặt bên ngoài", @@ -275,6 +275,8 @@ "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", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion":{ "one": "Gỡ ứng dụng?", "other": "Gỡ ứng dụng?" diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 314d9fe..0de58fc 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -55,7 +55,7 @@ "notInstalled": "未安装", "estimateInBrackets": "(推测)", "selectAll": "全选", - "deselectN": "取消选择 {}", + "deselectX": "取消选择 {}", "xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。", "removeSelectedAppsQuestion": "是否删除选中的应用?", "removeSelectedApps": "删除选中的应用", @@ -223,7 +223,7 @@ "moveNonInstalledAppsToBottom": "将未安装应用置底", "gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)", "about": "相关文档", - "requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)", + "requiresCredentialsInSettings": "{}: 此功能需要额外的凭据(在“设置”中添加)", "checkOnStart": "启动时进行一次检查", "tryInferAppIdFromCode": "尝试从源代码推断应用 ID", "removeOnExternalUninstall": "自动删除已卸载的外部应用", @@ -275,6 +275,8 @@ "completeAppInstallationNotifChannel": "完成应用安装", "checkingForUpdatesNotifChannel": "正在检查更新", "onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查", + "supportFixedAPKURL": "Support fixed APK URLs", + "selectX": "Select {}", "removeAppQuestion": { "one": "是否删除应用?", "other": "是否删除应用?" diff --git a/lib/app_sources/apkmirror.dart b/lib/app_sources/apkmirror.dart index 2265cc7..f1b903b 100644 --- a/lib/app_sources/apkmirror.dart +++ b/lib/app_sources/apkmirror.dart @@ -32,7 +32,8 @@ class APKMirror extends AppSource { @override String sourceSpecificStandardizeURL(String url) { - RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+'); + RegExp standardUrlRegEx = + RegExp('^https?://(www\\.)?$host/apk/[^/]+/[^/]+'); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { throw InvalidURLError(name); diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index c071c02..8e33b41 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -139,11 +139,11 @@ APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( } } // Apply the release filter if any - if (filterVersionsByRegEx != null) { + if (filterVersionsByRegEx?.isNotEmpty == true) { version = null; releaseChoices = []; for (var i = 0; i < releases.length; i++) { - if (RegExp(filterVersionsByRegEx) + if (RegExp(filterVersionsByRegEx!) .hasMatch(releases[i]['versionName'])) { version = releases[i]['versionName']; } diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index b0d74d8..f988a4f 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -54,17 +54,25 @@ class FDroidRepo extends AppSource { @override Future>> search(String query, {Map querySettings = const {}}) async { - query = removeQueryParamsFromUrl(standardizeUrl(query)); - var res = await sourceRequest('$query/index.xml'); + String? url = querySettings['url']; + if (url == null) { + throw NoReleasesError(); + } + url = removeQueryParamsFromUrl(standardizeUrl(url)); + var res = await sourceRequest('$url/index.xml'); if (res.statusCode == 200) { var body = parse(res.body); Map> results = {}; body.querySelectorAll('application').toList().forEach((app) { String appId = app.attributes['id']!; - results['$query?appId=$appId'] = [ - app.querySelector('name')?.innerHtml ?? appId, - app.querySelector('desc')?.innerHtml ?? '' - ]; + String appName = app.querySelector('name')?.innerHtml ?? appId; + String appDesc = app.querySelector('desc')?.innerHtml ?? ''; + if (query.isEmpty || + appId.contains(query) || + appName.contains(query) || + appDesc.contains(query)) { + results['$url?appId=$appId'] = [appName, appDesc]; + } }); return results; } else { diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index 700fa06..2456335 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -117,22 +117,23 @@ class GitHub extends AppSource { .decode(body['content'].toString().split('\n').join(''))) .split('\n') .map((e) => e.trim()); - var appId = trimmedLines - .where((l) => - l.startsWith('applicationId "') || - l.startsWith('applicationId \'')) - .first; - appId = appId - .split(appId.startsWith('applicationId "') ? '"' : '\'')[1]; - if (appId.startsWith('\${') && appId.endsWith('}')) { - appId = trimmedLines - .where((l) => l.startsWith( - 'def ${appId.substring(2, appId.length - 1)}')) - .first; - appId = appId.split(appId.contains('"') ? '"' : '\'')[1]; - } - if (appId.isNotEmpty) { + var appIds = trimmedLines.where((l) => + l.startsWith('applicationId "') || + l.startsWith('applicationId \'')); + appIds = appIds.map((appId) => appId + .split(appId.startsWith('applicationId "') ? '"' : '\'')[1]); + appIds = appIds.map((appId) { + if (appId.startsWith('\${') && appId.endsWith('}')) { + appId = trimmedLines + .where((l) => l.startsWith( + 'def ${appId.substring(2, appId.length - 1)}')) + .first; + appId = appId.split(appId.contains('"') ? '"' : '\'')[1]; + } return appId; + }).where((appId) => appId.isNotEmpty); + if (appIds.length == 1) { + return appIds.first; } } catch (err) { LogsProvider().add( diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 1567250..888d727 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -48,6 +48,12 @@ class GitLab extends AppSource { label: tr('fallbackToOlderReleases'), defaultValue: true) ] ]; + searchQuerySettingFormItems = [ + GeneratedFormTextField('PAT', + label: tr('gitlabPATLabel').split('(')[0], + password: true, + required: false) + ]; } @override @@ -80,12 +86,18 @@ class GitLab extends AppSource { @override Future>> search(String query, {Map querySettings = const {}}) async { - String? PAT = await getPATIfAny({}); - if (PAT == null) { - throw CredsNeededError(name); + String? PAT; + if (!hostChanged) { + PAT = await getPATIfAny({}); + if (PAT == null) { + throw CredsNeededError(name); + } + } + if ((querySettings['PAT'] as String?)?.isNotEmpty == true) { + PAT = querySettings['PAT']; } var url = - 'https://$host/api/v4/search?private_token=$PAT&scope=projects&search=${Uri.encodeQueryComponent(query)}'; + 'https://$host/api/v4/search?${PAT?.isNotEmpty == true ? 'private_token=$PAT&' : ''}scope=projects&search=${Uri.encodeQueryComponent(query)}'; var res = await sourceRequest(url); if (res.statusCode != 200) { throw getObtainiumHttpError(res); @@ -174,7 +186,6 @@ class GitLab extends AppSource { ...getLinksFromParsedHTML(entryContent, RegExp('/[^/]+\\.apk\$', caseSensitive: false), '') .where((element) => Uri.parse(element).host != '') - ]; var entryId = entry.querySelector('id')?.innerHtml; var version = diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index f6dab8a..1d4e06f 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -4,6 +4,7 @@ import 'package:html/parser.dart'; import 'package:http/http.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) { @@ -94,6 +95,10 @@ class HTML extends AppSource { label: tr('sortByFileNamesNotLinks')) ], [GeneratedFormSwitch('reverseSort', label: tr('reverseSort'))], + [ + GeneratedFormSwitch('supportFixedAPKURL', + defaultValue: true, label: tr('supportFixedAPKURL')), + ], [ GeneratedFormTextField('customLinkFilterRegex', label: tr('customLinkFilterRegex'), @@ -222,7 +227,10 @@ class HTML extends AppSource { throw NoReleasesError(); } var rel = links.last; - String? version = rel.hashCode.toString(); + String? version; + if (additionalSettings['supportFixedAPKURL'] != true) { + version = rel.hashCode.toString(); + } var versionExtractionRegEx = additionalSettings['versionExtractionRegEx'] as String?; if (versionExtractionRegEx?.isNotEmpty == true) { @@ -243,9 +251,9 @@ class HTML extends AppSource { throw NoVersionError(); } } - List apkUrls = - [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); - return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(), + rel = ensureAbsoluteUrl(rel, uri); + version ??= (await checkDownloadHash(rel)).toString(); + return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(), AppNames(uri.host, tr('app'))); } else { throw getObtainiumHttpError(res); diff --git a/lib/main.dart b/lib/main.dart index 36b1079..df67526 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.14.34'; +const String currentVersion = '0.14.35'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 08f3024..0ff0cda 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -254,57 +254,79 @@ class _AddAppPageState extends State { ], ); - runSearch() async { + runSearch({bool filtered = true}) async { setState(() { searching = true; }); + var sourceStrings = >{}; + sourceProvider.sources + .where((e) => e.canSearch && !e.excludeFromMassSearch) + .forEach((s) { + sourceStrings[s.name] = [s.name]; + }); try { - var results = await Future.wait(sourceProvider.sources - .where((e) => e.canSearch && !e.excludeFromMassSearch) - .map((e) async { - try { - return await e.search(searchQuery); - } catch (err) { - if (err is! CredsNeededError) { - rethrow; - } else { - return >{}; - } - } - })); - - // .then((results) async { - // Interleave results instead of simple reduce - Map> res = {}; - var si = 0; - var done = false; - while (!done) { - done = true; - for (var r in results) { - if (r.length > si) { - done = false; - res.addEntries([r.entries.elementAt(si)]); - } - } - si++; - } - if (res.isEmpty) { - throw ObtainiumError(tr('noResults')); - } - List? selectedUrls = res.isEmpty - ? [] - // ignore: use_build_context_synchronously - : await showDialog?>( + var searchSources = await showDialog?>( context: context, builder: (BuildContext ctx) { - return UrlSelectionModal( - urlsWithDescriptions: res, - selectedByDefault: false, - onlyOneSelectionAllowed: true, + return SelectionModal( + title: tr('selectX', args: [plural('source', 2)]), + entries: sourceStrings, + selectedByDefault: true, + onlyOneSelectionAllowed: false, + titlesAreLinks: false, ); - }); - if (selectedUrls != null && selectedUrls.isNotEmpty) { - changeUserInput(selectedUrls[0], true, false, isSearch: true); + }) ?? + []; + if (searchSources.isNotEmpty) { + var results = await Future.wait(sourceProvider.sources + .where((e) => searchSources.contains(e.name)) + .map((e) async { + try { + return await e.search(searchQuery); + } catch (err) { + if (err is! CredsNeededError) { + rethrow; + } else { + err.unexpected = true; + showError(err, context); + return >{}; + } + } + })); + + // .then((results) async { + // Interleave results instead of simple reduce + Map> res = {}; + var si = 0; + var done = false; + while (!done) { + done = true; + for (var r in results) { + if (r.length > si) { + done = false; + res.addEntries([r.entries.elementAt(si)]); + } + } + si++; + } + if (res.isEmpty) { + throw ObtainiumError(tr('noResults')); + } + List? selectedUrls = res.isEmpty + ? [] + // ignore: use_build_context_synchronously + : await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return SelectionModal( + entries: res, + selectedByDefault: false, + onlyOneSelectionAllowed: true, + ); + }); + if (selectedUrls != null && selectedUrls.isNotEmpty) { + changeUserInput(selectedUrls[0], true, false, isSearch: true); + } } } catch (e) { showError(e, context); @@ -470,23 +492,21 @@ class _AddAppPageState extends State { const SizedBox( height: 16, ), - ...sourceProvider.sources - .map((e) => GestureDetector( - onTap: e.host != null - ? () { - launchUrlString('https://${e.host}', - mode: LaunchMode.externalApplication); - } - : null, - child: Text( - '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', - style: TextStyle( - decoration: e.host != null - ? TextDecoration.underline - : TextDecoration.none, - fontStyle: FontStyle.italic), - ))) - + ...sourceProvider.sources.map((e) => GestureDetector( + onTap: e.host != null + ? () { + launchUrlString('https://${e.host}', + mode: LaunchMode.externalApplication); + } + : null, + child: Text( + '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', + style: TextStyle( + decoration: e.host != null + ? TextDecoration.underline + : TextDecoration.none, + fontStyle: FontStyle.italic), + ))) ]); return Scaffold( diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 8f76e80..d686740 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/app_sources/fdroidrepo.dart'; import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form_modal.dart'; @@ -189,17 +190,29 @@ class _ImportExportPageState extends State { items: [ [ GeneratedFormTextField('searchQuery', - label: tr('searchQuery')) + label: tr('searchQuery'), + required: source.name != FDroidRepo().name) + ], + ...source.searchQuerySettingFormItems.map((e) => [e]), + [ + GeneratedFormTextField('url', + label: source.host != null + ? tr('overrideSource') + : plural('url', 1).substring(2), + defaultValue: source.host ?? '', + required: true) ], - ...source.searchQuerySettingFormItems.map((e) => [e]) ], ); }); - if (values != null && - (values['searchQuery'] as String?)?.isNotEmpty == true) { + if (values != null) { setState(() { importInProgress = true; }); + if (values['url'] != source.host) { + source = sourceProvider.getSource(values['url'], + overrideSource: source.runtimeType.toString()); + } var urlsWithDescriptions = await source .search(values['searchQuery'] as String, querySettings: values); if (urlsWithDescriptions.isNotEmpty) { @@ -208,8 +221,8 @@ class _ImportExportPageState extends State { await showDialog?>( context: context, builder: (BuildContext ctx) { - return UrlSelectionModal( - urlsWithDescriptions: urlsWithDescriptions, + return SelectionModal( + entries: urlsWithDescriptions, selectedByDefault: false, ); }); @@ -269,8 +282,7 @@ class _ImportExportPageState extends State { await showDialog?>( context: context, builder: (BuildContext ctx) { - return UrlSelectionModal( - urlsWithDescriptions: urlsWithDescriptions); + return SelectionModal(entries: urlsWithDescriptions); }); if (selectedUrls != null) { var errors = await appsProvider.addAppsByURL(selectedUrls); @@ -300,6 +312,11 @@ class _ImportExportPageState extends State { }); } + var sourceStrings = >{}; + sourceProvider.sources.where((e) => e.canSearch).forEach((s) { + sourceStrings[s.name] = [s.name]; + }); + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView(slivers: [ @@ -409,6 +426,54 @@ class _ImportExportPageState extends State { const Divider( height: 32, ), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: importInProgress + ? null + : () async { + var searchSourceName = + await showDialog< + List?>( + context: context, + builder: + (BuildContext + ctx) { + return SelectionModal( + title: tr( + 'selectX', + args: [ + tr('source') + ]), + entries: + sourceStrings, + selectedByDefault: + false, + onlyOneSelectionAllowed: + true, + titlesAreLinks: + false, + ); + }) ?? + []; + var searchSource = + sourceProvider.sources + .where((e) => + searchSourceName + .contains( + e.name)) + .toList(); + if (searchSource.isNotEmpty) { + runSourceSearch( + searchSource[0]); + } + }, + child: Text(tr('searchX', + args: [tr('source')])))), + ], + ), + const SizedBox(height: 8), TextButton( onPressed: importInProgress ? null : urlListImport, @@ -424,39 +489,19 @@ class _ImportExportPageState extends State { )), ], ), - ...sourceProvider.sources - .where((element) => element.canSearch) - .map((source) => Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 8), - TextButton( - onPressed: importInProgress - ? null - : () { - runSourceSearch(source); - }, - child: Text( - tr('searchX', args: [source.name]))) - ])) - , - ...sourceProvider.massUrlSources - .map((source) => Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 8), - TextButton( - onPressed: importInProgress - ? null - : () { - runMassSourceImport(source); - }, - child: Text( - tr('importX', args: [source.name]))) - ])) - , + ...sourceProvider.massUrlSources.map((source) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 8), + TextButton( + onPressed: importInProgress + ? null + : () { + runMassSourceImport(source); + }, + child: Text( + tr('importX', args: [source.name]))) + ])), const Spacer(), const Divider( height: 32, @@ -532,38 +577,42 @@ class _ImportErrorDialogState extends State { } // ignore: must_be_immutable -class UrlSelectionModal extends StatefulWidget { - UrlSelectionModal( +class SelectionModal extends StatefulWidget { + SelectionModal( {super.key, - required this.urlsWithDescriptions, + required this.entries, this.selectedByDefault = true, - this.onlyOneSelectionAllowed = false}); + this.onlyOneSelectionAllowed = false, + this.titlesAreLinks = true, + this.title}); - Map> urlsWithDescriptions; + String? title; + Map> entries; bool selectedByDefault; bool onlyOneSelectionAllowed; + bool titlesAreLinks; @override - State createState() => _UrlSelectionModalState(); + State createState() => _SelectionModalState(); } -class _UrlSelectionModalState extends State { - Map>, bool> urlWithDescriptionSelections = {}; +class _SelectionModalState extends State { + Map>, bool> entrySelections = {}; @override void initState() { super.initState(); - for (var url in widget.urlsWithDescriptions.entries) { - urlWithDescriptionSelections.putIfAbsent(url, + for (var url in widget.entries.entries) { + entrySelections.putIfAbsent(url, () => widget.selectedByDefault && !widget.onlyOneSelectionAllowed); } if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) { - selectOnlyOne(widget.urlsWithDescriptions.entries.first.key); + selectOnlyOne(widget.entries.entries.first.key); } } selectOnlyOne(String url) { - for (var uwd in urlWithDescriptionSelections.keys) { - urlWithDescriptionSelections[uwd] = uwd.key == url; + for (var e in entrySelections.keys) { + entrySelections[e] = e.key == url; } } @@ -571,73 +620,88 @@ class _UrlSelectionModalState extends State { Widget build(BuildContext context) { return AlertDialog( scrollable: true, - title: Text( - widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')), + title: Text(widget.title ?? tr('pick')), content: Column(children: [ - ...urlWithDescriptionSelections.keys.map((urlWithD) { + ...entrySelections.keys.map((entry) { selectThis(bool? value) { setState(() { value ??= false; if (value! && widget.onlyOneSelectionAllowed) { - selectOnlyOne(urlWithD.key); + selectOnlyOne(entry.key); } else { - urlWithDescriptionSelections[urlWithD] = value!; + entrySelections[entry] = value!; } }); } var urlLink = GestureDetector( - onTap: () { - launchUrlString(urlWithD.key, - mode: LaunchMode.externalApplication); - }, + onTap: !widget.titlesAreLinks + ? null + : () { + launchUrlString(entry.key, + mode: LaunchMode.externalApplication); + }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - urlWithD.value[0], - style: const TextStyle( - decoration: TextDecoration.underline, + entry.value.isEmpty ? entry.key : entry.value[0], + style: TextStyle( + decoration: widget.titlesAreLinks + ? TextDecoration.underline + : null, fontWeight: FontWeight.bold), textAlign: TextAlign.start, ), - Text( - Uri.parse(urlWithD.key).host, - style: const TextStyle( - decoration: TextDecoration.underline, fontSize: 12), - ) + if (widget.titlesAreLinks) + Text( + Uri.parse(entry.key).host, + style: const TextStyle( + decoration: TextDecoration.underline, fontSize: 12), + ) ], )); - var descriptionText = Text( - urlWithD.value[1].length > 128 - ? '${urlWithD.value[1].substring(0, 128)}...' - : urlWithD.value[1], - style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), - ); + var descriptionText = entry.value.length <= 1 + ? const SizedBox.shrink() + : Text( + entry.value[1].length > 128 + ? '${entry.value[1].substring(0, 128)}...' + : entry.value[1], + style: const TextStyle( + fontStyle: FontStyle.italic, fontSize: 12), + ); - var selectedUrlsWithDs = urlWithDescriptionSelections.entries - .where((e) => e.value) - .toList(); + var selectedEntries = + entrySelections.entries.where((e) => e.value).toList(); var singleSelectTile = ListTile( - title: urlLink, - subtitle: GestureDetector( - onTap: () { - setState(() { - selectOnlyOne(urlWithD.key); - }); - }, - child: descriptionText, - ), - leading: Radio( - value: urlWithD.key, - groupValue: selectedUrlsWithDs.isEmpty + title: GestureDetector( + onTap: widget.titlesAreLinks ? null - : selectedUrlsWithDs.first.key.key, + : () { + selectThis(!(entrySelections[entry] ?? false)); + }, + child: urlLink, + ), + subtitle: entry.value.length <= 1 + ? null + : GestureDetector( + onTap: () { + setState(() { + selectOnlyOne(entry.key); + }); + }, + child: descriptionText, + ), + leading: Radio( + value: entry.key, + groupValue: selectedEntries.isEmpty + ? null + : selectedEntries.first.key.key, onChanged: (value) { setState(() { - selectOnlyOne(urlWithD.key); + selectOnlyOne(entry.key); }); }, ), @@ -645,7 +709,7 @@ class _UrlSelectionModalState extends State { var multiSelectTile = Row(children: [ Checkbox( - value: urlWithDescriptionSelections[urlWithD], + value: entrySelections[entry], onChanged: (value) { selectThis(value); }), @@ -660,14 +724,22 @@ class _UrlSelectionModalState extends State { const SizedBox( height: 8, ), - urlLink, GestureDetector( - onTap: () { - selectThis( - !(urlWithDescriptionSelections[urlWithD] ?? false)); - }, - child: descriptionText, + onTap: widget.titlesAreLinks + ? null + : () { + selectThis(!(entrySelections[entry] ?? false)); + }, + child: urlLink, ), + entry.value.length <= 1 + ? const SizedBox.shrink() + : GestureDetector( + onTap: () { + selectThis(!(entrySelections[entry] ?? false)); + }, + child: descriptionText, + ), const SizedBox( height: 8, ) @@ -687,24 +759,18 @@ class _UrlSelectionModalState extends State { }, child: Text(tr('cancel'))), TextButton( - onPressed: - urlWithDescriptionSelections.values.where((b) => b).isEmpty - ? null - : () { - Navigator.of(context).pop(urlWithDescriptionSelections - .entries - .where((entry) => entry.value) - .map((e) => e.key.key) - .toList()); - }, + onPressed: entrySelections.values.where((b) => b).isEmpty + ? null + : () { + Navigator.of(context).pop(entrySelections.entries + .where((entry) => entry.value) + .map((e) => e.key.key) + .toList()); + }, child: Text(widget.onlyOneSelectionAllowed ? tr('pick') - : tr('importX', args: [ - plural( - 'url', - urlWithDescriptionSelections.values - .where((b) => b) - .length) + : tr('selectX', args: [ + entrySelections.values.where((b) => b).length.toString() ]))) ], ); diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index ae0e992..dae50c4 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:http/http.dart' as http; +import 'package:crypto/crypto.dart'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; import 'package:android_intent_plus/flag.dart'; @@ -139,6 +140,100 @@ List> moveStrToEndMapEntryWithCount( return arr; } +Future downloadFileWithRetry( + String url, String fileNameNoExt, Function? onProgress, String destDir, + {bool useExisting = true, + Map? headers, + int retries = 3}) async { + try { + return await downloadFile(url, fileNameNoExt, onProgress, destDir, + useExisting: useExisting, headers: headers); + } catch (e) { + if (retries > 0 && e is ClientException) { + await Future.delayed(const Duration(seconds: 5)); + return await downloadFileWithRetry( + url, fileNameNoExt, onProgress, destDir, + useExisting: useExisting, headers: headers, retries: (retries - 1)); + } else { + rethrow; + } + } +} + +String hashListOfLists(List> data) { + var bytes = utf8.encode(jsonEncode(data)); + var digest = sha256.convert(bytes); + var hash = digest.toString(); + return hash.hashCode.toString(); +} + +Future checkDownloadHash(String url, + {int bytesToGrab = 1024, Map? headers}) async { + var req = Request('GET', Uri.parse(url)); + if (headers != null) { + req.headers.addAll(headers); + } + req.headers[HttpHeaders.rangeHeader] = 'bytes=0-$bytesToGrab'; + var client = http.Client(); + var response = await client.send(req); + if (response.statusCode < 200 || response.statusCode > 299) { + throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError')); + } + List> bytes = await response.stream.take(bytesToGrab).toList(); + return hashListOfLists(bytes); +} + +Future downloadFile( + String url, String fileNameNoExt, Function? onProgress, String destDir, + {bool useExisting = true, Map? headers}) async { + var req = Request('GET', Uri.parse(url)); + if (headers != null) { + req.headers.addAll(headers); + } + var client = http.Client(); + StreamedResponse response = await client.send(req); + String ext = + response.headers['content-disposition']?.split('.').last ?? 'apk'; + if (ext.endsWith('"') || ext.endsWith("other")) { + ext = ext.substring(0, ext.length - 1); + } + if (url.toLowerCase().endsWith('.apk') && ext != 'apk') { + ext = 'apk'; + } + File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); + if (!(downloadedFile.existsSync() && useExisting)) { + File tempDownloadedFile = File('${downloadedFile.path}.part'); + if (tempDownloadedFile.existsSync()) { + tempDownloadedFile.deleteSync(recursive: true); + } + var length = response.contentLength; + var received = 0; + double? progress; + var sink = tempDownloadedFile.openWrite(); + await response.stream.map((s) { + received += s.length; + progress = (length != null ? received / length * 100 : 30); + if (onProgress != null) { + onProgress(progress); + } + return s; + }).pipe(sink); + await sink.close(); + progress = null; + if (onProgress != null) { + onProgress(progress); + } + if (response.statusCode != 200) { + tempDownloadedFile.deleteSync(recursive: true); + throw response.reasonPhrase ?? tr('unexpectedError'); + } + tempDownloadedFile.renameSync(downloadedFile.path); + } else { + client.close(); + } + return downloadedFile; +} + class AppsProvider with ChangeNotifier { // In memory App state (should always be kept in sync with local storage versions) Map apps = {}; @@ -192,77 +287,6 @@ class AppsProvider with ChangeNotifier { }(); } - Future downloadFileWithRetry( - String url, String fileNameNoExt, Function? onProgress, - {bool useExisting = true, - Map? headers, - int retries = 3}) async { - try { - return await downloadFile(url, fileNameNoExt, onProgress, - useExisting: useExisting, headers: headers); - } catch (e) { - if (retries > 0 && e is ClientException) { - await Future.delayed(const Duration(seconds: 5)); - return await downloadFileWithRetry(url, fileNameNoExt, onProgress, - useExisting: useExisting, headers: headers, retries: (retries - 1)); - } else { - rethrow; - } - } - } - - Future downloadFile( - String url, String fileNameNoExt, Function? onProgress, - {bool useExisting = true, Map? headers}) async { - var destDir = APKDir.path; - var req = Request('GET', Uri.parse(url)); - if (headers != null) { - req.headers.addAll(headers); - } - var client = http.Client(); - StreamedResponse response = await client.send(req); - String ext = - response.headers['content-disposition']?.split('.').last ?? 'apk'; - if (ext.endsWith('"') || ext.endsWith("other")) { - ext = ext.substring(0, ext.length - 1); - } - if (url.toLowerCase().endsWith('.apk') && ext != 'apk') { - ext = 'apk'; - } - File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); - if (!(downloadedFile.existsSync() && useExisting)) { - File tempDownloadedFile = File('${downloadedFile.path}.part'); - if (tempDownloadedFile.existsSync()) { - tempDownloadedFile.deleteSync(recursive: true); - } - var length = response.contentLength; - var received = 0; - double? progress; - var sink = tempDownloadedFile.openWrite(); - await response.stream.map((s) { - received += s.length; - progress = (length != null ? received / length * 100 : 30); - if (onProgress != null) { - onProgress(progress); - } - return s; - }).pipe(sink); - await sink.close(); - progress = null; - if (onProgress != null) { - onProgress(progress); - } - if (response.statusCode != 200) { - tempDownloadedFile.deleteSync(recursive: true); - throw response.reasonPhrase ?? tr('unexpectedError'); - } - tempDownloadedFile.renameSync(downloadedFile.path); - } else { - client.close(); - } - return downloadedFile; - } - Future handleAPKIDChange(App app, PackageInfo? newInfo, File downloadedFile, String downloadUrl) async { // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed @@ -322,7 +346,7 @@ class AppsProvider with ChangeNotifier { notificationsProvider?.notify(notif); } prevProg = prog; - }); + }, APKDir.path); // Set to 90 for remaining steps, will make null in 'finally' if (apps[app.id] != null) { apps[app.id]!.downloadProgress = -1; diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index f31aa80..43822f4 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -67,10 +67,11 @@ appJSONCompatibilityModifiers(Map json) { .reduce((value, element) => [...value, ...element]); Map additionalSettings = getDefaultValuesFromFormItems([formItems]); + Map originalAdditionalSettings = {}; if (json['additionalSettings'] != null) { - additionalSettings.addEntries( - Map.from(jsonDecode(json['additionalSettings'])) - .entries); + originalAdditionalSettings = + Map.from(jsonDecode(json['additionalSettings'])); + additionalSettings.addEntries(originalAdditionalSettings.entries); } // If needed, migrate old-style additionalData to newer-style additionalSettings (V1) if (json['additionalData'] != null) { @@ -134,6 +135,11 @@ appJSONCompatibilityModifiers(Map json) { if (additionalSettings['autoApkFilterByArch'] == null) { additionalSettings['autoApkFilterByArch'] = false; } + // HTML 'fixed URL' support should be disabled if it previously did not exist + if (source.runtimeType == HTML().runtimeType && + originalAdditionalSettings['supportFixedAPKURL'] == null) { + additionalSettings['supportFixedAPKURL'] = false; + } json['additionalSettings'] = jsonEncode(additionalSettings); // F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately // This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid) @@ -596,7 +602,7 @@ class SourceProvider { AppSource? source; for (var s in sources.where((element) => element.host != null)) { if (RegExp( - '://(${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}|www\\.)${s.host}(/|\\z)?') + '://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}${s.host}(/|\\z)?') .hasMatch(url)) { source = s; break; diff --git a/pubspec.lock b/pubspec.lock index 9bc0922..b304f08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -118,10 +118,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -142,12 +142,12 @@ packages: dependency: transitive description: name: cross_file - sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" + sha256: "2f9d2cbccb76127ba28528cb3ae2c2326a122446a83de5a056aaa3880d3882c5" url: "https://pub.dev" source: hosted - version: "0.3.3+6" + version: "0.3.3+7" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab @@ -182,10 +182,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419" + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" device_info_plus_platform_interface: dependency: transitive description: @@ -291,10 +291,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3" + sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e url: "https://pub.dev" source: hosted - version: "16.1.0" + version: "16.2.0" flutter_local_notifications_linux: dependency: transitive description: @@ -538,42 +538,50 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" + sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "11.1.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e + sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6" url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "12.0.1" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306" url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "9.2.0" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: d96ff56a757b7f04fa825c469d296c5aebc55f743e87bd639fef91a466a24da8 + url: "https://pub.dev" + source: hosted + version: "0.1.0+1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1 url: "https://pub.dev" source: hosted - version: "3.12.0" + version: "4.0.2" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.0" petitparser: dependency: transitive description: @@ -847,10 +855,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" + sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" url_launcher_windows: dependency: transitive description: @@ -903,10 +911,10 @@ packages: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" + sha256: adb8c03c2be231bea5a8ed0e9039e9d18dbb049603376beaefa15393ede468a5 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" webview_flutter_wkwebview: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8859d6b..c79c667 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.14.34+228 # When changing this, update the tag in main() accordingly +version: 0.14.35+229 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0' @@ -66,6 +66,7 @@ dependencies: hsluv: ^1.1.3 connectivity_plus: ^5.0.0 shared_storage: ^0.8.0 + crypto: ^3.0.3 dev_dependencies: flutter_test: