Compare commits

...

40 Commits

Author SHA1 Message Date
d1a2831922 Merge pull request #1535 from ImranR98/dev
Update packages, increment version
2024-04-13 03:09:10 -04:00
b042050ea3 Update packages, increment version 2024-04-13 03:05:44 -04:00
2d43dfe0a7 Merge pull request #1525 from gidano/main
Update hu.json
2024-04-13 03:04:13 -04:00
b1a740223c Merge pull request #1529 from DwainZwerg/patch-14
Update de.json
2024-04-13 03:04:02 -04:00
41c98d97b1 Merge pull request #1532 from gepbird/fix-generated-form-update
Fix out of bounds array access in GeneratedForm update
2024-04-13 03:03:47 -04:00
ccc0e7696b Fix out of bounds array access in GeneratedForm update 2024-04-08 21:45:19 +02:00
6d41ed8011 Update de.json
Corrected translation
2024-04-08 08:44:28 +02:00
c1476a7d58 Update hu.json 2024-04-07 09:37:28 +02:00
b1ee39be3b Merge pull request #1524 from ImranR98/dev
Add release asset download button (#1493) + Minor UI change
2024-04-07 02:25:27 -04:00
c2ccdd3d05 Merge remote-tracking branch 'origin/main' into dev 2024-04-07 02:21:45 -04:00
a621f1dfb4 Update packages, increment version 2024-04-07 02:21:38 -04:00
8527ad0007 Bugfix in release asset download 2024-04-07 02:19:40 -04:00
377ecef1b4 More resilient release asset import (if Downloads inaccessible, use export dir) 2024-04-07 02:18:26 -04:00
1f9921e6ff UI improvements 2024-04-07 02:13:36 -04:00
00988ed04d Improve release asset download UI 2024-04-07 01:41:35 -04:00
3d1113c057 Add release asset download button (#1493) 2024-04-07 01:28:45 -04:00
1c81f0c1e1 Merge pull request #1516 from ImranR98/dev
* Add support for GitLab CI artifact links in releases (#1506)
* Improve load speed on return to foreground
2024-04-03 18:15:44 -04:00
b26043ef5e Merge remote-tracking branch 'origin/main' into dev 2024-04-03 18:14:04 -04:00
e3ad144c06 Merge pull request #1503 from ngocanhtve/main
Update vi.json
2024-04-03 18:13:53 -04:00
fdb45c48ce Upgrade packages, increment version 2024-04-03 18:12:42 -04:00
8619cfa819 Add support for GitLab CI artifact links in releases (#1506) 2024-04-02 14:07:32 -04:00
1fe4cdd648 Improve load speed on return to foreground 2024-04-02 13:45:32 -04:00
ce22197ec9 Update vi.json
Complete.
2024-03-29 12:07:13 +07:00
f3d2dfe386 Update release.yml 2024-03-29 00:23:06 -04:00
677790af9c Update release.yml 2024-03-29 00:22:43 -04:00
85941349d8 Update release.yml 2024-03-29 00:17:35 -04:00
db755e0f90 Update release.yml 2024-03-29 00:14:26 -04:00
1019a01249 Update release.yml 2024-03-29 00:02:11 -04:00
35da44bc64 Update release.yml 2024-03-29 00:00:53 -04:00
b7ce07f965 Merge pull request #1502 from ImranR98/dev
- Added 'Share new Apps with AppVerifier' (#255)
- **Removed SourceForge (#1487)**
- Delete downloaded APK on install fail (#1495)
- Fix GitLab's broken track-only mode (#1496)
- Auto-delete download if likely invalid (#1498)
2024-03-28 23:43:25 -04:00
f2c15c5c8e Update packages and Flutter, increment version 2024-03-28 23:40:36 -04:00
8f0a6b7635 Merge remote-tracking branch 'origin/main' into dev 2024-03-28 23:40:11 -04:00
491c42d68b Removed SourceForge (#1487) 2024-03-28 23:27:22 -04:00
a883857230 Auto-delete download if likely invalid (#1498) 2024-03-28 23:13:45 -04:00
6468d0edcc Fix GitLab's broken track-only mode (#1496) 2024-03-28 23:05:16 -04:00
48eeeb4f00 Merge remote-tracking branch 'origin/main' into dev 2024-03-28 22:29:52 -04:00
0b25f25669 Merge pull request #1486 from fitojb/patch-1
Fixes to Spanish translation
2024-03-28 22:29:45 -04:00
d76b7375cb Added 'Share new Apps with AppVerifier' (#255) 2024-03-28 22:28:55 -04:00
f76aa51b54 Delete downloaded APK on install fail (#1495) 2024-03-23 19:26:39 -04:00
9e19a293d4 Fixes to Spanish translation
Includes assorted typography and treatment fixes.
2024-03-18 17:52:08 -06:00
35 changed files with 670 additions and 318 deletions

View File

@ -15,7 +15,16 @@ jobs:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Flutter Doctor
id: flutter_doctor
run: |
flutter doctor -v
- name: Import GPG key
id: import_pgp_key
uses: crazy-max/ghaction-import-gpg@v6

View File

@ -18,7 +18,6 @@ Currently supported App sources:
- [F-Droid](https://f-droid.org/)
- Third Party F-Droid Repos
- [IzzyOnDroid](https://android.izzysoft.de/)
- [SourceForge](https://sourceforge.net/)
- [SourceHut](https://git.sr.ht/)
- Other - General:
- [APKPure](https://apkpure.net/)

View File

@ -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",
@ -299,6 +302,8 @@
"note": "Note",
"selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.",
"badDownload": "The APK could not be parsed (incompatible or partial download)",
"beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)",
"appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?"

View File

@ -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",
@ -218,7 +221,7 @@
"dontShowTrackOnlyWarnings": "Nezobrazovat varování pro 'Jen sledované'",
"dontShowAPKOriginWarnings": "Nezobrazovat varování pro původ APK",
"moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
"gitlabPATLabel": "GitLab Personal Access Token",
"gitlabPATLabel": "Osobní přístupový token GitLab",
"about": "O",
"requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)",
"checkOnStart": "Zkontrolovat jednou při spuštění",
@ -299,6 +302,8 @@
"note": "Poznámka",
"selfHostedNote": "Rozbalovací seznam \"{}\" lze použít k dosažení vlastních/obvyklých instancí libovolného zdroje.",
"badDownload": "APK nelze analyzovat (nekompatibilní nebo částečné stažení)",
"beforeNewInstallsShareToAppVerifier": "Sdílení nových aplikací s aplikací AppVerifier (pokud je k dispozici)",
"appVerifierInstructionToast": "Sdílejte do aplikace AppVerifier a po dokončení se sem vraťte.",
"removeAppQuestion": {
"one": "Odstranit Apku?",
"other": "Odstranit Apky?"

View File

@ -19,7 +19,7 @@
"noDescription": "Keine Beschreibung",
"cancel": "Abbrechen",
"continue": "Weiter",
"requiredInBrackets": "(Benötigt)",
"requiredInBrackets": "(wird benötigt)",
"dropdownNoOptsError": "FEHLER: DROPDOWN MUSS MINDESTENS EINE OPTION HABEN",
"colour": "Farbe",
"githubStarredRepos": "GitHub Starred Repos",
@ -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": "release Asset",
"downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App",
"noAPKFound": "Keine APK gefunden",
"noVersionDetection": "Keine Versionserkennung",
@ -299,6 +302,8 @@
"note": "Hinweis",
"selfHostedNote": "Das „{}“-Dropdown-Menü kann verwendet werden, um selbst gehostete/angepasste Instanzen einer beliebigen Quelle zu erreichen.",
"badDownload": "Die APK konnte nicht geparst werden (inkompatibler oder teilweiser Download)",
"beforeNewInstallsShareToAppVerifier": "Neue Apps mit AppVerifier teilen (falls verfügbar)",
"appVerifierInstructionToast": "Geben Sie die Daten an AppVerifier weiter und kehren Sie dann hierher zurück, wenn Sie fertig sind.",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"

View File

@ -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",
@ -299,6 +302,8 @@
"note": "Note",
"selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.",
"badDownload": "The APK could not be parsed (incompatible or partial download)",
"beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)",
"appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"

View File

@ -1,15 +1,15 @@
{
"invalidURLForSource": "URL de la aplicación {} no es válida",
"invalidURLForSource": "El URL de la aplicación {} no es válido",
"noReleaseFound": "No se ha podido encontrar una versión válida",
"noVersionFound": "No se ha podido determinar la versión",
"urlMatchesNoSource": "La URL no coincide con ninguna fuente conocida",
"urlMatchesNoSource": "El URL no coincide con ninguna fuente conocida",
"cantInstallOlderVersion": "No se puede instalar una versión previa de la aplicación",
"appIdMismatch": "La ID del paquete descargado no coincide con la ID de la aplicación instalada",
"appIdMismatch": "El id. del paquete descargado no coincide con la ID de la aplicación instalada",
"functionNotImplemented": "Esta clase no ha implementado esta función",
"placeholder": "Espacio reservado",
"someErrors": "Han ocurrido algunos errores",
"unexpectedError": "Error inesperado",
"ok": "OK",
"ok": "Aceptar",
"and": "y",
"githubPATLabel": "Token de acceso personal a GitHub\n(reduce tiempos de espera)",
"includePrereleases": "Incluir versiones preliminares",
@ -34,75 +34,75 @@
"cancelled": "Cancelado",
"appAlreadyAdded": "Aplicación añadida anteriormente",
"alreadyUpToDateQuestion": "¿Aplicación actualizada previamente?",
"addApp": "Añadir Aplicación",
"addApp": "Añadir aplicación",
"appSourceURL": "URL de origen de la aplicación",
"error": "Error",
"add": "Añadir",
"searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
"search": "Buscar",
"additionalOptsFor": "Opciones Adicionales para {}",
"supportedSources": "Fuentes Soportadas",
"additionalOptsFor": "Opciones adicionales para {}",
"supportedSources": "Fuentes admitidas",
"trackOnlyInBrackets": "(Solo seguimiento)",
"searchableInBrackets": "(soporta búsqueda)",
"searchableInBrackets": "(permite búsqueda)",
"appsString": "Aplicaciones",
"noApps": "Sin Aplicaciones",
"noAppsForFilter": "Sin aplicaciones para filtrar",
"byX": "por: {}",
"percentProgress": "Progreso: {}%",
"pleaseWait": "Por favor, espere",
"updateAvailable": "Actualización Disponible",
"notInstalled": "No Instalado",
"percentProgress": "Progreso: {} %",
"pleaseWait": "Espere un momento",
"updateAvailable": "Actualización disponible",
"notInstalled": "No instalado",
"pseudoVersion": "pseudoversión",
"selectAll": "Seleccionar Todo",
"selectAll": "Seleccionar todo",
"deselectX": "Deseleccionar {}",
"xWillBeRemovedButRemainInstalled": "{} se eliminada de Obtainium pero continuará instalada en el dispositivo.",
"xWillBeRemovedButRemainInstalled": "{} se elimina de Obtainium pero continuará instalada en el dispositivo.",
"removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
"removeSelectedApps": "Eliminar Aplicaciones Seleccionadas",
"removeSelectedApps": "Eliminar aplicaciones seleccionadas",
"updateX": "Actualizar {}",
"installX": "Instalar {}",
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo seguimiento)\ncomo actualizada",
"changeX": "Cambiar {}",
"installUpdateApps": "Instalar/Actualizar aplicaciones",
"installUpdateSelectedApps": "Instalar/Actualizar aplicaciones seleccionadas",
"installUpdateApps": "Instalar/actualizar aplicaciones",
"installUpdateSelectedApps": "Instalar/actualizar aplicaciones seleccionadas",
"markXSelectedAppsAsUpdated": "¿Marcar {} aplicaciones seleccionadas como actualizadas?",
"no": "No",
"yes": "Sí",
"markSelectedAppsUpdated": "Marcar aplicaciones seleccionadas como actualizadas",
"pinToTop": "Fijar arriba",
"unpinFromTop": "Desfijar de arriba",
"pinToTop": "Anclar al principio",
"unpinFromTop": "Desanclar del principio",
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar estado de instalación para las aplicaciones seleccionadas?",
"installStatusOfXWillBeResetExplanation": "Se restaurará el estado de instalación de las aplicaciones seleccionadas.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
"customLinkMessage": "Estos enlaces funcionan en dispositivos con Obtainium instalado",
"shareAppConfigLinks": "Compartir la configuración de la aplicación como enlace HTML",
"shareSelectedAppURLs": "Compartir URLs de las aplicaciones seleccionadas",
"shareSelectedAppURLs": "Compartir URL de las aplicaciones seleccionadas",
"resetInstallStatus": "Restaurar estado de instalación",
"more": "Más",
"removeOutdatedFilter": "Elimiar filtro de aplicaciones desactualizado",
"removeOutdatedFilter": "Eliminar filtro de aplicaciones desactualizado",
"showOutdatedOnly": "Mostrar solo aplicaciones desactualizadas",
"filter": "Filtrar",
"filterApps": "Filtrar Actualizaciones",
"filterApps": "Filtrar actualizaciones",
"appName": "Nombre de la aplicación",
"author": "Autor",
"upToDateApps": "Aplicaciones Actualizadas",
"nonInstalledApps": "Aplicaciones No Instaladas",
"importExport": "Importar/Exportar",
"upToDateApps": "Aplicaciones actualizadas",
"nonInstalledApps": "Aplicaciones no instaladas",
"importExport": "Importar/exportar",
"settings": "Ajustes",
"exportedTo": "Exportado a {}",
"obtainiumExport": "Exportar Obtainium",
"invalidInput": "Input incorrecto",
"invalidInput": "Entrada no válida",
"importedX": "Importado {}",
"obtainiumImport": "Importar Obtainium",
"importFromURLList": "Importar desde lista de URLs",
"searchQuery": "Consulta de Búsqueda",
"appURLList": "Lista de URLs de Aplicaciones",
"importFromURLList": "Importar desde lista de URL",
"searchQuery": "Término de búsqueda",
"appURLList": "Lista de URL de aplicaciones",
"line": "Línea",
"searchX": "Buscar {}",
"noResults": "Resultados no encontrados",
"noResults": "No se encontró ningún resultado",
"importX": "Importar desde {}",
"importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como \"No Instalada\".\nPara solucionarlo, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
"importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como «No instalada».\nPara solucionarlo, reinstálelas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a los URL y a los métodos de importación mediante terceros.",
"importErrors": "Errores de Importación",
"importedXOfYApps": "{} de {} Aplicaciones importadas.",
"followingURLsHadErrors": "Las siguientes URLs han tenido problemas:",
"importedXOfYApps": "{} de {} aplicaciones importadas.",
"followingURLsHadErrors": "Los URL siguientes han tenido problemas:",
"selectURL": "Seleccionar URL",
"selectURLs": "Seleccionar URLs",
"pick": "Escoger",
@ -110,9 +110,9 @@
"dark": "Oscuro",
"light": "Claro",
"followSystem": "Seguir al sistema",
"useBlackTheme": "Negro puro en tema Oscuro",
"appSortBy": "Ordenar Apps Por",
"authorName": "Autor/Nombre",
"useBlackTheme": "Negro puro en tema oscuro",
"appSortBy": "Ordenar aplicaciones por",
"authorName": "Autor/nombre",
"nameAuthor": "Nombre/Autor",
"asAdded": "Según se Añadieron",
"appSortOrder": "Orden de Clasificación",
@ -122,22 +122,22 @@
"neverManualOnly": "Nunca, solo manual",
"appearance": "Apariencia",
"showWebInAppView": "Mostrar vista de la web de origen",
"pinUpdates": "Fijar actualizaciones al principio",
"pinUpdates": "Anclar actualizaciones al principio",
"updates": "Actualizaciones",
"sourceSpecific": "Fuente Específica",
"sourceSpecific": "Fuente específica",
"appSource": "Obtainium en GitHub",
"noLogs": "Sin Logs",
"appLogs": "Logs",
"noLogs": "Ningún registro",
"appLogs": "Registros",
"close": "Cerrar",
"share": "Compartir",
"appNotFound": "Aplicación no encontrada",
"obtainiumExportHyphenatedLowercase": "obtainium-exportación",
"pickAnAPK": "Seleccione una APK",
"appHasMoreThanOnePackage": "{} tiene más de un paquete:",
"deviceSupportsXArch": "Su dispositivo soporta las siguientes arquitecturas de procesador: {}.",
"deviceSupportsFollowingArchs": "Su dispositivo soporta las siguientes arquitecturas de procesador:",
"deviceSupportsXArch": "Su dispositivo admite las siguientes arquitecturas de procesador: {}.",
"deviceSupportsFollowingArchs": "Su dispositivo admite las siguientes arquitecturas de procesador:",
"warning": "Aviso",
"sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
"sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es «{}» pero el paquete de la actualización viene de «{}». ¿Desea continuar?",
"updatesAvailable": "Actualizaciones Disponibles",
"updatesAvailableNotifDescription": "Notifica al usuario de que hay actualizaciones para una o más aplicaciones monitoreadas por Obtainium",
"noNewUpdates": "No hay nuevas actualizaciones.",
@ -145,17 +145,17 @@
"appsUpdated": "Aplicaciones actualizadas",
"appsUpdatedNotifDescription": "Notifica al usuario de que una o más aplicaciones han sido actualizadas en segundo plano",
"xWasUpdatedToY": "{} ha sido actualizada a {}.",
"errorCheckingUpdates": "Error buscando ectualizaciones",
"errorCheckingUpdates": "Error al buscar actualizaciones",
"errorCheckingUpdatesNotifDescription": "Una notificación que muestra cuándo la comprobación de actualizaciones en segundo plano falla",
"appsRemoved": "Aplicaciones eliminadas",
"appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
"xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
"completeAppInstallation": "Instalación completa de la aplicación",
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierto para instalar aplicaciones",
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para terminar de instalar una aplicación",
"completeAppInstallationNotifDescription": "Le pide volver a Obtainium para terminar de instalar una aplicación",
"checkingForUpdates": "Buscando actualizaciones...",
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
"pleaseAllowInstallPerm": "Por favor, permita que Obtainium instale aplicaciones",
"pleaseAllowInstallPerm": "Permita que Obtainium instale aplicaciones",
"trackOnly": "Solo para seguimiento",
"errorWithHttpStatusCode": "Error {}",
"versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
@ -173,8 +173,8 @@
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
"fdroidThirdPartyRepo": "Repositorio de tercera parte F-Droid",
"steamMobile": "Móvil de vapor",
"steamChat": "Chat de vapor",
"steamMobile": "Steam para móviles",
"steamChat": "Chat de Steam",
"install": "Instalar",
"markInstalled": "Marcar como instalada",
"update": "Actualizar",
@ -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",
@ -192,14 +195,14 @@
"noCategory": "Sin categoría",
"noCategories": "Sin categorías",
"deleteCategoriesQuestion": "¿Eliminar categorías?",
"categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin categoría'.",
"categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas se marcarán como «Sin categoría».",
"addCategory": "Añadir categoría",
"label": "Nombre",
"language": "Idioma",
"copiedToClipboard": "Copiado al portapapeles",
"copiedToClipboard": "Se copió en el portapapeles",
"storagePermissionDenied": "Permiso de almacenamiento rechazado",
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaciones seleccionadas.",
"filterAPKsByRegEx": "Filtrar por APKs",
"filterAPKsByRegEx": "Filtrar por APK",
"removeFromObtainium": "Eliminar de Obtainium",
"uninstallFromDevice": "Desinstalar del dispositivo",
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
@ -212,8 +215,8 @@
"versionDetection": "Detección de versiones",
"standardVersionDetection": "Por versión",
"groupByCategory": "Agrupar por categoría",
"autoApkFilterByArch": "Filtrar APKs por arquitectura del procesador (si es posible)",
"overrideSource": "Sobrescribir Fuente",
"autoApkFilterByArch": "Filtrar APK por arquitectura del procesador (si es posible)",
"overrideSource": "Anular fuente",
"dontShowAgain": "No mostrar de nuevo",
"dontShowTrackOnlyWarnings": "No mostrar avisos sobre apps en 'solo seguimiento'",
"dontShowAPKOriginWarnings": "No mostrar avisos sobre las fuentes de las APKs",
@ -262,7 +265,7 @@
"takeFirstLink": "Usar primer enlace",
"skipSort": "Omitir orden",
"debugMenu": "Menu Depurar",
"bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
"bgTaskStarted": "Iniciada tarea en segundo plano; revise los registros.",
"runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
"versionExtractWholePage": "Aplicar la versión de extracción regex a la página entera",
"installing": "Instalando",
@ -297,23 +300,25 @@
"latest": "Versión más reciente",
"invertRegEx": "Invertir expresión regular",
"note": "Nota",
"selfHostedNote": "El desplegable \"{}\" puede utilizarse para acceder a instancias autoalojadas/personalizadas de cualquier fuente.",
"selfHostedNote": "El desplegable «{}» puede utilizarse para acceder a instancias autoalojadas/personalizadas de cualquier fuente.",
"badDownload": "No se ha podido analizar el APK (incompatible o descarga parcial)",
"beforeNewInstallsShareToAppVerifier": "Compartir nuevas aplicaciones con AppVerifier (si está disponible)",
"appVerifierInstructionToast": "Comparta con AppVerifier y vuelva aquí cuando esté listo.",
"removeAppQuestion": {
"one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?"
"one": "¿Eliminar aplicación?",
"other": "¿Eliminar aplicaciones?"
},
"tooManyRequestsTryAgainInMinutes": {
"one": "Muchas peticiones (limitado) - prueba de nuevo en {} minuto",
"other": "Muchas peticiones (limitado) - prueba de nuevo en {} minutos"
"one": "Muchas peticiones (limitado); pruebe de nuevo en {} minuto",
"other": "Muchas peticiones (limitado); pruebe de nuevo en {} minutos"
},
"bgUpdateGotErrorRetryInMinutes": {
"one": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minuto",
"other": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minutos"
"one": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}; se volverá a probar en {} minuto",
"other": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}; se volverá a probar en {} minutos"
},
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualización - se notificará al usuario si es necesario",
"other": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualizaciones - se notificará al usuario si es necesario"
"one": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualización; se le notificará si es necesario",
"other": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualizaciones; se le notificará si es necesario"
},
"apps": {
"one": "{} Aplicación",
@ -336,16 +341,16 @@
"other": "{} días"
},
"clearedNLogsBeforeXAfterY": {
"one": "Eliminado {n} log (previo a = {before}, posterior a = {after})",
"other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})"
"one": "Eliminado {n} registro (previo a = {before}, posterior a = {after})",
"other": "Eliminados {n} registros (previos a = {before}, posteriores a = {after})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} y 1 aplicación más tiene actualizaciones.",
"other": "{} y {} aplicaciones más tienen actualizaciones."
},
"xAndNMoreUpdatesInstalled": {
"one": "{} y 1 aplicación más han sido actualizadas.",
"other": "{} y {} aplicaciones más han sido actualizadas."
"one": "{} y 1 aplicación más se han actualizado.",
"other": "{} y {} aplicaciones más se han actualizado."
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} y 1 aplicación más podría haber sido actualizada.",

View File

@ -183,6 +183,9 @@
"disableVersionDetection": "غیرفعال کردن تشخیص نسخه",
"noVersionDetectionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند.",
"downloadingX": "در حال دانلود {}",
"downloadX": "Download {}",
"downloadedX": "Downloaded {}",
"releaseAsset": "Release Asset",
"downloadNotifDescription": "کاربر را از پیشرفت دانلود یک برنامه مطلع می کند",
"noAPKFound": "APK پیدا نشد فایل",
"noVersionDetection": "بدون تشخیص نسخه",
@ -299,6 +302,8 @@
"note": "Note",
"selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.",
"badDownload": "The APK could not be parsed (incompatible or partial download)",
"beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)",
"appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"

View File

@ -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",
@ -299,6 +302,8 @@
"note": "Note",
"selfHostedNote": "La liste déroulante \"{}\" peut être utilisée pour accéder aux instances auto-hébergées/personnalisées de n'importe quelle source.",
"badDownload": "L'APK n'a pas pu être analysé (téléchargement incompatible ou partiel)",
"beforeNewInstallsShareToAppVerifier": "Partager les nouvelles applications avec AppVerifier (si disponible)",
"appVerifierInstructionToast": "Partagez avec AppVerifier, puis revenez ici lorsque vous êtes prêt.",
"removeAppQuestion": {
"one": "Supprimer l'application ?",
"other": "Supprimer les applications ?"

View File

@ -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": "Kiadási tartalom",
"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",
@ -218,7 +221,7 @@
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
"moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
"gitlabPATLabel": "GitLab Personal Access Token",
"gitlabPATLabel": "GitLab személyes hozzáférési token",
"about": "Rólunk",
"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",
@ -299,6 +302,8 @@
"note": "Megjegyzés:",
"selfHostedNote": "A \"{}\" legördülő menü használható bármely forrás saját üzemeltetésű/egyéni példányainak eléréséhez.",
"badDownload": "Az APK-t nem lehetett elemezni (inkompatibilis vagy részleges letöltés)",
"beforeNewInstallsShareToAppVerifier": "Új alkalmazások megosztása az AppVerifierrel (ha elérhető)",
"appVerifierInstructionToast": "Ossza meg az AppVerifierrel, majd térjen vissza ide, ha kész.",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazásokat?"

View File

@ -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",
@ -218,7 +221,7 @@
"dontShowTrackOnlyWarnings": "Non mostrare gli avvisi 'Solo-Monitoraggio'",
"dontShowAPKOriginWarnings": "Non mostrare gli avvisi di origine dell'APK",
"moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista",
"gitlabPATLabel": "GitLab Personal Access Token",
"gitlabPATLabel": "GitLab Token di accesso personale",
"about": "Informazioni",
"requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)",
"checkOnStart": "Controlla una volta all'avvio",
@ -299,6 +302,8 @@
"note": "Nota",
"selfHostedNote": "Il menu a tendina \"{}\" può essere usato per raggiungere istanze autogestite/personali di qualsiasi fonte.",
"badDownload": "Non è stato possibile analizzare l'APK (download incompatibile o parziale).",
"beforeNewInstallsShareToAppVerifier": "Condividere le nuove applicazioni con AppVerifier (se disponibile)",
"appVerifierInstructionToast": "Condividete con AppVerifier, quindi tornate qui quando siete pronti.",
"removeAppQuestion": {
"one": "Rimuovere l'app?",
"other": "Rimuovere le app?"

View File

@ -183,6 +183,9 @@
"disableVersionDetection": "バージョン検出を無効にする",
"noVersionDetectionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリにのみ使用する必要があります。",
"downloadingX": "{} をダウンロード中",
"downloadX": "ダウンロード",
"downloadedX": "ダウンロード",
"releaseAsset": "リリース資産",
"downloadNotifDescription": "アプリのダウンロード状況を通知する",
"noAPKFound": "APKが見つかりません",
"noVersionDetection": "バージョン検出を行わない",
@ -299,6 +302,8 @@
"note": "注",
"selfHostedNote": "ドロップダウン\"{}\"を使用すると、あらゆるソースのセルフホスト/カスタムインスタンスにアクセスできます。",
"badDownload": "APK を解析できませんでした(互換性がないか、部分的にダウンロードされています)。",
"beforeNewInstallsShareToAppVerifier": "AppVerifierで新しいアプリを共有する利用可能な場合",
"appVerifierInstructionToast": "AppVerifierに共有し、準備ができたらここに戻ってください。",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"

View File

@ -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",
@ -218,7 +221,7 @@
"dontShowTrackOnlyWarnings": "Geen waarschuwingen voor 'Track-Only' weergeven",
"dontShowAPKOriginWarnings": "APK-herkomstwaarschuwingen niet weergeven",
"moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave",
"gitlabPATLabel": "GitLab Personal Access Token",
"gitlabPATLabel": "GitLab persoonlijk toegangskenmerk",
"about": "Over",
"requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)",
"checkOnStart": "Controleren op updates bij opstarten",
@ -299,6 +302,8 @@
"note": "Opmerking",
"selfHostedNote": "De \"{}\" dropdown kan gebruikt worden om zelf gehoste/aangepaste instanties van elke bron te bereiken.",
"badDownload": "De APK kon niet worden verwerkt (incompatibele of gedeeltelijke download)",
"beforeNewInstallsShareToAppVerifier": "Nieuwe Apps delen met AppVerifier (indien beschikbaar)",
"appVerifierInstructionToast": "Deel naar AppVerifier en keer hier terug als je klaar bent.",
"removeAppQuestion": {
"one": "App verwijderen?",
"other": "Apps verwijderen?"

View File

@ -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",
@ -299,6 +302,8 @@
"note": "Uwaga",
"selfHostedNote": "Lista rozwijana \"{}\" może być używana do uzyskiwania dostępu do samodzielnie hostowanych / niestandardowych instancji dowolnego źródła.",
"badDownload": "Nie można przeanalizować pliku APK (niekompatybilny lub częściowo pobrany).",
"beforeNewInstallsShareToAppVerifier": "Udostępnianie nowych aplikacji za pomocą AppVerifier (jeśli dostępne)",
"appVerifierInstructionToast": "Udostępnij w AppVerifier, a następnie wróć tutaj, gdy będziesz gotowy.",
"removeAppQuestion": {
"one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?",

View File

@ -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",
@ -299,6 +302,8 @@
"note": "Nota",
"selfHostedNote": "O menu suspenso \"{}\" pode ser usado para acessar instâncias auto-hospedadas/personalizadas de qualquer fonte.",
"badDownload": "Não foi possível analisar o APK (transferência incompatível ou parcial)",
"beforeNewInstallsShareToAppVerifier": "Partilhar novas aplicações com o AppVerifier (se disponível)",
"appVerifierInstructionToast": "Partilhe com o AppVerifier e, em seguida, regresse aqui quando estiver pronto.",
"removeAppQuestion": {
"one": "Remover aplicativo?",
"other": "Remover aplicativos?"

View File

@ -183,6 +183,9 @@
"disableVersionDetection": "Отключить обнаружение версии",
"noVersionDetectionExplanation": "Эта настройка должна использоваться только для приложений, где обнаружение версии не работает корректно",
"downloadingX": "Загрузка {}",
"downloadX": "Скачать {}",
"downloadedX": "Загружено {}",
"releaseAsset": "Освобождение актива",
"downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения",
"noAPKFound": "APK не найден",
"noVersionDetection": "Обнаружение версий отключено",
@ -299,6 +302,8 @@
"note": "Примечание",
"selfHostedNote": "Выпадающий список \"{}\" можно использовать для доступа к самостоятельно размещенным/настроенным экземплярам любого источника.",
"badDownload": "APK не удалось разобрать (несовместимая или неполная загрузка)",
"beforeNewInstallsShareToAppVerifier": "Поделитесь новыми приложениями с AppVerifier (если доступно)",
"appVerifierInstructionToast": "Поделитесь с AppVerifier, а затем вернитесь сюда, когда будете готовы.",
"removeAppQuestion": {
"one": "Удалить приложение?",
"other": "Удалить приложения?"

View File

@ -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",
@ -218,7 +221,7 @@
"dontShowTrackOnlyWarnings": "Visa inte 'Följ-Endast' varningar",
"dontShowAPKOriginWarnings": "Visa inte APK-ursprung varningar",
"moveNonInstalledAppsToBottom": "Flytta icke-installerade appar till botten av appvyn",
"gitlabPATLabel": "GitLab Personal Access Token",
"gitlabPATLabel": "Personligt åtkomsttoken för GitLab",
"about": "Om",
"requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
"checkOnStart": "Kolla efter uppdateringar vid start",
@ -299,6 +302,8 @@
"note": "Anmärkning",
"selfHostedNote": "Rullgardinsmenyn \"{}\" kan användas för att nå självhostade/anpassade instanser av valfri källa.",
"badDownload": "APK kunde inte analyseras (inkompatibel eller partiell nedladdning)",
"beforeNewInstallsShareToAppVerifier": "Dela nya appar med AppVerifier (om tillgängligt)",
"appVerifierInstructionToast": "Dela till AppVerifier och återvänd sedan hit när du är klar.",
"removeAppQuestion": {
"one": "Ta Bort App?",
"other": "Ta Bort Appar?"

View File

@ -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",
@ -299,6 +302,8 @@
"note": "Not",
"selfHostedNote": "\"{}\" açılır menüsü, herhangi bir kaynağın kendi kendine barındırılan/özel örneklerine ulaşmak için kullanılabilir.",
"badDownload": "APK ayrıştırılamadı (uyumsuz veya kısmi indirme)",
"beforeNewInstallsShareToAppVerifier": "Yeni Uygulamaları AppVerifier ile paylaşın (varsa)",
"appVerifierInstructionToast": "AppVerifier ile paylaşın, hazır olduğunuzda buraya dönün.",
"removeAppQuestion": {
"one": "Uygulamayı Kaldır?",
"other": "Uygulamaları Kaldır?"

View File

@ -183,6 +183,9 @@
"disableVersionDetection": "Вимкнути визначення версії",
"noVersionDetectionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно.",
"downloadingX": "Завантаження {}",
"downloadX": "Завантажити {}",
"downloadedX": "Завантажено {}",
"releaseAsset": "Звільнити актив",
"downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку",
"noAPKFound": "APK не знайдено",
"noVersionDetection": "Визначення версії відключено",
@ -232,7 +235,6 @@
"addInfoBelow": "Додати цю інформацію нижче.",
"addInfoInSettings": "Додати цю інформацію у налаштуваннях.",
"githubSourceNote": "Лімітування швидкості GitHub можна уникнути, використовуючи ключ API.",
"gitlabSourceNote": "Вилучення APK з GitLab може не працювати без ключа API.",
"sortByLastLinkSegment": "Сортувати лише за останнім сегментом посилання",
"filterReleaseNotesByRegEx": "Фільтрувати примітки до релізу за регулярним виразом",
"customLinkFilterRegex": "Фільтр кастомного посилання на APK за регулярним виразом (за замовчуванням '.apk$')",
@ -300,6 +302,8 @@
"note": "Примітка",
"selfHostedNote": "Випадаючий список \"{}\" може використовуватися для доступу до власних/призначених для самостійного використання екземплярів будь-якого джерела.",
"badDownload": "APK не вдалося розпарсити (несумісний або часткове завантаження)",
"beforeNewInstallsShareToAppVerifier": "Діліться новими додатками з AppVerifier (якщо доступно)",
"appVerifierInstructionToast": "Надішліть на AppVerifier, а потім поверніться сюди, коли будете готові.",
"removeAppQuestion": {
"one": "Видалити застосунок?",
"other": "Видалити застосунки?"

View File

@ -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,6 +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": "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?"

View File

@ -183,6 +183,9 @@
"disableVersionDetection": "禁用版本检测",
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
"downloadingX": "正在下载“{}”",
"downloadX": "下载 {}",
"downloadedX": "下载 {}",
"releaseAsset": "释放资产",
"downloadNotifDescription": "提示应用的下载进度",
"noAPKFound": "未找到 APK 文件",
"noVersionDetection": "禁用版本检测",
@ -299,6 +302,8 @@
"note": "备注",
"selfHostedNote": "可以通过“{}”下拉菜单来指向任意来源的自托管/自定义实例。",
"badDownload": "无法解析 APK 文件(不兼容或文件不完整)",
"beforeNewInstallsShareToAppVerifier": "与 AppVerifier 共享新应用程序(如有)",
"appVerifierInstructionToast": "分享到 AppVerifier准备就绪后返回此处。",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"

View File

@ -271,17 +271,14 @@ class GitHub extends AppSource {
}
}
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?)
?.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<MapEntry<String, String>> getReleaseAssetUrls(dynamic release) =>
(release['assets'] as List<dynamic>?)?.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<MapEntry<String, String>> 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<MapEntry<String, String>>,
getAppNames(standardUrl),
releaseDate: releaseDate,
changeLog: changeLog.isEmpty ? null : changeLog);
changeLog: changeLog.isEmpty ? null : changeLog,
allAssetUrls:
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>);
} else {
if (onHttpErrorCode != null) {
onHttpErrorCode(res);

View File

@ -121,9 +121,11 @@ class GitLab extends AppSource {
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
bool trackOnly = additionalSettings['trackOnly'] == true;
// Request data from REST API
Response res = await sourceRequest(
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/releases?$optionalAuth',
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
additionalSettings);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
@ -152,9 +154,8 @@ class GitLab extends AppSource {
var apkUrlsSet = apkUrlsFromAssets.toSet();
apkUrlsSet.addAll(uploadedAPKsFromDescription);
var releaseDateString = e['released_at'] ?? e['created_at'];
DateTime? releaseDate = releaseDateString != null
? DateTime.parse(releaseDateString)
: null;
DateTime? releaseDate =
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
return APKDetails(
e['tag_name'] ?? e['name'],
getApkUrlsFromUrls(apkUrlsSet.toList()),
@ -164,20 +165,31 @@ class GitLab extends AppSource {
if (apkDetailsList.isEmpty) {
throw NoReleasesError();
}
var finalResult = apkDetailsList.first;
// Fallback procedure
bool fallbackToOlderReleases =
additionalSettings['fallbackToOlderReleases'] == true;
if (fallbackToOlderReleases) {
if (additionalSettings['trackOnly'] != true) {
apkDetailsList =
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
}
if (apkDetailsList.isEmpty) {
throw NoReleasesError();
}
if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) {
apkDetailsList =
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
finalResult = apkDetailsList.first;
}
return apkDetailsList.first;
if (finalResult.apkUrls.isEmpty && !trackOnly) {
throw NoAPKError();
}
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;
}
}

View File

@ -245,8 +245,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
void someValueChanged({bool isBuilding = false, bool forceInvalid = false}) {
Map<String, dynamic> returnValues = values;
var valid = true;
for (int r = 0; r < widget.items.length; r++) {
for (int i = 0; i < widget.items[r].length; i++) {
for (int r = 0; r < formInputs.length; r++) {
for (int i = 0; i < formInputs[r].length; i++) {
if (formInputs[r][i] is TextFormField) {
valid = valid && validateTextField(formInputs[r][i] as TextFormField);
}

View File

@ -155,7 +155,8 @@ class AddAppPageState extends State<AddAppPage> {
// 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'));
}

View File

@ -158,6 +158,29 @@ class _AppPageState extends State<AppPage> {
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,
),

View File

@ -854,69 +854,78 @@ class AppsPageState extends State<AppsPage> {
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 =
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
for (var a in selectedApps) {
urls +=
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
'id': a.id,
'url': a.url,
'author': a.author,
'name': a.name,
'preferredApkIndex':
a.preferredApkIndex,
'additionalSettings':
jsonEncode(a.additionalSettings)
}))}">${a.name}</a></li>\n';
}
urls +=
'</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
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 =
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
for (var a in selectedApps) {
urls +=
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
'id': a.id,
'url': a.url,
'author': a.author,
'name': a.name,
'preferredApkIndex':
a.preferredApkIndex,
'additionalSettings':
jsonEncode(a.additionalSettings)
}))}">${a.name}</a></li>\n';
}
urls +=
'</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
Share.share(urls,
subject: 'Obtainium - ${tr('appsString')}');
},
tooltip: tr('shareAppConfigLinks'),
icon: const Icon(Icons.ios_share),
),
child: Text(tr('markSelectedAppsUpdated'))),
]),
),
);

View File

@ -351,6 +351,22 @@ class _SettingsPageState extends State<SettingsPage> {
],
),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child:
Text(tr('removeOnExternalUninstall'))),
Switch(
value: settingsProvider
.removeOnExternalUninstall,
onChanged: (value) {
settingsProvider
.removeOnExternalUninstall = value;
})
],
),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -363,6 +379,43 @@ class _SettingsPageState extends State<SettingsPage> {
})
],
),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(tr(
'beforeNewInstallsShareToAppVerifier')),
GestureDetector(
onTap: () {
launchUrlString(
'https://github.com/soupslurpr/AppVerifier',
mode: LaunchMode
.externalApplication);
},
child: Text(
tr('about'),
style: const TextStyle(
decoration:
TextDecoration.underline,
fontSize: 12),
)),
],
)),
Switch(
value: settingsProvider
.beforeNewInstallsShareToAppVerifier,
onChanged: (value) {
settingsProvider
.beforeNewInstallsShareToAppVerifier =
value;
})
],
),
installMethodDropdown,
height32,
Text(
@ -474,22 +527,6 @@ class _SettingsPageState extends State<SettingsPage> {
],
),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child:
Text(tr('removeOnExternalUninstall'))),
Switch(
value: settingsProvider
.removeOnExternalUninstall,
onChanged: (value) {
settingsProvider
.removeOnExternalUninstall = value;
})
],
),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@ -5,6 +5,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart';
@ -31,6 +32,7 @@ import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart';
import 'package:share_plus/share_plus.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
import 'native_provider.dart';
@ -289,16 +291,17 @@ Future<File> downloadFile(
return s;
}).pipe(sink);
await sink.close();
bool likelyCorruptFile = (progress ?? 0) > 101;
progress = null;
if (onProgress != null) {
onProgress(progress);
}
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode < 200 ||
response.statusCode > 299 ||
likelyCorruptFile) {
tempDownloadedFile.deleteSync(recursive: true);
throw response.reasonPhrase ?? tr('unexpectedError');
}
print(tempDownloadedFile.lengthSync());
print(fullContentLength);
if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.renameSync(downloadedFile.path);
}
@ -357,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();
@ -563,7 +566,8 @@ class AppsProvider with ChangeNotifier {
zipFile: File(filePath), destinationDir: Directory(destinationPath));
}
Future<bool> installXApkDir(DownloadedXApkDir dir,
Future<bool> installXApkDir(
DownloadedXApkDir dir, BuildContext? firstTimeWithContext,
{bool needsBGWorkaround = false}) async {
// We don't know which APKs in an XAPK are supported by the user's device
// So we try installing all of them and assume success if at least one installed
@ -577,7 +581,8 @@ class AppsProvider with ChangeNotifier {
if (file.path.toLowerCase().endsWith('.apk')) {
try {
somethingInstalled = somethingInstalled ||
await installApk(DownloadedApk(dir.appId, file),
await installApk(
DownloadedApk(dir.appId, file), firstTimeWithContext,
needsBGWorkaround: needsBGWorkaround);
} catch (e) {
logs.add(
@ -599,8 +604,19 @@ class AppsProvider with ChangeNotifier {
return somethingInstalled;
}
Future<bool> installApk(DownloadedApk file,
Future<bool> installApk(
DownloadedApk file, BuildContext? firstTimeWithContext,
{bool needsBGWorkaround = false}) async {
if (firstTimeWithContext != null &&
settingsProvider.beforeNewInstallsShareToAppVerifier &&
(await getInstalledInfo('dev.soupslurpr.appverifier')) != null) {
XFile f = XFile.fromData(file.file.readAsBytesSync(),
mimeType: 'application/vnd.android.package-archive');
Fluttertoast.showToast(
msg: tr('appVerifierInstructionToast'),
toastLength: Toast.LENGTH_LONG);
await Share.shareXFiles([f]);
}
var newInfo =
await pm.getPackageArchiveInfo(archiveFilePath: file.file.path);
if (newInfo == null) {
@ -648,7 +664,13 @@ class AppsProvider with ChangeNotifier {
}
bool installed = false;
if (code != null && code != 0 && code != 3) {
throw InstallError(code);
try {
file.file.deleteSync(recursive: true);
} catch (e) {
//
} finally {
throw InstallError(code);
}
} else if (code == 0) {
installed = true;
apps[file.appId]!.app.installedVersion =
@ -683,23 +705,28 @@ class AppsProvider with ChangeNotifier {
await intent.launch();
}
Future<MapEntry<String, String>?> confirmApkUrl(
App app, BuildContext? context) async {
Future<MapEntry<String, String>?> 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<String, String>? apkUrl =
app.apkUrls[app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
MapEntry<String, String>? appFileUrl = urlsToSelectFrom[
app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
// get device supported architecture
List<String> 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,
);
});
}
@ -709,8 +736,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) &&
@ -719,13 +746,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
@ -752,7 +779,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]!
@ -830,17 +857,23 @@ class AppsProvider with ChangeNotifier {
try {
if (!skipInstalls) {
bool sayInstalled = true;
var contextIfNewInstall =
apps[id]?.installedInfo == null ? context : null;
if (downloadedFile != null) {
if (willBeSilent && context == null) {
installApk(downloadedFile, needsBGWorkaround: true);
installApk(downloadedFile, contextIfNewInstall,
needsBGWorkaround: true);
} else {
sayInstalled = await installApk(downloadedFile);
sayInstalled =
await installApk(downloadedFile, contextIfNewInstall);
}
} else {
if (willBeSilent && context == null) {
installXApkDir(downloadedDir!, needsBGWorkaround: true);
installXApkDir(downloadedDir!, contextIfNewInstall,
needsBGWorkaround: true);
} else {
sayInstalled = await installXApkDir(downloadedDir!);
sayInstalled =
await installXApkDir(downloadedDir!, contextIfNewInstall);
}
}
if (willBeSilent && context == null) {
@ -883,6 +916,85 @@ class AppsProvider with ChangeNotifier {
return installedIds;
}
Future<List<String>> downloadAppAssets(
List<String> appIds, BuildContext context,
{bool forceParallelDownloads = false}) async {
NotificationsProvider notificationsProvider =
context.read<NotificationsProvider>();
List<MapEntry<MapEntry<String, String>, App>> filesToDownload = [];
for (var id in appIds) {
if (apps[id] == null) {
throw ObtainiumError(tr('appNotFound'));
}
MapEntry<String, String>? 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<String> downloadedIds = [];
Future<void> downloadFn(MapEntry<String, String> 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<Directory> getAppsDir() async {
Directory appsDir =
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
@ -1454,38 +1566,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<String, String>? initVal;
final List<String>? archs;
final bool pickAnyAsset;
@override
State<APKPicker> createState() => _APKPickerState();
State<AppFilePicker> createState() => _AppFilePickerState();
}
class _APKPickerState extends State<APKPicker> {
MapEntry<String, String>? apkUrl;
class _AppFilePickerState extends State<AppFilePicker> {
MapEntry<String, String>? 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<String>(
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;
});
}),
),
@ -1512,7 +1635,7 @@ class _APKPickerState extends State<APKPicker> {
TextButton(
onPressed: () {
HapticFeedback.selectionClick();
Navigator.of(context).pop(apkUrl);
Navigator.of(context).pop(fileUrl);
},
child: Text(tr('continue')))
],

View File

@ -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'),

View File

@ -479,4 +479,13 @@ class SettingsProvider with ChangeNotifier {
prefs?.setStringList('searchDeselected', list);
notifyListeners();
}
bool get beforeNewInstallsShareToAppVerifier {
return prefs?.getBool('beforeNewInstallsShareToAppVerifier') ?? true;
}
set beforeNewInstallsShareToAppVerifier(bool val) {
prefs?.setBool('beforeNewInstallsShareToAppVerifier', val);
notifyListeners();
}
}

View File

@ -47,9 +47,10 @@ class APKDetails {
late AppNames names;
late DateTime? releaseDate;
late String? changeLog;
late List<MapEntry<String, String>> allAssetUrls;
APKDetails(this.version, this.apkUrls, this.names,
{this.releaseDate, this.changeLog});
{this.releaseDate, this.changeLog, this.allAssetUrls = const []});
}
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
@ -223,6 +224,7 @@ class App {
String? installedVersion;
late String latestVersion;
List<MapEntry<String, String>> apkUrls = [];
List<MapEntry<String, String>> otherAssetUrls = [];
late int preferredApkIndex;
late Map<String, dynamic> 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<String, dynamic> 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<String, dynamic>,
json['lastUpdateCheck'] == null
? null
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
json['pinned'] ?? false,
categories: json['categories'] != null
? (json['categories'] as List<dynamic>)
.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<String, dynamic>,
json['lastUpdateCheck'] == null
? null
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
json['pinned'] ?? false,
categories: json['categories'] != null
? (json['categories'] as List<dynamic>)
.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<String, dynamic> 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,
@ -736,7 +743,6 @@ class SourceProvider {
FDroid(),
FDroidRepo(),
IzzyOnDroid(),
SourceForge(),
SourceHut(),
APKPure(),
Aptoide(),
@ -893,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);
}

View File

@ -5,10 +5,10 @@ packages:
dependency: "direct main"
description:
name: android_intent_plus
sha256: e1c62bb41c90e15083b7fb84dc327fe90396cc9c1445b55ff1082144fabfb4d9
sha256: "2bfdbee8d65e7c26f88b66f0a91f2863da4d3596d8a658b4162c8de5cf04b074"
url: "https://pub.dev"
source: hosted
version: "4.0.3"
version: "5.0.2"
android_package_installer:
dependency: "direct main"
description:
@ -38,10 +38,10 @@ packages:
dependency: "direct main"
description:
name: app_links
sha256: fd7fc1569870b4b0d90d17a9f36661a6ff92400fecb6e4adab4abe0f0488bb5f
sha256: "42dc15aecf2618ace4ffb74a2e58a50e45cd1b9f2c17c8f0cafe4c297f08c815"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "4.0.1"
archive:
dependency: transitive
description:
@ -54,10 +54,10 @@ packages:
dependency: transitive
description:
name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.5.0"
async:
dependency: transitive
description:
@ -70,10 +70,10 @@ packages:
dependency: "direct main"
description:
name: background_fetch
sha256: eb3af263d390d7e68ecb90f2ae984d2bfd96dceb4c7b4f72418dd5383b49de0a
sha256: dbffec0317ccdef6e2014cb543e147f52441e29c4fcb53dfd23558c4d92ddece
url: "https://pub.dev"
source: hosted
version: "1.2.4"
version: "1.3.2"
boolean_selector:
dependency: transitive
description:
@ -126,18 +126,18 @@ packages:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
sha256: ebe15d94de9dd7c31dc2ac54e42780acdf3384b1497c69290c9f3c5b0279fc57
url: "https://pub.dev"
source: hosted
version: "5.0.2"
version: "6.0.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb
url: "https://pub.dev"
source: hosted
version: "1.2.4"
version: "2.0.0"
convert:
dependency: transitive
description:
@ -190,10 +190,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91
url: "https://pub.dev"
source: hosted
version: "9.1.2"
version: "10.1.0"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -254,10 +254,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: caa6bc229eab3e32eb2f37b53a5f9d22a6981474afd210c512a7546c1e1a04f6
sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03
url: "https://pub.dev"
source: hosted
version: "6.2.0"
version: "8.0.0+1"
fixnum:
dependency: transitive
description:
@ -299,18 +299,18 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1
sha256: a701df4866f9a38bb8e4450a54c143bbeeb0ce2381e7df5a36e1006f3b43bb28
url: "https://pub.dev"
source: hosted
version: "17.0.0"
version: "17.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -336,18 +336,18 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: cb44f7831b23a6bdd0f501718b0d2e8045cbc625a15f668af37ddb80314821db
sha256: "31c12de79262b5431c5492e9c89948aa789158435f707d3519a7fdef6af28af7"
url: "https://pub.dev"
source: hosted
version: "0.6.21"
version: "0.6.22+1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
url: "https://pub.dev"
source: hosted
version: "2.0.17"
version: "2.0.19"
flutter_test:
dependency: "direct dev"
description: flutter
@ -426,10 +426,10 @@ packages:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.6.7"
version: "0.7.1"
json_annotation:
dependency: transitive
description:
@ -538,18 +538,18 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.2.4"
path_provider_foundation:
dependency: transitive
description:
@ -586,10 +586,10 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44"
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.dev"
source: hosted
version: "11.3.0"
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
@ -602,10 +602,10 @@ packages:
dependency: transitive
description:
name: permission_handler_apple
sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b
sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
url: "https://pub.dev"
source: hosted
version: "9.4.0"
version: "9.4.4"
permission_handler_html:
dependency: transitive
description:
@ -618,10 +618,10 @@ packages:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78"
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
version: "4.2.1"
permission_handler_windows:
dependency: transitive
description:
@ -658,10 +658,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:
@ -674,34 +674,34 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
sha256: fb5319f3aab4c5dda5ebb92dca978179ba21f8c783ee4380910ef4c1c6824f51
url: "https://pub.dev"
source: hosted
version: "7.2.2"
version: "8.0.3"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
version: "3.4.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
@ -775,10 +775,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:
@ -855,18 +855,18 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
url: "https://pub.dev"
source: hosted
version: "6.2.5"
version: "6.2.6"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
url_launcher_ios:
dependency: transitive
description:
@ -919,10 +919,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
url: "https://pub.dev"
source: hosted
version: "4.3.3"
version: "4.4.0"
vector_math:
dependency: transitive
description:
@ -983,10 +983,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:

View File

@ -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.0.6+2256 # When changing this, update the tag in main() accordingly
version: 1.1.3+2260
environment:
sdk: '>=3.0.0 <4.0.0'
@ -48,22 +48,22 @@ dependencies:
url_launcher: ^6.1.5
permission_handler: ^11.0.0
fluttertoast: ^8.0.9
device_info_plus: ^9.0.0
file_picker: ^6.0.0
device_info_plus: ^10.0.1
file_picker: ^8.0.0+1
animations: ^2.0.4
android_package_installer:
git:
url: https://github.com/ImranR98/android_package_installer
ref: main
android_package_manager: ^0.7.0
share_plus: ^7.0.0
share_plus: ^8.0.2
sqflite: ^2.2.0+3
easy_localization: ^3.0.1
android_intent_plus: ^4.0.0
android_intent_plus: ^5.0.1
flutter_markdown: ^0.6.14
flutter_archive: ^6.0.0
hsluv: ^1.1.3
connectivity_plus: ^5.0.0
connectivity_plus: ^6.0.1
shared_storage: ^0.8.0
crypto: ^3.0.3
app_links: ^4.0.0