Compare commits

...

59 Commits

Author SHA1 Message Date
f80c9ec33e Merge pull request #563 from ImranR98/dev
Flutter version - related change
2023-05-13 02:36:09 -04:00
7681e23de9 Flutter version - related change 2023-05-13 02:35:44 -04:00
22a60df40e Merge pull request #562 from ImranR98/dev
Fixed breaking bug for some sources (#561)
2023-05-13 02:32:23 -04:00
431a01f2a5 Fixed breaking bug for some sources (#561) 2023-05-13 02:32:06 -04:00
0cd4385de7 Merge pull request #544 from LilligantMatsuri/main
Update zh.json
2023-05-12 18:02:58 -04:00
0774b3ddc3 Merge pull request #558 from iDazai/patch-1
Update de.json
2023-05-12 18:02:52 -04:00
b60b1ed058 Merge pull request #560 from ImranR98/dev
XAPK Bugfixes #541, HTML User-Agent #545, Better APK Cleanup #551, Search UI Improvements #550
2023-05-12 18:02:21 -04:00
b196715d60 Search UI improvements 2023-05-12 18:00:21 -04:00
0673e90dff Better APK cleanup 2023-05-12 17:53:07 -04:00
59cfa242fb Update de.json
translate newly added English text
improved some German text
2023-05-10 18:20:40 +02:00
65ab72ba90 Increment version 2023-05-09 00:40:39 -04:00
408bca8951 XAPK bugfixes, HTML default User-Agent 2023-05-09 00:37:06 -04:00
480467492a Update zh.json
- Translate new strings
- Slight improvements

Signed-off-by: Matsuri <matsuri@vmoe.info>
2023-05-07 22:00:02 +08:00
219b04aedb Merge pull request #538 from bluefly000/japanese-translation
Update ja.json
2023-05-06 14:43:26 -04:00
a0709856ef Merge branch 'main' into japanese-translation 2023-05-06 14:43:14 -04:00
577642850f Merge pull request #542 from ImranR98/dev
Add (Incomplete) XAPK Support (#541), Auto-Check Updates on Start (#539), UI Tweaks (#540)
2023-05-06 14:42:46 -04:00
e1db024034 Increment version 2023-05-06 14:40:14 -04:00
cc268aeeda "Check updates on start" toggle 2023-05-06 14:25:17 -04:00
d5f7eced8b UI tweaks 2023-05-06 13:28:41 -04:00
cc3c4cc79f Add XAPK support (incomplete - OBB not copied) 2023-05-06 13:20:58 -04:00
89b61884f1 Update ja.json 2023-05-06 15:52:23 +09:00
33d3fc2d8e Merge pull request #537 from ImranR98/dev
APKPure Bugfix
2023-05-06 01:38:17 -04:00
b07f5dd6b6 APKPure Bugfix 2023-05-06 01:37:51 -04:00
b43e13bb56 Merge pull request #536 from ImranR98/dev
Slight UI improvement on Add App page
2023-05-06 01:26:24 -04:00
3be5543df4 Slight UI improvement on Add App page 2023-05-06 01:25:39 -04:00
91ad9efa43 Merge pull request #535 from ImranR98/dev
Add APKPure (#531), SourceHut (#483), GitLab Search (#422), Sorting Option (#264), Bug Workaround (#534)
2023-05-06 00:39:16 -04:00
ee292146d1 Better GitHub release sorting in some cases (#534) 2023-05-06 00:30:46 -04:00
12867634b6 Increment version 2023-05-06 00:13:05 -04:00
2e4fe89b85 APKPure bugfix, upgrade packages 2023-05-06 00:12:25 -04:00
b4642e16ad GitLab search (#422) + better settings UI 2023-05-06 00:06:48 -04:00
8ca5964d31 Updated README 2023-05-05 23:09:40 -04:00
30c89fe385 Option to move non-installed apps to bottom (#264) 2023-05-05 23:08:34 -04:00
fb9e66332d APKPure, SourceHut, Bugfixes 2023-05-05 22:35:32 -04:00
84b512f282 Merge pull request #530 from ImranR98/dev
Added F-Droid search (#526) + search UI improvements
2023-05-03 18:41:44 -04:00
6f9aa85a72 Merge remote-tracking branch 'origin/main' into dev 2023-05-03 18:41:03 -04:00
639fc20fcb Added F-Droid search (#526) + search UI improvements 2023-05-03 18:40:34 -04:00
75631e5c5a Merge pull request #529 from ImranR98/dev
SourceForge URL flexibility (#525), Add language names, enable Spanish
2023-05-03 18:02:05 -04:00
9ec345761e Actually increment version 2023-05-03 18:01:47 -04:00
1f9c2c1699 Update packages, increment version 2023-05-03 18:00:57 -04:00
cbec486ad1 Add language names and enable Spanish 2023-05-03 18:00:24 -04:00
85ef60d4a8 Merge pull request #524 from mehdijahann/main
Update fa.json
2023-05-03 17:50:54 -04:00
44bde571bf Merge pull request #522 from markus-gitdev/main
Update de.json
2023-05-03 17:50:46 -04:00
eaaee5e7cd Merge pull request #523 from bluefly000/japanese-translation
Update ja.json
2023-05-03 17:50:38 -04:00
e1980f4de2 SourceForge URL flexibility (#525) 2023-05-03 17:49:50 -04:00
be9c671a56 Update fa.json 2023-05-03 09:06:44 +08:00
0404449842 Update fa.json 2023-05-03 09:03:55 +08:00
d6366a145e Update ja.json 2023-05-02 13:38:22 +09:00
0a751cf545 Update de.json
Correction of "changeX".
New translations for:
- "overrideSource"
- "dontShowAgain"
- "dontShowTrackOnlyWarnings"
- "dontShowAPKOriginWarnings"
2023-05-01 13:58:46 +02:00
5885ea57ad Merge pull request #519 from ImranR98/dev
Add Jenkins jobs as a Source (#514), switch to Apps page after App added (#508), Bugfixes (#510)
2023-04-30 17:05:28 -04:00
f8b326529f Add Jenkins to README 2023-04-30 17:03:54 -04:00
9f5f1174ba Increment version 2023-04-30 17:02:33 -04:00
779de58f74 Jenkins uses release dates only + APK delete bugfix 2023-04-30 17:00:00 -04:00
76e316422c Added Jenkins Source (#514) 2023-04-30 16:28:09 -04:00
36273fe02d Switch to apps tab after app added (#508) 2023-04-30 15:49:25 -04:00
03b592521c Fixed link sorting for HTML Source 2023-04-30 15:39:21 -04:00
a5ef47a060 Merge pull request #516 from gidano/main
Update hu.json
2023-04-30 14:40:26 -04:00
289c801fec Merge pull request #511 from ThePhoDit/es-translations
Spanish translations
2023-04-30 14:40:17 -04:00
73d04b1564 Update hu.json 2023-04-30 16:09:15 +02:00
9469d56144 Spanish translations 2023-04-30 11:37:07 +02:00
46 changed files with 1604 additions and 379 deletions

View File

@ -15,8 +15,11 @@ Currently supported App sources:
- [Mullvad](https://mullvad.net/en/)
- [Signal](https://signal.org/)
- [SourceForge](https://sourceforge.net/)
- [SourceHut](https://git.sr.ht/)
- [APKMirror](https://apkmirror.com/) (Track-Only)
- [APKPure](https://apkpure.com/)
- Third Party F-Droid Repos
- Jenkins Jobs
- [Steam](https://store.steampowered.com/mobile)
- [Telegram App](https://telegram.org)
- [VLC](https://www.videolan.org/vlc/download-android.html)

View File

@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -20,7 +20,6 @@
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
"githubPATFormat": "Benutzername:Token",
"githubPATLinkText": "Über GitHub PATs",
"includePrereleases": "Vorabversionen einbeziehen",
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
@ -71,7 +70,7 @@
"updateX": "Aktualisiere {}",
"installX": "Installiere {}",
"markXTrackOnlyAsUpdated": "Markiere {}\n(Nur Nachverfolgen)\nals aktualisiert",
"changeX": "Ändern {}",
"changeX": "Ändere {}",
"installUpdateApps": "Apps installieren/aktualisieren",
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
@ -122,12 +121,12 @@
"followSystem": "System folgen",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"useBlackTheme": "Verwende Pure Black Dark Theme",
"appSortBy": "App sortieren nach",
"authorName": "Autor/Name",
"nameAuthor": "Name/Autor",
"asAdded": "Wie hinzugefügt",
"appSortOrder": "App Sortierung nach",
"appSortOrder": "App sortieren nach",
"ascending": "Aufsteigend",
"descending": "Absteigend",
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
@ -208,7 +207,7 @@
"addCategory": "Kategorie hinzufügen",
"label": "Bezeichnung",
"language": "Sprache",
"copiedToClipboard": "Copied to Clipboard",
"copiedToClipboard": "In die Zwischenablage kopiert",
"storagePermissionDenied": "Speicherberechtigung verweigert",
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
@ -219,15 +218,20 @@
"releaseDateAsVersionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert, aber ein Veröffentlichungsdatum verfügbar ist.",
"changes": "Änderungen",
"releaseDate": "Veröffentlichungsdatum",
"importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)",
"importFromURLsInFile": "Importieren von URLs aus Datei (z. B. OPML)",
"versionDetection": "Versionserkennung",
"standardVersionDetection": "Standardversionserkennung",
"groupByCategory": "Nach Kategorie gruppieren",
"autoApkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
"overrideSource": "Override Source",
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"overrideSource": "Quelle überschreiben",
"dontShowAgain": "Nicht noch einmal zeigen",
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
"gitlabPATLabel": "GitLab Personal Access Token (Aktiviert Suche)",
"about": "Über",
"requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
"checkOnStart": "Überprüfe einmalig beim Start",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"
@ -254,7 +258,7 @@
},
"minute": {
"one": "{} Minute",
"other": "{} Minutes"
"other": "{} Minuten"
},
"hour": {
"one": "{} Stunde",

View File

@ -20,7 +20,6 @@
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
"githubPATHint": "PAT must be in this format: username:token",
"githubPATFormat": "username:token",
"githubPATLinkText": "About GitHub PATs",
"includePrereleases": "Include prereleases",
"fallbackToOlderReleases": "Fallback to older releases",
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
@ -122,7 +121,7 @@
"followSystem": "Follow System",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"useBlackTheme": "Use Pure Black Dark Theme",
"appSortBy": "App Sort By",
"authorName": "Author/Name",
"nameAuthor": "Name/Author",
@ -228,6 +227,11 @@
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
"about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
"checkOnStart": "Check Once on Start",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"

283
assets/translations/es.json Normal file
View File

@ -0,0 +1,283 @@
{
"invalidURLForSource": "URL de la aplicación {} no válida",
"noReleaseFound": "No se ha podido encontrar una versión válida",
"noVersionFound": "No se ha podido determinar la versión de la publicación",
"urlMatchesNoSource": "La 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",
"functionNotImplemented": "Esta clase no ha implementado esta función",
"placeholder": "Espacio reservado",
"someErrors": "Han ocurrido algunos errores",
"unexpectedError": "Error Inesperado",
"ok": "Correcto",
"and": "y",
"startedBgUpdateTask": "Empezada la tarea de comprobación de actualizaciones en segundo plano",
"bgUpdateIgnoreAfterIs": "El parámetro ignoreAfter de la actualización en segundo plano es {}",
"startedActualBGUpdateCheck": "Ha comenzado la comprobación de actualizaciones en segundo plano",
"bgUpdateTaskFinished": "Ha finalizado la comprobación de actualizaciones en segundo plano",
"firstRun": "Esta es la primera ejecución de Obtainium",
"settingUpdateCheckIntervalTo": "Cambiando intervalo de actualización a {}",
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
"githubPATFormat": "nombre_de_usuario:token",
"includePrereleases": "Incluir versiones preliminares",
"fallbackToOlderReleases": "Retorceder a versiones previas",
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
"invalidRegEx": "Expresión regular inválida",
"noDescription": "Sin descripción",
"cancel": "Cancelar",
"continue": "Continuar",
"requiredInBrackets": "(Requerido)",
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
"colour": "Color",
"githubStarredRepos": "Repositorios favoritos de GitHub",
"uname": "Nombre de usuario",
"wrongArgNum": "Número de argumentos provistos inválido",
"xIsTrackOnly": "{} es de 'Solo Seguimiento'",
"source": "Origen",
"app": "Aplicación",
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
"youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
"trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.",
"cancelled": "Cancelado",
"appAlreadyAdded": "Aplicación ya añadida",
"alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
"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 {}",
"supportedSourcesBelow": "Fuentes Soportadas:",
"trackOnlyInBrackets": "(Solo Seguimiento)",
"searchableInBrackets": "(Soporta Búsquedas)",
"appsString": "Aplicaciones",
"noApps": "Sin Aplicaciones",
"noAppsForFilter": "Sin Aplicaciones para Filtrar",
"byX": "Por {}",
"percentProgress": "Progreso: {}%",
"pleaseWait": "Por favor, espere",
"updateAvailable": "Actualización Disponible",
"estimateInBracketsShort": "(Aprox.)",
"notInstalled": "No Instalado",
"estimateInBrackets": "(Aproximado)",
"selectAll": "Seleccionar Todo",
"deselectN": "Deseleccionar {}",
"xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
"removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
"removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
"updateX": "Actualizar {}",
"installX": "Instalar {}",
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada",
"changeX": "Cambiar {}",
"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",
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?",
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de utilidad cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
"shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas",
"resetInstallStatus": "Restaurar Estado de Instalación",
"more": "Más",
"removeOutdatedFilter": "Elimiar Filtro de Aplicaciones Desactualizado",
"showOutdatedOnly": "Mostrar solo Aplicaciones Desactualizadas",
"filter": "Filtrar",
"filterActive": "Filtrar *",
"filterApps": "Filtrar Actualizaciones",
"appName": "Nombre de la Aplicación",
"author": "Autor",
"upToDateApps": "Aplicaciones Actualizadas",
"nonInstalledApps": "Aplicaciones No Instaladas",
"importExport": "Importar/Exportar",
"settings": "Ajustes",
"exportedTo": "Exportado a {}",
"obtainiumExport": "Exportar Obtainium",
"invalidInput": "Input incorrecto",
"importedX": "Importado {}",
"obtainiumImport": "Importar Obtainium",
"importFromURLList": "Importar desde lista de URLs",
"searchQuery": "Consulta de Búsqueda",
"appURLList": "Lista de URLs de Aplicaciones",
"line": "Línea",
"searchX": "Buscar {}",
"noResults": "Resultados no encontrados",
"importX": "Importar {}",
"importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, 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.",
"importErrors": "Import Errors",
"importedXOfYApps": "{} de {} Aplicaciones importadas.",
"followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
"okay": "Correcto",
"selectURL": "Seleccionar URL",
"selectURLs": "Seleccionar URLs",
"pick": "Escoger",
"theme": "Tema",
"dark": "Oscuro",
"light": "Claro",
"followSystem": "Seguir al Sistema",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Usar tema oscuro con negros puros",
"appSortBy": "Ordenar Aplicaciones Por",
"authorName": "Autor/Nombre",
"nameAuthor": "Nombre/Autor",
"asAdded": "Según se Añadieron",
"appSortOrder": "Orden de Clasificación de Aplicaciones",
"ascending": "Ascendente",
"descending": "Descendente",
"bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
"neverManualOnly": "Nunca - Solo Manual",
"appearance": "Apariencia",
"showWebInAppView": "Mostrar Vista de la Web de Origen",
"pinUpdates": "Fijar Actualizaciones en la Parte Superior de la Vista de Aplicaciones",
"updates": "Actualizaciones",
"sourceSpecific": "Fuente Específica",
"appSource": "Fuente de la Aplicación",
"noLogs": "Sin Logs",
"appLogs": "Logs de la Aplicación",
"close": "Cerrar",
"share": "Compartir",
"appNotFound": "Aplicación no encontrada",
"obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "Elige una APK",
"appHasMoreThanOnePackage": "{} tiene más de un paquete:",
"deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
"deviceSupportsFollowingArchs": "Tu dispositivo soporta 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?",
"updatesAvailable": "Actualizaciones Disponibles",
"updatesAvailableNotifDescription": "Notifica al usuario de que hay actualizaciones para una o más aplicaciones monitorizadas por Obtainium",
"noNewUpdates": "No hay nuevas actualizaciones.",
"xHasAnUpdate": "{} tiene una actualización.",
"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 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 abierta para instalar aplicaciones",
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación",
"checkingForUpdates": "Buscando Actualizaciones",
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
"pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
"trackOnly": "Solo Seguimiento",
"errorWithHttpStatusCode": "Error {}",
"versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
"unknown": "Desconocido",
"none": "Ninguno",
"never": "Nunca",
"latestVersionX": "Última Versión: {}",
"installedVersionX": "Versión Instalada: {}",
"lastUpdateCheckX": "Última Comprobación: {}",
"remove": "Eliminar",
"yesMarkUpdated": "Sí, Marcar como Actualizada",
"fdroid": "Repositorio oficial de F-Droid",
"appIdOrName": "ID o Nombre de la Aplicación",
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
"fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
"steam": "Steam",
"steamMobile": "Steam Mobile",
"steamChat": "Steam Chat",
"install": "Instalar",
"markInstalled": "Marcar como Instalda",
"update": "Actualizar",
"markUpdated": "Marcar como Actualizada",
"additionalOptions": "Opciones Adicionales",
"disableVersionDetection": "Descativar Detección de Versiones",
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
"downloadingX": "Descargando {}",
"downloadNotifDescription": "Notifica al usuario de progreso de descarga de una aplicación",
"noAPKFound": "APK no encontrada",
"noVersionDetection": "Sin detección de versiones",
"categorize": "Catogorizar",
"categories": "Categorías",
"category": "Categoría",
"noCategory": "Sin Categoría",
"noCategories": "Sin Categorías",
"deleteCategoriesQuestion": "¿Borrar Categorías?",
"categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán margadas como 'Sin Categoría'.",
"addCategory": "Añadir Categoría",
"label": "Nombre",
"language": "Idioma",
"copiedToClipboard": "Copiado al Portapapeles",
"storagePermissionDenied": "Permiso de Almacenamiento rechazado",
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.",
"filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares",
"removeFromObtainium": "Eliminar de Obtainium",
"uninstallFromDevice": "Desinstalar del Dispositivo",
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
"releaseDateAsVersion": "Usar Fecha de Publicación como Versión",
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
"changes": "Cambios",
"releaseDate": "Fecha de Publicación",
"importFromURLsInFile": "Importar de URls en un Archivo (como OPML)",
"versionDetection": "Detección de Versiones",
"standardVersionDetection": "Detección de versiones estándar",
"groupByCategory": "Agrupar por Categoría",
"autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
"overrideSource": "Sobrescribir Fuente",
"dontShowAgain": "No mostrar de nuevo",
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
"about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
"checkOnStart": "Check Once on Start",
"removeAppQuestion": {
"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"
},
"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"
},
"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"
},
"apps": {
"one": "{} Aplicación",
"other": "{} Aplicaciones"
},
"url": {
"one": "{} URL",
"other": "{} URLs"
},
"minute": {
"one": "{} Minuto",
"other": "{} Minutos"
},
"hour": {
"one": "{} Hora",
"other": "{} Horas"
},
"day": {
"one": "{} Día",
"other": "{} Días"
},
"clearedNLogsBeforeXAfterY": {
"one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
"other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} y 1 aplicación más tiene actualizaciones.",
"other": "{} y {} aplicaciones más tiene actualizaciones."
},
"xAndNMoreUpdatesInstalled": {
"one": "{} y 1 aplicación más han sido actualizadas.",
"other": "{} y {} aplicaciones más han sido actualizadas."
}
}

View File

@ -20,7 +20,6 @@
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
"githubPATHint": "PAT باید در این قالب باشد: username:token",
"githubPATFormat": "username:token",
"githubPATLinkText": "درباره گیتهاب PATs",
"includePrereleases": "شامل نسخه های اولیه",
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
@ -122,7 +121,7 @@
"followSystem": "هماهنگ با سیستم",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"useBlackTheme": "استفاده از تم تیره سیاه خالص",
"appSortBy": "مرتب سازی برنامه بر اساس",
"authorName": "سازنده/اسم",
"nameAuthor": "اسم/سازنده",
@ -224,10 +223,15 @@
"standardVersionDetection": "تشخیص نسخه استاندارد",
"groupByCategory": "گروه بر اساس دسته",
"autoApkFilterByArch": "در صورت امکان سعی کنید APKها را بر اساس معماری CPU فیلتر کنید",
"overrideSource": "Override Source",
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"overrideSource": "نادیده گرفتن منبع",
"dontShowAgain": "دوباره این را نشان نده",
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
"about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
"checkOnStart": "Check Once on Start",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"

View File

@ -20,7 +20,6 @@
"githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)",
"githubPATHint": "Le JAP doit être dans ce format : username:token",
"githubPATFormat": "username:token",
"githubPATLinkText": "À propos des JAP GitHub",
"includePrereleases": "Inclure les avant-premières",
"fallbackToOlderReleases": "Retour aux anciennes versions",
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
@ -122,7 +121,7 @@
"followSystem": "Suivre le système",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"useBlackTheme": "Use Pure Black Dark Theme",
"appSortBy": "Applications triées par",
"authorName": "Auteur/Nom",
"nameAuthor": "Nom/Auteur",
@ -228,6 +227,11 @@
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
"about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
"checkOnStart": "Check Once on Start",
"removeAppQuestion": {
"one": "Supprimer l'application ?",
"other": "Supprimer les applications ?"

View File

@ -20,7 +20,6 @@
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
"githubPATFormat": "felhasználónév:token",
"githubPATLinkText": "A GitHub PAT-okról",
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
@ -223,10 +222,15 @@
"standardVersionDetection": "Alapért. verzió érzékelés",
"groupByCategory": "Csoportosítás Kategória alapján",
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
"overrideSource": "Override Source",
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"overrideSource": "Forrás felülbírálása",
"dontShowAgain": "Ne mutassa ezt újra",
"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": "Move Non-Installed Apps to Bottom of Apps View",
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
"about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
"checkOnStart": "Check Once on Start",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?"

View File

@ -20,7 +20,6 @@
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
"githubPATHint": "PAT deve seguire questo formato: username:token",
"githubPATFormat": "username:token",
"githubPATLinkText": "Informazioni su GitHub PAT",
"includePrereleases": "Includi prerelease",
"fallbackToOlderReleases": "Ripiega su release precedenti",
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
@ -122,7 +121,7 @@
"followSystem": "Segui sistema",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"useBlackTheme": "Use Pure Black Dark Theme",
"appSortBy": "App ordinate per",
"authorName": "Autore/Nome",
"nameAuthor": "Nome/Autore",
@ -228,6 +227,11 @@
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
"about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
"checkOnStart": "Check Once on Start",
"removeAppQuestion": {
"one": "Rimuovere l'App?",
"other": "Rimuovere le App?"

View File

@ -20,7 +20,6 @@
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
"githubPATFormat": "ユーザー名:トークン",
"githubPATLinkText": "GitHub PATsについて",
"includePrereleases": "プレリリースを含む",
"fallbackToOlderReleases": "旧リリースへのフォールバック",
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
@ -122,7 +121,7 @@
"followSystem": "システムに従う",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Use pure black dark theme",
"useBlackTheme": "ピュアブラックダークテーマを使用する",
"appSortBy": "アプリの並び方",
"authorName": "作者名/アプリ名",
"nameAuthor": "アプリ名/作者名",
@ -224,10 +223,15 @@
"standardVersionDetection": "標準のバージョン検出",
"groupByCategory": "カテゴリ別にグループ化する",
"autoApkFilterByArch": "可能であればCPUアーキテクチャによるAPKのフィルタリングを試みる",
"overrideSource": "Override Source",
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"overrideSource": "ソースの上書き",
"dontShowAgain": "二度と表示しない",
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
"dontShowAPKOriginWarnings": "APK Originの警告を表示しない",
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
"gitlabPATLabel": "GitLab パーソナルアクセストークン (検索を有効化する)",
"about": "概要",
"requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)",
"checkOnStart": "Check Once on Start",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"

View File

@ -20,7 +20,6 @@
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
"githubPATFormat": "username:token",
"githubPATLinkText": "关于 GitHub 个人访问令牌",
"includePrereleases": "包含预发行版",
"fallbackToOlderReleases": "将旧发行版作为备选",
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
@ -34,7 +33,7 @@
"githubStarredRepos": "GitHub 已星标仓库",
"uname": "用户名",
"wrongArgNum": "参数数量错误",
"xIsTrackOnly": "{} 为“仅追踪”模式",
"xIsTrackOnly": "{}为“仅追踪”模式",
"source": "源代码",
"app": "应用",
"appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
@ -51,8 +50,8 @@
"search": "搜索",
"additionalOptsFor": "{} 的更多选项",
"supportedSourcesBelow": "支持的来源:",
"trackOnlyInBrackets": "仅追踪",
"searchableInBrackets": "可搜索",
"trackOnlyInBrackets": "(仅追踪)",
"searchableInBrackets": "(可搜索)",
"appsString": "应用列表",
"noApps": "无应用",
"noAppsForFilter": "没有符合条件的应用",
@ -60,9 +59,9 @@
"percentProgress": "进度:{}%",
"pleaseWait": "请稍候",
"updateAvailable": "更新可用",
"estimateInBracketsShort": "(预计)",
"estimateInBracketsShort": "(推测)",
"notInstalled": "未安装",
"estimateInBrackets": "(预计)",
"estimateInBrackets": "(推测)",
"selectAll": "全选",
"deselectN": "取消选择 {}",
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
@ -75,8 +74,8 @@
"installUpdateApps": "安装/更新应用",
"installUpdateSelectedApps": "安装/更新选中的应用",
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
"no": "不要",
"yes": "好的",
"no": "",
"yes": "",
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
"pinToTop": "置顶",
"unpinFromTop": "取消置顶",
@ -143,7 +142,7 @@
"close": "关闭",
"share": "分享",
"appNotFound": "未找到应用",
"obtainiumExportHyphenatedLowercase": "obtainium-导出",
"obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "选择一个 APK 文件",
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
"deviceSupportsXArch": "您的设备支持 {} 架构。",
@ -173,13 +172,13 @@
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
"unknown": "未知",
"none": "无",
"never": "从",
"never": "从",
"latestVersionX": "最新版本:{}",
"installedVersionX": "当前版本:{}",
"lastUpdateCheckX": "上次更新检查:{}",
"remove": "删除",
"yesMarkUpdated": "是,标记为已更新",
"fdroid": "F-Droid Official",
"yesMarkUpdated": "是,标记为已更新",
"fdroid": "F-Droid 官方存储库",
"appIdOrName": "应用 ID 或名称",
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
"reposHaveMultipleApps": "存储库中可能包含多个应用",
@ -194,7 +193,7 @@
"additionalOptions": "附加选项",
"disableVersionDetection": "禁用版本检测",
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
"downloadingX": "正在下载 {}",
"downloadingX": "正在下载{}",
"downloadNotifDescription": "提示应用的下载进度",
"noAPKFound": "未找到 APK 文件",
"noVersionDetection": "禁用版本检测",
@ -223,11 +222,16 @@
"versionDetection": "版本检测",
"standardVersionDetection": "常规版本检测",
"groupByCategory": "按类别分组显示",
"autoApkFilterByArch": "如果可能,尝试按 CPU 架构筛选 APK 文件",
"overrideSource": "Override Source",
"dontShowAgain": "Don't show this again",
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
"autoApkFilterByArch": "如果可能,尝试按设备支持的 CPU 架构筛选 APK 文件",
"overrideSource": "覆盖来源",
"dontShowAgain": "不再显示",
"dontShowTrackOnlyWarnings": "不显示“仅追踪”模式警告",
"dontShowAPKOriginWarnings": "不显示 APK 文件来源警告",
"moveNonInstalledAppsToBottom": "将未安装应用置底",
"gitlabPATLabel": "GitLab 个人访问令牌(用于搜索)",
"about": "相关文档",
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
"checkOnStart": "启动时进行一次检查",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"

View File

@ -0,0 +1,116 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class APKCombo extends AppSource {
APKCombo() {
host = 'apkcombo.com';
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/+[^/]+/+[^/]+');
var match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) {
return Uri.parse(standardUrl).pathSegments.last;
}
@override
Map<String, String> get requestHeaders => {
"User-Agent": "curl/8.0.1",
"Accept": "*/*",
"Connection": "keep-alive",
"Host": "$host"
};
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
var res = await sourceRequest('$standardUrl/download/apk');
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var html = parse(res.body);
return html
.querySelectorAll('#variants-tab > div > ul > li')
.map((e) {
String? arch = e
.querySelector('code')
?.text
.trim()
.replaceAll(',', '')
.replaceAll(':', '-')
.replaceAll(' ', '-');
return e.querySelectorAll('a').map((e) {
String? url = e.attributes['href'];
if (url != null &&
!Uri.parse(url).path.toLowerCase().endsWith('.apk')) {
url = null;
}
String verCode =
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
return MapEntry<String, String>(
arch != null ? '$arch-$verCode.apk' : '', url ?? '');
}).toList();
})
.reduce((value, element) => [...value, ...element])
.where((element) => element.value.isNotEmpty)
.toList();
}
@override
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
var freshURLs = await getApkUrls(standardUrl);
var path2Match = Uri.parse(apkUrl).path;
for (var url in freshURLs) {
if (Uri.parse(url.value).path == path2Match) {
return url.value;
}
}
throw NoAPKError();
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
String appId = tryInferringAppId(standardUrl)!;
String host = Uri.parse(standardUrl).host;
var preres = await sourceRequest(standardUrl);
if (preres.statusCode != 200) {
throw getObtainiumHttpError(preres);
}
var res = parse(preres.body);
String? version = res.querySelector('div.version')?.text.trim();
if (version == null) {
throw NoVersionError();
}
String appName = res.querySelector('div.app_name')?.text.trim() ?? appId;
String author = res.querySelector('div.author')?.text.trim() ?? appName;
List<String> infoArray = res
.querySelectorAll('div.information-table > .item > div.value')
.map((e) => e.text.trim())
.toList();
DateTime? releaseDate;
if (infoArray.length >= 2) {
try {
releaseDate = DateFormat('MMMM d, yyyy').parse(infoArray[1]);
} catch (e) {
// ignore
}
}
return APKDetails(
version, await getApkUrls(standardUrl), AppNames(author, appName),
releaseDate: releaseDate);
}
}

View File

@ -57,7 +57,7 @@ class APKMirror extends AppSource {
true
? additionalSettings['filterReleaseTitlesByRegEx']
: null;
Response res = await get(Uri.parse('$standardUrl/feed'));
Response res = await sourceRequest('$standardUrl/feed');
if (res.statusCode == 200) {
var items = parse(res.body).querySelectorAll('item');
dynamic targetRelease;

View File

@ -0,0 +1,78 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class APKPure extends AppSource {
APKPure() {
host = 'apkpure.com';
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegExB = RegExp('^https?://m.$host/+[^/]+/+[^/]+');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
if (match != null) {
url = 'https://$host/${Uri.parse(url).path}';
}
RegExp standardUrlRegExA = RegExp('^https?://$host/+[^/]+/+[^/]+');
match = standardUrlRegExA.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) {
return Uri.parse(standardUrl).pathSegments.last;
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
String appId = tryInferringAppId(standardUrl)!;
String host = Uri.parse(standardUrl).host;
var res = await sourceRequest('$standardUrl/download');
if (res.statusCode == 200) {
var html = parse(res.body);
String? version = html.querySelector('span.info-sdk span')?.text.trim();
if (version == null) {
throw NoVersionError();
}
String? dateString =
html.querySelector('span.info-other span.date')?.text.trim();
DateTime? releaseDate;
try {
releaseDate = dateString != null
? DateFormat('MMM dd, yyyy').parse(dateString)
: null;
releaseDate = dateString != null && releaseDate == null
? DateFormat('MMMM dd, yyyy').parse(dateString)
: null;
} catch (err) {
// ignore
}
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
List<MapEntry<String, String>> apkUrls = [
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
];
String author = html
.querySelector('span.info-sdk')
?.text
.trim()
.substring(version.length + 4) ??
Uri.parse(standardUrl).pathSegments.reversed.last;
String appName =
html.querySelector('h1.info-title')?.text.trim() ?? appId;
return APKDetails(version, apkUrls, AppNames(author, appName),
releaseDate: releaseDate);
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -9,6 +9,7 @@ import 'package:obtainium/providers/source_provider.dart';
class Codeberg extends AppSource {
Codeberg() {
host = 'codeberg.org';
overrideEligible = true;
additionalSourceSpecificSettingFormItems = [];
@ -70,7 +71,7 @@ class Codeberg extends AppSource {
}
@override
Future<Map<String, String>> search(String query) async {
Future<Map<String, List<String>>> search(String query) async {
return gh.searchCommon(
query,
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
@ -9,6 +10,7 @@ class FDroid extends AppSource {
FDroid() {
host = 'f-droid.org';
name = tr('fdroid');
canSearch = true;
}
@override
@ -64,8 +66,37 @@ class FDroid extends AppSource {
String? appId = tryInferringAppId(standardUrl);
String host = Uri.parse(standardUrl).host;
return getAPKUrlsFromFDroidPackagesAPIResponse(
await get(Uri.parse('https://$host/api/v1/packages/$appId')),
await sourceRequest('https://$host/api/v1/packages/$appId'),
'https://$host/repo/$appId',
standardUrl);
}
@override
Future<Map<String, List<String>>> search(String query) async {
Response res = await sourceRequest(
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {};
parse(res.body).querySelectorAll('.package-header').forEach((e) {
String? url = e.attributes['href'];
if (url != null) {
try {
standardizeUrl(url);
} catch (e) {
url = null;
}
}
if (url != null) {
urlsWithDescriptions[url] = [
e.querySelector('.package-name')?.text.trim() ?? '',
e.querySelector('.package-summary')?.text.trim() ??
tr('noDescription')
];
}
});
return urlsWithDescriptions;
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -8,6 +8,7 @@ import 'package:obtainium/providers/source_provider.dart';
class FDroidRepo extends AppSource {
FDroidRepo() {
name = tr('fdroidThirdPartyRepo');
overrideEligible = true;
additionalSourceAppSpecificSettingFormItems = [
[
@ -28,7 +29,7 @@ class FDroidRepo extends AppSource {
if (appIdOrName == null) {
throw NoReleasesError();
}
var res = await get(Uri.parse('$standardUrl/index.xml'));
var res = await sourceRequest('$standardUrl/index.xml');
if (res.statusCode == 200) {
var body = parse(res.body);
var foundApps = body.querySelectorAll('application').where((element) {

View File

@ -2,8 +2,10 @@ import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/html.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/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -11,6 +13,7 @@ import 'package:url_launcher/url_launcher_string.dart';
class GitHub extends AppSource {
GitHub() {
host = 'github.com';
overrideEligible = true;
additionalSourceSpecificSettingFormItems = [
GeneratedFormTextField('github-creds',
@ -34,7 +37,7 @@ class GitHub extends AppSource {
hint: tr('githubPATFormat'),
belowWidgets: [
const SizedBox(
height: 8,
height: 4,
),
GestureDetector(
onTap: () {
@ -43,10 +46,13 @@ class GitHub extends AppSource {
mode: LaunchMode.externalApplication);
},
child: Text(
tr('githubPATLinkText'),
tr('about'),
style: const TextStyle(
decoration: TextDecoration.underline, fontSize: 12),
))
)),
const SizedBox(
height: 4,
),
])
];
@ -108,7 +114,7 @@ class GitHub extends AppSource {
true
? additionalSettings['filterReleaseTitlesByRegEx']
: null;
Response res = await get(Uri.parse(requestUrl));
Response res = await sourceRequest(requestUrl);
if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>;
@ -129,7 +135,7 @@ class GitHub extends AppSource {
? DateTime.parse(rel['published_at'])
: null;
releases.sort((a, b) {
// See #478
// See #478 and #534
if (a == b) {
return 0;
} else if (a == null) {
@ -137,8 +143,19 @@ class GitHub extends AppSource {
} else if (b == null) {
return 1;
} else {
return getReleaseDateFromRelease(a)!
.compareTo(getReleaseDateFromRelease(b)!);
var stdFormats = findStandardFormatsForVersion(a['tag_name'], true)
.intersection(findStandardFormatsForVersion(b['tag_name'], true));
if (stdFormats.isNotEmpty) {
var reg = RegExp(stdFormats.first);
var matchA = reg.firstMatch(a['tag_name']);
var matchB = reg.firstMatch(b['tag_name']);
return compareAlphaNumeric(
(a['tag_name'] as String).substring(matchA!.start, matchA.end),
(b['tag_name'] as String).substring(matchB!.start, matchB.end));
} else {
return getReleaseDateFromRelease(a)!
.compareTo(getReleaseDateFromRelease(b)!);
}
}
});
releases = releases.reversed.toList();
@ -213,19 +230,21 @@ class GitHub extends AppSource {
return AppNames(names[0], names[1]);
}
Future<Map<String, String>> searchCommon(
Future<Map<String, List<String>>> searchCommon(
String query, String requestUrl, String rootProp,
{Function(Response)? onHttpErrorCode}) async {
Response res = await get(Uri.parse(requestUrl));
Response res = await sourceRequest(requestUrl);
if (res.statusCode == 200) {
Map<String, String> urlsWithDescriptions = {};
Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
urlsWithDescriptions.addAll({
e['html_url'] as String:
((e['archived'] == true ? '[ARCHIVED] ' : '') +
(e['description'] != null
? e['description'] as String
: tr('noDescription')))
e['html_url'] as String: [
e['full_name'] as String,
((e['archived'] == true ? '[ARCHIVED] ' : '') +
(e['description'] != null
? e['description'] as String
: tr('noDescription')))
]
});
}
return urlsWithDescriptions;
@ -238,7 +257,7 @@ class GitHub extends AppSource {
}
@override
Future<Map<String, String>> search(String query) async {
Future<Map<String, List<String>>> search(String query) async {
return searchCommon(
query,
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',

View File

@ -1,14 +1,47 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher_string.dart';
class GitLab extends AppSource {
GitLab() {
host = 'gitlab.com';
overrideEligible = true;
canSearch = true;
additionalSourceSpecificSettingFormItems = [
GeneratedFormTextField('gitlab-creds',
label: tr('gitlabPATLabel'),
password: true,
required: false,
belowWidgets: [
const SizedBox(
height: 4,
),
GestureDetector(
onTap: () {
launchUrlString(
'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token',
mode: LaunchMode.externalApplication);
},
child: Text(
tr('about'),
style: const TextStyle(
decoration: TextDecoration.underline, fontSize: 12),
)),
const SizedBox(
height: 4,
)
])
];
additionalSourceAppSpecificSettingFormItems = [
[
@ -28,6 +61,37 @@ class GitLab extends AppSource {
return url.substring(0, match.end);
}
Future<String?> getPATIfAny() async {
SettingsProvider settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
String? creds = settingsProvider
.getSettingString(additionalSourceSpecificSettingFormItems[0].key);
return creds != null && creds.isNotEmpty ? creds : null;
}
@override
Future<Map<String, List<String>>> search(String query) async {
String? PAT = await getPATIfAny();
if (PAT == null) {
throw CredsNeededError(name);
}
var url =
'https://$host/api/v4/search?private_token=$PAT&scope=projects&search=${Uri.encodeQueryComponent(query)}';
var res = await sourceRequest(url);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var json = jsonDecode(res.body) as List<dynamic>;
Map<String, List<String>> results = {};
json.forEach((element) {
results['https://$host/${element['path_with_namespace']}'] = [
element['name_with_namespace'],
element['description'] ?? tr('noDescription')
];
});
return results;
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) =>
'$standardUrl/-/releases';
@ -39,7 +103,7 @@ class GitLab extends AppSource {
) async {
bool fallbackToOlderReleases =
additionalSettings['fallbackToOlderReleases'] == true;
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
Response res = await sourceRequest('$standardUrl/-/tags?format=atom');
if (res.statusCode == 200) {
var standardUri = Uri.parse(standardUrl);
var parsedHtml = parse(res.body);

View File

@ -4,7 +4,98 @@ import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
try {
Uri.parse(ambiguousUrl).origin;
return ambiguousUrl;
} catch (err) {
// is relative
}
var currPathSegments = referenceAbsoluteUrl.path
.split('/')
.where((element) => element.trim().isNotEmpty)
.toList();
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
} else if (ambiguousUrl.split('/').length == 1) {
return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
} else {
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
}
}
int compareAlphaNumeric(String a, String b) {
List<String> aParts = _splitAlphaNumeric(a);
List<String> bParts = _splitAlphaNumeric(b);
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
String aPart = aParts[i];
String bPart = bParts[i];
bool aIsNumber = _isNumeric(aPart);
bool bIsNumber = _isNumeric(bPart);
if (aIsNumber && bIsNumber) {
int aNumber = int.parse(aPart);
int bNumber = int.parse(bPart);
int cmp = aNumber.compareTo(bNumber);
if (cmp != 0) {
return cmp;
}
} else if (!aIsNumber && !bIsNumber) {
int cmp = aPart.compareTo(bPart);
if (cmp != 0) {
return cmp;
}
} else {
// Alphanumeric strings come before numeric strings
return aIsNumber ? 1 : -1;
}
}
return aParts.length.compareTo(bParts.length);
}
List<String> _splitAlphaNumeric(String s) {
List<String> parts = [];
StringBuffer sb = StringBuffer();
bool isNumeric = _isNumeric(s[0]);
sb.write(s[0]);
for (int i = 1; i < s.length; i++) {
bool currentIsNumeric = _isNumeric(s[i]);
if (currentIsNumeric == isNumeric) {
sb.write(s[i]);
} else {
parts.add(sb.toString());
sb.clear();
sb.write(s[i]);
isNumeric = currentIsNumeric;
}
}
parts.add(sb.toString());
return parts;
}
bool _isNumeric(String s) {
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
}
class HTML extends AppSource {
HTML() {
overrideEligible = true;
}
@override
// TODO: implement requestHeaders choice, hardcoded for now
Map<String, String>? get requestHeaders => {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
};
@override
String sourceSpecificStandardizeURL(String url) {
return url;
@ -16,14 +107,16 @@ class HTML extends AppSource {
Map<String, dynamic> additionalSettings,
) async {
var uri = Uri.parse(standardUrl);
Response res = await get(uri);
Response res = await sourceRequest(standardUrl);
if (res.statusCode == 200) {
List<String> links = parse(res.body)
.querySelectorAll('a')
.map((element) => element.attributes['href'] ?? '')
.where((element) => element.toLowerCase().endsWith('.apk'))
.where((element) =>
Uri.parse(element).path.toLowerCase().endsWith('.apk'))
.toList();
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
links.sort(
(a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last));
if (additionalSettings['apkFilterRegEx'] != null) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
links = links.where((element) => reg.hasMatch(element)).toList();
@ -34,25 +127,8 @@ class HTML extends AppSource {
var rel = links.last;
var apkName = rel.split('/').last;
var version = apkName.substring(0, apkName.length - 4);
List<String> apkUrls = [rel].map((e) {
try {
Uri.parse(e).origin;
return e;
} catch (err) {
// is relative
}
var currPathSegments = uri.path
.split('/')
.where((element) => element.trim().isNotEmpty)
.toList();
if (e.startsWith('/') || currPathSegments.isEmpty) {
return '${uri.origin}/$e';
} else if (e.split('/').length == 1) {
return '${uri.origin}/${currPathSegments.join('/')}/$e';
} else {
return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e';
}
}).toList();
List<String> apkUrls =
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
return APKDetails(
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
} else {

View File

@ -31,8 +31,8 @@ class IzzyOnDroid extends AppSource {
) async {
String? appId = tryInferringAppId(standardUrl);
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
await get(
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
await sourceRequest(
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
'https://android.izzysoft.de/frepo/$appId',
standardUrl);
}

View File

@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class Jenkins extends AppSource {
Jenkins() {
overrideEligible = true;
overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
}
@override
String trimJobUrl(String url) {
RegExp standardUrlRegEx = RegExp('.*/job/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) =>
'$standardUrl/-/releases';
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
standardUrl = trimJobUrl(standardUrl);
Response res =
await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json');
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
var releaseDate = json['timestamp'] == null
? null
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
var version =
json['number'] == null ? null : (json['number'] as int).toString();
if (version == null) {
throw NoVersionError();
}
var apkUrls = (json['artifacts'] as List<dynamic>)
.map((e) {
var path = (e['relativePath'] as String?);
if (path != null && path.isNotEmpty) {
path = '$standardUrl/lastSuccessfulBuild/artifact/$path';
}
return path == null
? const MapEntry<String, String>('', '')
: MapEntry<String, String>(
(e['fileName'] ?? e['relativePath']) as String, path);
})
.where((url) =>
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
.toList();
return APKDetails(
version,
apkUrls,
releaseDate: releaseDate,
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last));
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -28,7 +28,7 @@ class Mullvad extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/en/download/android'));
Response res = await sourceRequest('$standardUrl/en/download/android');
if (res.statusCode == 200) {
var versions = parse(res.body)
.querySelectorAll('p')

View File

@ -78,7 +78,7 @@ class NeutronCode extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse(standardUrl));
Response res = await sourceRequest(standardUrl);
if (res.statusCode == 200) {
var http = parse(res.body);
var name = http.querySelector('.pd-title')?.innerHtml;

View File

@ -19,7 +19,7 @@ class Signal extends AppSource {
Map<String, dynamic> additionalSettings,
) async {
Response res =
await get(Uri.parse('https://updates.$host/android/latest.json'));
await sourceRequest('https://updates.$host/android/latest.json');
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
String? apkUrl = json['url'];

View File

@ -6,12 +6,19 @@ import 'package:obtainium/providers/source_provider.dart';
class SourceForge extends AppSource {
SourceForge() {
host = 'sourceforge.net';
overrideEligible = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
RegExp standardUrlRegExB = RegExp('^https?://$host/p/[^/]+');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
if (match != null) {
url =
'https://${Uri.parse(url.substring(0, match.end)).host}/projects/${url.substring(Uri.parse(url.substring(0, match.end)).host.length + '/projects/'.length + 1)}';
}
RegExp standardUrlRegExA = RegExp('^https?://$host/projects/[^/]+');
match = standardUrlRegExA.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
@ -23,7 +30,7 @@ class SourceForge extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/rss?path=/'));
Response res = await sourceRequest('$standardUrl/rss?path=/');
if (res.statusCode == 200) {
var parsedHtml = parse(res.body);
var allDownloadLinks =

View File

@ -0,0 +1,101 @@
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:easy_localization/easy_localization.dart';
class SourceHut extends AppSource {
SourceHut() {
host = 'git.sr.ht';
overrideEligible = true;
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormSwitch('fallbackToOlderReleases',
label: tr('fallbackToOlderReleases'), defaultValue: true)
]
];
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Uri standardUri = Uri.parse(standardUrl);
String appName = standardUri.pathSegments.last;
bool fallbackToOlderReleases =
additionalSettings['fallbackToOlderReleases'] == true;
Response res = await sourceRequest('$standardUrl/refs/rss.xml');
if (res.statusCode == 200) {
var parsedHtml = parse(res.body);
List<APKDetails> apkDetailsList = [];
int ind = 0;
for (var entry in parsedHtml.querySelectorAll('item').sublist(0, 6)) {
// Limit 5 for speed
if (!fallbackToOlderReleases && ind > 0) {
break;
}
String? version = entry.querySelector('title')?.text.trim();
if (version == null) {
throw NoVersionError();
}
String? releaseDateString = entry.querySelector('pubDate')?.innerHtml;
var link = entry.querySelector('link');
String releasePage = '$standardUrl/refs/$version';
DateTime? releaseDate = releaseDateString != null
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
: null;
var res2 = await sourceRequest(releasePage);
List<MapEntry<String, String>> apkUrls = [];
if (res2.statusCode == 200) {
apkUrls = getApkUrlsFromUrls(parse(res2.body)
.querySelectorAll('a')
.map((e) => e.attributes['href'] ?? '')
.where((e) => e.toLowerCase().endsWith('.apk'))
.map((e) => ensureAbsoluteUrl(e, standardUri))
.toList());
}
apkDetailsList.add(APKDetails(
version,
apkUrls,
AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName,
appName),
releaseDate: releaseDate));
ind++;
}
if (apkDetailsList.isEmpty) {
throw NoReleasesError();
}
if (fallbackToOlderReleases) {
if (additionalSettings['trackOnly'] != true) {
apkDetailsList =
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
}
if (apkDetailsList.isEmpty) {
throw NoReleasesError();
}
}
return apkDetailsList.first;
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -29,7 +29,7 @@ class SteamMobile extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('https://$host/mobile'));
Response res = await sourceRequest('https://$host/mobile');
if (res.statusCode == 200) {
var apkNamePrefix = additionalSettings['app'] as String?;
if (apkNamePrefix == null) {

View File

@ -20,7 +20,7 @@ class TelegramApp extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK'));
Response res = await sourceRequest('https://t.me/s/TAndroidAPK');
if (res.statusCode == 200) {
var http = parse(res.body);
var messages =

View File

@ -19,8 +19,8 @@ class VLC extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(
Uri.parse('https://www.videolan.org/vlc/download-android.html'));
Response res = await sourceRequest(
'https://www.videolan.org/vlc/download-android.html');
if (res.statusCode == 200) {
var dwUrlBase = 'get.videolan.org/vlc-android';
var dwLinks = parse(res.body)
@ -38,7 +38,7 @@ class VLC extends AppSource {
throw NoVersionError();
}
String? targetUrl = 'https://$dwUrlBase/$version/';
Response res2 = await get(Uri.parse(targetUrl));
Response res2 = await sourceRequest(targetUrl);
String mirrorDwBase =
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
List<String> apkUrls = [];

View File

@ -14,21 +14,21 @@ class WhatsApp extends AppSource {
}
@override
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
Response res = await sourceRequest('https://www.whatsapp.com/android');
if (res.statusCode == 200) {
var targetLinks = parse(res.body)
.querySelectorAll('a')
.map((e) => e.attributes['href'])
.where((e) => e != null)
.map((e) => e.attributes['href'] ?? '')
.where((e) => e.isNotEmpty)
.where((e) =>
e!.contains('scontent.whatsapp.net') &&
e.contains('WhatsApp.apk'))
e.contains('content.whatsapp.net') && e.contains('WhatsApp.apk'))
.toList();
if (targetLinks.isEmpty) {
throw NoAPKError();
}
return targetLinks[0]!;
return targetLinks[0];
} else {
throw getObtainiumHttpError(res);
}
@ -39,7 +39,7 @@ class WhatsApp extends AppSource {
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
Response res = await sourceRequest('https://www.whatsapp.com/android');
if (res.statusCode == 200) {
var targetElements = parse(res.body)
.querySelectorAll('p')

View File

@ -25,6 +25,11 @@ class InvalidURLError extends ObtainiumError {
: super(tr('invalidURLForSource', args: [sourceName]));
}
class CredsNeededError extends ObtainiumError {
CredsNeededError(String sourceName)
: super(tr('requiresCredentialsInSettings', args: [sourceName]));
}
class NoReleasesError extends ObtainiumError {
NoReleasesError() : super(tr('noReleaseFound'));
}

View File

@ -21,21 +21,22 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.12.1';
const String currentVersion = '0.13.3';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
const int bgUpdateCheckAlarmId = 666;
const supportedLocales = [
Locale('en'),
Locale('zh'),
Locale('it'),
Locale('ja'),
Locale('hu'),
Locale('de'),
Locale('fa'),
Locale('fr')
List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('en'), 'English'),
MapEntry(Locale('zh'), '汉语'),
MapEntry(Locale('it'), 'Italiano'),
MapEntry(Locale('ja'), '日本語'),
MapEntry(Locale('hu'), 'Magyar'),
MapEntry(Locale('de'), 'Deutsch'),
MapEntry(Locale('fa'), 'فارسی'),
MapEntry(Locale('fr'), 'Français'),
MapEntry(Locale('es'), 'Español'),
];
const fallbackLocale = Locale('en');
const localeDir = 'assets/translations';
@ -52,7 +53,7 @@ Future<void> loadTranslations() async {
saveLocale: true,
forceLocale: forceLocale != null ? Locale(forceLocale) : null,
fallbackLocale: fallbackLocale,
supportedLocales: supportedLocales,
supportedLocales: supportedLocales.map((e) => e.key).toList(),
assetLoader: const RootBundleAssetLoader(),
useOnlyLangCode: true,
useFallbackTranslations: true,
@ -171,7 +172,7 @@ void main() async {
Provider(create: (context) => LogsProvider())
],
child: EasyLocalization(
supportedLocales: supportedLocales,
supportedLocales: supportedLocales.map((e) => e.key).toList(),
path: localeDir,
fallbackLocale: fallbackLocale,
useOnlyLangCode: true,
@ -221,7 +222,7 @@ class _ObtainiumState extends State<Obtainium> {
], onlyIfExists: false);
}
if (!supportedLocales
.map((e) => e.languageCode)
.map((e) => e.key.languageCode)
.contains(context.locale.languageCode) ||
settingsProvider.forcedLocale == null &&
context.deviceLocale.languageCode !=

View File

@ -13,17 +13,20 @@ class GitHubStars implements MassAppUrlSource {
@override
late List<String> requiredArgs = [tr('uname')];
Future<Map<String, String>> getOnePageOfUserStarredUrlsWithDescriptions(
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
String username, int page) async {
Response res = await get(Uri.parse(
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
if (res.statusCode == 200) {
Map<String, String> urlsWithDescriptions = {};
Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
urlsWithDescriptions.addAll({
e['html_url'] as String: e['description'] != null
? e['description'] as String
: tr('noDescription')
e['html_url'] as String: [
e['full_name'] as String,
e['description'] != null
? e['description'] as String
: tr('noDescription')
]
});
}
return urlsWithDescriptions;
@ -35,11 +38,12 @@ class GitHubStars implements MassAppUrlSource {
}
@override
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args) async {
Future<Map<String, List<String>>> getUrlsWithDescriptions(
List<String> args) async {
if (args.length != requiredArgs.length) {
throw ObtainiumError(tr('wrongArgNum'));
}
Map<String, String> urlsWithDescriptions = {};
Map<String, List<String>> urlsWithDescriptions = {};
var page = 1;
while (true) {
var pageUrls =

View File

@ -159,14 +159,23 @@ class _AddAppPageState extends State<AddAppPage> {
app.preferredApkIndex =
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
// ignore: use_build_context_synchronously
var downloadedApk = await appsProvider.downloadApp(
var downloadedArtifact = await appsProvider.downloadApp(
app, globalNavigatorKey.currentContext);
app.id = downloadedApk.appId;
DownloadedApk? downloadedFile;
DownloadedXApkDir? downloadedDir;
if (downloadedArtifact is DownloadedApk) {
downloadedFile = downloadedArtifact;
} else {
downloadedDir = downloadedArtifact as DownloadedXApkDir;
}
app.id = downloadedFile?.appId ?? downloadedDir!.appId;
}
if (appsProvider.apps.containsKey(app.id)) {
throw ObtainiumError(tr('appAlreadyAdded'));
}
if (app.additionalSettings['trackOnly'] == true) {
if (app.additionalSettings['trackOnly'] == true ||
app.additionalSettings['versionDetection'] !=
'standardVersionDetection') {
app.installedVersion = app.latestVersion;
}
app.categories = pickedCategories;
@ -246,13 +255,22 @@ class _AddAppPageState extends State<AddAppPage> {
searching = true;
});
try {
var results = await Future.wait(sourceProvider.sources
.where((e) => e.canSearch)
.map((e) => e.search(searchQuery)));
var results = await Future.wait(
sourceProvider.sources.where((e) => e.canSearch).map((e) async {
try {
return await e.search(searchQuery);
} catch (err) {
if (err is! CredsNeededError) {
rethrow;
} else {
return <String, List<String>>{};
}
}
}));
// .then((results) async {
// Interleave results instead of simple reduce
Map<String, String> res = {};
Map<String, List<String>> res = {};
var si = 0;
var done = false;
while (!done) {
@ -265,6 +283,9 @@ class _AddAppPageState extends State<AddAppPage> {
}
si++;
}
if (res.isEmpty) {
throw ObtainiumError(tr('noResults'));
}
List<String>? selectedUrls = res.isEmpty
? []
// ignore: use_build_context_synchronously
@ -300,8 +321,10 @@ class _AddAppPageState extends State<AddAppPage> {
'overrideSource',
defaultValue: HTML().runtimeType.toString(),
[
...sourceProvider.sources.map(
(s) => MapEntry(s.runtimeType.toString(), s.name))
...sourceProvider.sources
.where((s) => s.overrideEligible)
.map((s) =>
MapEntry(s.runtimeType.toString(), s.name))
],
label: tr('overrideSource'))
]
@ -327,8 +350,8 @@ class _AddAppPageState extends State<AddAppPage> {
],
),
const SizedBox(
height: 25,
),
height: 16,
)
]);
bool shouldShowSearchBar() =>
@ -357,33 +380,33 @@ class _AddAppPageState extends State<AddAppPage> {
const SizedBox(
width: 16,
),
ElevatedButton(
onPressed: searchQuery.isEmpty || doingSomething
? null
: () {
runSearch();
},
child: Text(tr('search')))
searching
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: searchQuery.isEmpty || doingSomething
? null
: () {
runSearch();
},
child: Text(tr('search')))
],
);
Widget getAdditionalOptsCol() => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Divider(
height: 64,
const SizedBox(
height: 16,
),
Text(
tr('additionalOptsFor',
args: [pickedSource?.name ?? tr('source')]),
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold)),
const SizedBox(
height: 16,
),
if (pickedSourceOverride != null ||
pickedSource.runtimeType.toString() ==
HTML().runtimeType.toString())
getHTMLSourceOverrideDropdown(),
GeneratedForm(
key: Key(pickedSource.runtimeType.toString()),
items: pickedSource!.combinedAppSpecificSettingFormItems,
@ -457,11 +480,15 @@ class _AddAppPageState extends State<AddAppPage> {
const SizedBox(
height: 16,
),
if (shouldShowSearchBar())
const SizedBox(
height: 16,
),
if (pickedSourceOverride != null ||
(pickedSource != null &&
pickedSource.runtimeType.toString() ==
HTML().runtimeType.toString()))
getHTMLSourceOverrideDropdown(),
if (shouldShowSearchBar()) getSearchBarRow(),
const SizedBox(
height: 16,
),
if (pickedSource != null)
getAdditionalOptsCol()
else

View File

@ -444,7 +444,9 @@ class _AppPageState extends State<AppPage> {
Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
child: LinearProgressIndicator(
value: app!.downloadProgress! / 100))
value: app!.downloadProgress! >= 0
? app.downloadProgress! / 100
: null))
],
));

View File

@ -52,6 +52,9 @@ class AppsPageState extends State<AppsPage> {
}
}
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
@override
Widget build(BuildContext context) {
var appsProvider = context.watch<AppsProvider>();
@ -61,6 +64,27 @@ class AppsPageState extends State<AppsPage> {
var currentFilterIsUpdatesOnly =
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
refresh() {
HapticFeedback.lightImpact();
setState(() {
refreshingSince = DateTime.now();
});
return appsProvider.checkUpdates().catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
refreshingSince = null;
});
});
}
if (!appsProvider.loadingApps &&
appsProvider.apps.isNotEmpty &&
settingsProvider.checkJustStarted() &&
settingsProvider.checkOnStart) {
_refreshIndicatorKey.currentState?.show();
}
selectedAppIds = selectedAppIds
.where((element) => listedApps.map((e) => e.app.id).contains(element))
.toSet();
@ -185,6 +209,18 @@ class AppsPageState extends State<AppsPage> {
listedApps = [...temp, ...listedApps];
}
if (settingsProvider.buryNonInstalled) {
var temp = [];
listedApps = listedApps.where((sa) {
if (sa.app.installedVersion == null) {
temp.add(sa);
return false;
}
return true;
}).toList();
listedApps = [...listedApps, ...temp];
}
var tempPinned = [];
var tempNotPinned = [];
for (var a in listedApps) {
@ -303,7 +339,7 @@ class AppsPageState extends State<AppsPage> {
?.isBefore(refreshingSince!) ??
true))
.length /
appsProvider.apps.length,
(appsProvider.apps.isNotEmpty ? appsProvider.apps.length : 1),
),
)
];
@ -503,10 +539,16 @@ class AppsPageState extends State<AppsPage> {
? FontWeight.bold
: FontWeight.normal)),
trailing: listedApps[index].downloadProgress != null
? Text(tr('percentProgress', args: [
listedApps[index].downloadProgress?.toInt().toString() ??
'100'
]))
? SizedBox(
width: 110,
child: Text(tr('percentProgress', args: [
listedApps[index].downloadProgress! >= 0
? listedApps[index]
.downloadProgress!
.toInt()
.toString()
: tr('pleaseWait')
])))
: trailingRow,
onTap: () {
if (selectedAppIds.isNotEmpty) {
@ -1005,19 +1047,8 @@ class AppsPageState extends State<AppsPage> {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: RefreshIndicator(
onRefresh: () {
HapticFeedback.lightImpact();
setState(() {
refreshingSince = DateTime.now();
});
return appsProvider.checkUpdates().catchError((e) {
showError(e, context);
}).whenComplete(() {
setState(() {
refreshingSince = null;
});
});
},
key: _refreshIndicatorKey,
onRefresh: refresh,
child: CustomScrollView(slivers: <Widget>[
CustomAppBar(title: tr('appsString')),
...getLoadingWidgets(),

View File

@ -6,6 +6,8 @@ import 'package:obtainium/pages/add_app.dart';
import 'package:obtainium/pages/apps.dart';
import 'package:obtainium/pages/import_export.dart';
import 'package:obtainium/pages/settings.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:provider/provider.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@ -24,6 +26,7 @@ class NavigationPageItem {
class _HomePageState extends State<HomePage> {
List<int> selectedIndexHistory = [];
int prevAppCount = -1;
List<NavigationPageItem> pages = [
NavigationPageItem(tr('appsString'), Icons.apps,
@ -36,6 +39,39 @@ class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
AppsProvider appsProvider = context.watch<AppsProvider>();
switchToPage(int index) async {
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
}
if (prevAppCount >= 0 &&
appsProvider.apps.length > prevAppCount &&
selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last == 1) {
switchToPage(0);
}
prevAppCount = appsProvider.apps.length;
return WillPopScope(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
@ -65,27 +101,7 @@ class _HomePageState extends State<HomePage> {
.toList(),
onDestinationSelected: (int index) async {
HapticFeedback.selectionClick();
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>)
.currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
await switchToPage(index);
},
selectedIndex:
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,

View File

@ -470,7 +470,7 @@ class UrlSelectionModal extends StatefulWidget {
this.selectedByDefault = true,
this.onlyOneSelectionAllowed = false});
Map<String, String> urlsWithDescriptions;
Map<String, List<String>> urlsWithDescriptions;
bool selectedByDefault;
bool onlyOneSelectionAllowed;
@ -479,7 +479,7 @@ class UrlSelectionModal extends StatefulWidget {
}
class _UrlSelectionModalState extends State<UrlSelectionModal> {
Map<MapEntry<String, String>, bool> urlWithDescriptionSelections = {};
Map<MapEntry<String, List<String>>, bool> urlWithDescriptionSelections = {};
@override
void initState() {
super.initState();
@ -522,16 +522,28 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
launchUrlString(urlWithD.key,
mode: LaunchMode.externalApplication);
},
child: Text(
Uri.parse(urlWithD.key).path.substring(1),
style: const TextStyle(decoration: TextDecoration.underline),
textAlign: TextAlign.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
urlWithD.value[0],
style: const TextStyle(
decoration: TextDecoration.underline,
fontWeight: FontWeight.bold),
textAlign: TextAlign.start,
),
Text(
Uri.parse(urlWithD.key).host,
style: const TextStyle(
decoration: TextDecoration.underline, fontSize: 12),
)
],
));
var descriptionText = Text(
urlWithD.value.length > 128
? '${urlWithD.value.substring(0, 128)}...'
: urlWithD.value,
urlWithD.value[1].length > 128
? '${urlWithD.value[1].substring(0, 128)}...'
: urlWithD.value[1],
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
);

View File

@ -144,8 +144,8 @@ class _SettingsPageState extends State<SettingsPage> {
child: Text(tr('followSystem')),
),
...supportedLocales.map((e) => DropdownMenuItem(
value: e.toLanguageTag(),
child: Text(e.toLanguageTag().toUpperCase()),
value: e.key.toLanguageTag(),
child: Text(e.value),
))
],
onChanged: (value) {
@ -205,6 +205,10 @@ class _SettingsPageState extends State<SettingsPage> {
height: 16,
);
const height32 = SizedBox(
height: 32,
);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: CustomScrollView(slivers: <Widget>[
@ -217,9 +221,38 @@ class _SettingsPageState extends State<SettingsPage> {
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tr('updates'),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
intervalDropdown,
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(tr('checkOnStart'))),
Switch(
value: settingsProvider.checkOnStart,
onChanged: (value) {
settingsProvider.checkOnStart = value;
})
],
),
height32,
Text(
tr('sourceSpecific'),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
...sourceSpecificFields,
height32,
Text(
tr('appearance'),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
themeDropdown,
@ -227,7 +260,7 @@ class _SettingsPageState extends State<SettingsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('useBlackTheme')),
Flexible(child: Text(tr('useBlackTheme'))),
Switch(
value: settingsProvider.useBlackTheme,
onChanged: (value) {
@ -254,7 +287,7 @@ class _SettingsPageState extends State<SettingsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('showWebInAppView')),
Flexible(child: Text(tr('showWebInAppView'))),
Switch(
value: settingsProvider.showAppWebpage,
onChanged: (value) {
@ -266,7 +299,7 @@ class _SettingsPageState extends State<SettingsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('pinUpdates')),
Flexible(child: Text(tr('pinUpdates'))),
Switch(
value: settingsProvider.pinUpdates,
onChanged: (value) {
@ -278,7 +311,21 @@ class _SettingsPageState extends State<SettingsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('groupByCategory')),
Flexible(
child: Text(
tr('moveNonInstalledAppsToBottom'))),
Switch(
value: settingsProvider.buryNonInstalled,
onChanged: (value) {
settingsProvider.buryNonInstalled = value;
})
],
),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(tr('groupByCategory'))),
Switch(
value: settingsProvider.groupByCategory,
onChanged: (value) {
@ -290,7 +337,9 @@ class _SettingsPageState extends State<SettingsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('dontShowTrackOnlyWarnings')),
Flexible(
child:
Text(tr('dontShowTrackOnlyWarnings'))),
Switch(
value:
settingsProvider.hideTrackOnlyWarning,
@ -304,7 +353,9 @@ class _SettingsPageState extends State<SettingsPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tr('dontShowAPKOriginWarnings')),
Flexible(
child:
Text(tr('dontShowAPKOriginWarnings'))),
Switch(
value:
settingsProvider.hideAPKOriginWarning,
@ -314,31 +365,11 @@ class _SettingsPageState extends State<SettingsPage> {
})
],
),
const Divider(
height: 16,
),
height16,
Text(
tr('updates'),
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
intervalDropdown,
const Divider(
height: 48,
),
Text(
tr('sourceSpecific'),
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
...sourceSpecificFields,
const Divider(
height: 48,
),
height32,
Text(
tr('categories'),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
height16,

View File

@ -27,6 +27,7 @@ import 'package:flutter_fgbg/flutter_fgbg.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart';
import 'package:archive/archive.dart';
class AppInMemory {
late App app;
@ -46,6 +47,13 @@ class DownloadedApk {
DownloadedApk(this.appId, this.file);
}
class DownloadedXApkDir {
String appId;
File file;
Directory extracted;
DownloadedXApkDir(this.appId, this.file, this.extracted);
}
List<String> generateStandardVersionRegExStrings() {
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
var basics = [
@ -114,22 +122,35 @@ class AppsProvider with ChangeNotifier {
// Load Apps into memory (in background, this is done later instead of in the constructor)
await loadApps();
// Delete any partial APKs
var cutoff = DateTime.now().subtract(const Duration(days: 7));
(await getExternalCacheDirectories())
?.first
.listSync()
.where((element) => element.path.endsWith('.apk.part'))
.where((element) =>
element.path.endsWith('.part') ||
element.statSync().modified.isBefore(cutoff))
.forEach((partialApk) {
partialApk.delete();
});
}();
}
downloadFile(String url, String fileName, Function? onProgress,
{bool useExisting = true}) async {
Future<File> downloadFile(
String url, String fileNameNoExt, Function? onProgress,
{bool useExisting = true, Map<String, String>? headers}) async {
var destDir = (await getExternalCacheDirectories())!.first.path;
StreamedResponse response =
await Client().send(Request('GET', Uri.parse(url)));
File downloadedFile = File('$destDir/$fileName');
var req = Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
var client = 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);
}
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
if (!(downloadedFile.existsSync() && useExisting)) {
File tempDownloadedFile = File('${downloadedFile.path}.part');
if (tempDownloadedFile.existsSync()) {
@ -157,11 +178,34 @@ class AppsProvider with ChangeNotifier {
throw response.reasonPhrase ?? tr('unexpectedError');
}
tempDownloadedFile.renameSync(downloadedFile.path);
} else {
client.close();
}
return downloadedFile;
}
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
Future<File> handleAPKIDChange(App app, PackageArchiveInfo 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
// The former case should be handled (give the App its real ID), the latter is a security issue
if (app.id != newInfo.packageName) {
var isTempId = SourceProvider().isTempId(app);
if (apps[app.id] != null && !isTempId) {
throw IDChangedError();
}
var originalAppId = app.id;
app.id = newInfo.packageName;
downloadedFile = downloadedFile.renameSync(
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}');
if (apps[originalAppId] != null) {
await removeApps([originalAppId]);
await saveApps([app], onlyIfExists: !isTempId);
}
}
return downloadedFile;
}
Future<Object> downloadApp(App app, BuildContext? context) async {
NotificationsProvider? notificationsProvider =
context?.read<NotificationsProvider>();
var notifId = DownloadNotification(app.finalName, 0).id;
@ -170,15 +214,16 @@ class AppsProvider with ChangeNotifier {
notifyListeners();
}
try {
String downloadUrl = await SourceProvider()
.getSource(app.url, overrideSource: app.overrideSource)
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value);
var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
AppSource source = SourceProvider()
.getSource(app.url, overrideSource: app.overrideSource);
String downloadUrl = await source.apkUrlPrefetchModifier(
app.apkUrls[app.preferredApkIndex].value, app.url);
var notif = DownloadNotification(app.finalName, 100);
notificationsProvider?.cancel(notif.id);
int? prevProg;
File downloadedFile =
await downloadFile(downloadUrl, fileName, (double? progress) {
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
var downloadedFile = await downloadFile(downloadUrl, fileNameNoExt,
headers: source.requestHeaders, (double? progress) {
int? prog = progress?.ceil();
if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = progress;
@ -190,33 +235,45 @@ class AppsProvider with ChangeNotifier {
}
prevProg = prog;
});
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
// The former case should be handled (give the App its real ID), the latter is a security issue
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
if (app.id != newInfo.packageName) {
var isTempId = SourceProvider().isTempId(app);
if (apps[app.id] != null && !isTempId) {
throw IDChangedError();
}
var originalAppId = app.id;
app.id = newInfo.packageName;
downloadedFile = downloadedFile.renameSync(
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
if (apps[originalAppId] != null) {
await removeApps([originalAppId]);
await saveApps([app], onlyIfExists: !isTempId);
}
// Set to 90 for remaining steps, will make null in 'finally'
if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = -1;
notifyListeners();
notif = DownloadNotification(app.finalName, -1);
notificationsProvider?.notify(notif);
}
// Delete older versions of the APK if any
PackageArchiveInfo? newInfo;
var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk');
Directory? xapkDir;
if (isAPK) {
newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
} else {
// Assume XAPK
String xapkDirPath = '${downloadedFile.path}-dir';
unzipFile(downloadedFile.path, '${downloadedFile.path}-dir');
xapkDir = Directory(xapkDirPath);
var apks = xapkDir
.listSync()
.where((e) => e.path.toLowerCase().endsWith('.apk'))
.toList();
newInfo = await PackageArchiveInfo.fromPath(apks.first.path);
}
downloadedFile =
await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl);
// Delete older versions of the file if any
for (var file in downloadedFile.parent.listSync()) {
var fn = file.path.split('/').last;
if (fn.startsWith('${app.id}-') &&
fn.endsWith('.apk') &&
fn != fileName) {
FileSystemEntity.isFileSync(file.path) &&
file.path != downloadedFile.path) {
file.delete();
}
}
return DownloadedApk(app.id, downloadedFile);
if (isAPK) {
return DownloadedApk(app.id, downloadedFile);
} else {
return DownloadedXApkDir(app.id, downloadedFile, xapkDir!);
}
} finally {
notificationsProvider?.cancel(notifId);
if (apps[app.id] != null) {
@ -263,11 +320,43 @@ class AppsProvider with ChangeNotifier {
}
}
// Unfortunately this 'await' does not actually wait for the APK to finish installing
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
// But even then, we don't know if it actually succeeded
Future<void> installApk(DownloadedApk file, {bool silent = false}) async {
void unzipFile(String filePath, String destinationPath) {
final bytes = File(filePath).readAsBytesSync();
final archive = ZipDecoder().decodeBytes(bytes);
for (final file in archive) {
final filename = '$destinationPath/${file.name}';
if (file.isFile) {
final data = file.content as List<int>;
File(filename)
..createSync(recursive: true)
..writeAsBytesSync(data);
} else {
Directory(filename).create(recursive: true);
}
}
}
Future<void> installXApkDir(DownloadedXApkDir dir,
{bool silent = false}) async {
try {
var somethingInstalled = false;
for (var apk in dir.extracted
.listSync()
.where((f) => f is File && f.path.toLowerCase().endsWith('.apk'))) {
somethingInstalled = somethingInstalled ||
await installApk(DownloadedApk(dir.appId, apk as File),
silent: silent);
}
if (somethingInstalled) {
dir.file.delete();
}
} finally {
dir.extracted.delete(recursive: true);
}
}
Future<bool> installApk(DownloadedApk file, {bool silent = false}) async {
// TODO: Use 'silent' when/if ever possible
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
AppInfo? appInfo;
@ -283,14 +372,17 @@ class AppsProvider with ChangeNotifier {
}
int? code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
bool installed = false;
if (code != null && code != 0 && code != 3) {
throw InstallError(code);
} else if (code == 0) {
installed = true;
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
file.file.delete();
}
await saveApps([apps[file.appId]!.app]);
return installed;
}
void uninstallApp(String appId) async {
@ -416,9 +508,16 @@ class AppsProvider with ChangeNotifier {
for (var id in appsToInstall) {
try {
// ignore: use_build_context_synchronously
var downloadedFile = await downloadApp(apps[id]!.app, context);
bool willBeSilent =
await canInstallSilently(apps[downloadedFile.appId]!.app);
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
DownloadedApk? downloadedFile;
DownloadedXApkDir? downloadedDir;
if (downloadedArtifact is DownloadedApk) {
downloadedFile = downloadedArtifact;
} else {
downloadedDir = downloadedArtifact as DownloadedXApkDir;
}
bool willBeSilent = await canInstallSilently(
apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app);
willBeSilent = false; // TODO: Remove this when silent updates work
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
true)) {
@ -428,7 +527,18 @@ class AppsProvider with ChangeNotifier {
// ignore: use_build_context_synchronously
await waitForUserToReturnToForeground(context);
}
await installApk(downloadedFile, silent: willBeSilent);
apps[id]?.downloadProgress = -1;
notifyListeners();
try {
if (downloadedFile != null) {
await installApk(downloadedFile, silent: willBeSilent);
} else {
await installXApkDir(downloadedDir!, silent: willBeSilent);
}
} finally {
apps[id]?.downloadProgress = null;
notifyListeners();
}
installedIds.add(id);
} catch (e) {
errors.add(id, e.toString());
@ -680,11 +790,18 @@ class AppsProvider with ChangeNotifier {
}
Future<void> removeApps(List<String> appIds) async {
var apkFiles = (await getExternalCacheDirectories())?.first.listSync();
for (var appId in appIds) {
File file = File('${(await getAppsDir()).path}/$appId.json');
if (file.existsSync()) {
file.deleteSync();
}
apkFiles
?.where(
(element) => element.path.split('/').last.startsWith('$appId-'))
.forEach((element) {
element.delete();
});
if (apps.containsKey(appId)) {
apps.remove(appId);
}
@ -730,7 +847,7 @@ class AppsProvider with ChangeNotifier {
apps[i].installedVersion = null;
}
}
await saveApps(apps, attemptToCorrectInstallStatus: !remove);
await saveApps(apps, attemptToCorrectInstallStatus: false);
}
if (remove) {
await removeApps(apps.map((e) => e.id).toList());

View File

@ -167,7 +167,8 @@ class NotificationsProvider {
progress: progPercent ?? 0,
maxProgress: 100,
showProgress: progPercent != null,
onlyAlertOnce: onlyAlertOnce)));
onlyAlertOnce: onlyAlertOnce,
indeterminate: progPercent != null && progPercent < 0)));
}
Future<void> notify(ObtainiumNotification notif,

View File

@ -35,6 +35,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
class SettingsProvider with ChangeNotifier {
SharedPreferences? prefs;
bool justStarted = true;
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
@ -92,6 +93,15 @@ class SettingsProvider with ChangeNotifier {
notifyListeners();
}
bool get checkOnStart {
return prefs?.getBool('checkOnStart') ?? false;
}
set checkOnStart(bool checkOnStart) {
prefs?.setBool('checkOnStart', checkOnStart);
notifyListeners();
}
SortColumnSettings get sortColumn {
return SortColumnSettings.values[
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
@ -120,6 +130,14 @@ class SettingsProvider with ChangeNotifier {
return result;
}
bool checkJustStarted() {
if (justStarted) {
justStarted = false;
return true;
}
return false;
}
Future<bool> getInstallPermission({bool enforce = false}) async {
while (!(await Permission.requestInstallPackages.isGranted)) {
// Explicit request as InstallPlugin request sometimes bugged
@ -154,6 +172,15 @@ class SettingsProvider with ChangeNotifier {
notifyListeners();
}
bool get buryNonInstalled {
return prefs?.getBool('buryNonInstalled') ?? false;
}
set buryNonInstalled(bool show) {
prefs?.setBool('buryNonInstalled', show);
notifyListeners();
}
bool get groupByCategory {
return prefs?.getBool('groupByCategory') ?? false;
}
@ -216,7 +243,7 @@ class SettingsProvider with ChangeNotifier {
String? get forcedLocale {
var fl = prefs?.getString('forcedLocale');
return supportedLocales
.where((element) => element.toLanguageTag() == fl)
.where((element) => element.key.toLanguageTag() == fl)
.isNotEmpty
? fl
: null;
@ -226,7 +253,7 @@ class SettingsProvider with ChangeNotifier {
if (fl == null) {
prefs?.remove('forcedLocale');
} else if (supportedLocales
.where((element) => element.toLanguageTag() == fl)
.where((element) => element.key.toLanguageTag() == fl)
.isNotEmpty) {
prefs?.setString('forcedLocale', fl);
}

View File

@ -7,7 +7,9 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:html/dom.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/apkcombo.dart';
import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/apkpure.dart';
import 'package:obtainium/app_sources/codeberg.dart';
import 'package:obtainium/app_sources/fdroid.dart';
import 'package:obtainium/app_sources/fdroidrepo.dart';
@ -15,10 +17,12 @@ import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/gitlab.dart';
import 'package:obtainium/app_sources/izzyondroid.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/app_sources/jenkins.dart';
import 'package:obtainium/app_sources/mullvad.dart';
import 'package:obtainium/app_sources/neutroncode.dart';
import 'package:obtainium/app_sources/signal.dart';
import 'package:obtainium/app_sources/sourceforge.dart';
import 'package:obtainium/app_sources/sourcehut.dart';
import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/app_sources/telegramapp.dart';
import 'package:obtainium/app_sources/vlc.dart';
@ -314,11 +318,28 @@ abstract class AppSource {
late String name;
bool enforceTrackOnly = false;
bool changeLogIfAnyIsMarkDown = true;
bool overrideEligible = false;
AppSource() {
name = runtimeType.toString();
}
overrideVersionDetectionFormDefault(String vd, bool disableStandard) {
additionalAppSpecificSourceAgnosticSettingFormItems =
additionalAppSpecificSourceAgnosticSettingFormItems.map((e) {
return e.map((e2) {
if (e2.key == 'versionDetection') {
var item = e2 as GeneratedFormDropdown;
item.defaultValue = vd;
if (disableStandard) {
item.disabledOptKeys = ['standardVersionDetection'];
}
}
return e2;
}).toList();
}).toList();
}
String standardizeUrl(String url) {
url = preStandardizeUrl(url);
if (!hostChanged) {
@ -327,6 +348,18 @@ abstract class AppSource {
return url;
}
Map<String, String>? get requestHeaders => null;
Future<Response> sourceRequest(String url) async {
if (requestHeaders != null) {
var req = Request('GET', Uri.parse(url));
req.headers.addAll(requestHeaders!);
return Response.fromStream(await Client().send(req));
} else {
return get(Uri.parse(url));
}
}
String sourceSpecificStandardizeURL(String url) {
throw NotImplementedError();
}
@ -341,7 +374,7 @@ abstract class AppSource {
[];
// Some additional data may be needed for Apps regardless of Source
final List<List<GeneratedFormItem>>
List<List<GeneratedFormItem>>
additionalAppSpecificSourceAgnosticSettingFormItems = [
[
GeneratedFormSwitch(
@ -393,12 +426,13 @@ abstract class AppSource {
return null;
}
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
return apkUrl;
}
bool canSearch = false;
Future<Map<String, String>> search(String query) {
Future<Map<String, List<String>>> search(String query) {
throw NotImplementedError();
}
@ -416,7 +450,7 @@ ObtainiumError getObtainiumHttpError(Response res) {
abstract class MassAppUrlSource {
late String name;
late List<String> requiredArgs;
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
Future<Map<String, List<String>>> getUrlsWithDescriptions(List<String> args);
}
regExValidator(String? value) {
@ -440,8 +474,12 @@ class SourceProvider {
FDroid(),
IzzyOnDroid(),
FDroidRepo(),
Jenkins(),
SourceForge(),
SourceHut(),
APKMirror(),
APKPure(),
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
Mullvad(),
Signal(),
VLC(),

View File

@ -5,18 +5,18 @@ packages:
dependency: "direct main"
description:
name: android_alarm_manager_plus
sha256: "88a8001851fdc9bd54fa4e30d0277bb900a50f3d86ff244da7f027400bf23ac0"
sha256: ed5fb34f8befc382fb4800b02aa86a34d279b911e1c05752f702d12fcfe26e0e
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "3.0.0"
android_intent_plus:
dependency: "direct main"
description:
name: android_intent_plus
sha256: "04cbc7c332a6f0bba88fed354de78813e9d24049c1800aaf10f449c7adc22603"
sha256: f79fbb8ccb64b5584d19caa9c3d15613bf21cfbd829a6ca7f089fb5dfd43f8aa
url: "https://pub.dev"
source: hosted
version: "3.1.9"
version: "4.0.0"
android_package_installer:
dependency: "direct main"
description:
@ -34,22 +34,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
archive:
dependency: "direct main"
description:
name: archive
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
url: "https://pub.dev"
source: hosted
version: "3.3.7"
args:
dependency: transitive
description:
name: args
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
async:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
@ -62,10 +70,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.3.0"
clock:
dependency: transitive
description:
@ -78,10 +86,18 @@ packages:
dependency: transitive
description:
name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.17.1"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
cross_file:
dependency: transitive
description:
@ -94,10 +110,10 @@ packages:
dependency: transitive
description:
name: crypto
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
csslib:
dependency: transitive
description:
@ -126,10 +142,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903
sha256: "9b1a0c32b2a503f8fe9f8764fac7b5fcd4f6bd35d8f49de5350bccf9e2a33b8a"
url: "https://pub.dev"
source: hosted
version: "8.2.2"
version: "9.0.0"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -150,10 +166,10 @@ packages:
dependency: "direct main"
description:
name: easy_localization
sha256: "6a2e99fa0bfe5765bf4c6ca9b137d5de2c75593007178c5e4cd2ae985f870080"
sha256: f30e9b20ed4d1b890171c30241d9b9c43efe21fee55dee7bd68f94daf269ea75
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2-dev.2"
easy_logger:
dependency: transitive
description:
@ -174,10 +190,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.0.2"
file:
dependency: transitive
description:
@ -190,10 +206,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff
sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
url: "https://pub.dev"
source: hosted
version: "5.2.10"
version: "5.3.0"
flutter:
dependency: "direct main"
description: flutter
@ -219,26 +235,26 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "2876372952b65ca7f684e698eba22bda1cf581fa071dd30ba2f01900f507d0d1"
sha256: ee6ee56855aa920899b68586b538474d086c149932220b47b92502cbfb5ba5e5
url: "https://pub.dev"
source: hosted
version: "14.0.0+1"
version: "14.0.0+2"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "909bb95de05a2e793503a2437146285a2f600cd0b3f826e26b870a334d8586d7"
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "4.0.0+1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "63235c42de5b6c99846969a27ad0209c401e6b77b0498939813725b5791c107c"
sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "7.0.0+1"
flutter_localizations:
dependency: transitive
description: flutter
@ -256,10 +272,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb"
sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
version: "2.0.14"
flutter_test:
dependency: "direct dev"
description: flutter
@ -282,18 +298,18 @@ packages:
dependency: "direct main"
description:
name: html
sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb"
sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8"
url: "https://pub.dev"
source: hosted
version: "0.15.2"
version: "0.15.3"
http:
dependency: "direct main"
description:
name: http
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
url: "https://pub.dev"
source: hosted
version: "0.13.5"
version: "0.13.6"
http_parser:
dependency: transitive
description:
@ -314,42 +330,42 @@ packages:
dependency: transitive
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
url: "https://pub.dev"
source: hosted
version: "0.17.0"
version: "0.18.0"
js:
dependency: transitive
description:
name: js
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.7"
lints:
dependency: transitive
description:
name: lints
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.1.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5
sha256: "8e332924094383133cee218b676871f42db2514f1f6ac617b6cf6152a7faab8e"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
version: "7.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev"
source: hosted
version: "0.12.13"
version: "0.12.15"
material_color_utilities:
dependency: transitive
description:
@ -362,10 +378,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
version: "1.9.1"
mime:
dependency: transitive
description:
@ -402,18 +418,18 @@ packages:
dependency: transitive
description:
name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.2"
version: "1.8.3"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
url: "https://pub.dev"
source: hosted
version: "2.0.14"
version: "2.0.15"
path_provider_android:
dependency: transitive
description:
@ -426,10 +442,10 @@ packages:
dependency: transitive
description:
name: path_provider_foundation
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.2.3"
path_provider_linux:
dependency: transitive
description:
@ -498,10 +514,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
url: "https://pub.dev"
source: hosted
version: "5.1.0"
version: "5.4.0"
platform:
dependency: transitive
description:
@ -518,6 +534,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
url: "https://pub.dev"
source: hosted
version: "3.7.3"
process:
dependency: transitive
description:
@ -538,10 +562,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1
sha256: "322a1ec9d9fe07e2e2252c098ce93d12dbd06133cc4c00ffe6a4ef505c295c17"
url: "https://pub.dev"
source: hosted
version: "6.3.4"
version: "7.0.0"
share_plus_platform_interface:
dependency: transitive
description:
@ -554,10 +578,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
shared_preferences_android:
dependency: transitive
description:
@ -570,10 +594,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
shared_preferences_linux:
dependency: transitive
description:
@ -623,10 +647,10 @@ packages:
dependency: "direct main"
description:
name: sqflite
sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00"
url: "https://pub.dev"
source: hosted
version: "2.2.8"
version: "2.2.8+2"
sqflite_common:
dependency: transitive
description:
@ -679,10 +703,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev"
source: hosted
version: "0.4.16"
version: "0.5.1"
timezone:
dependency: transitive
description:
@ -695,26 +719,26 @@ packages:
dependency: transitive
description:
name: typed_data
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
url: "https://pub.dev"
source: hosted
version: "6.1.10"
version: "6.1.11"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd"
sha256: "7aac14be5f4731b923cc697ae2d42043945076cd0dbb8806baecc92c1dc88891"
url: "https://pub.dev"
source: hosted
version: "6.0.31"
version: "6.0.33"
url_launcher_ios:
dependency: transitive
description:
@ -791,10 +815,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: d6cf18cd6c809c5a9294cd99707a21986aac4e08c87e1916ce2590315fb55d3a
sha256: "1acea8def62592123e2fbbca164ed8681a98a890bdcbb88f916d5b4a22687759"
url: "https://pub.dev"
source: hosted
version: "3.6.2"
version: "3.7.0"
webview_flutter_platform_interface:
dependency: transitive
description:
@ -807,18 +831,26 @@ packages:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: c94d242d8cbe1012c06ba7ac790c46d6e6b68723b7d34f8c74ed19f68d166f49
sha256: "4646bb68297803bdbb96d46853e8fcb560d6cb5e04153fa64581535767875dfe"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.3"
win32:
dependency: transitive
description:
name: win32
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "4.1.4"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xdg_directories:
dependency: transitive
description:
@ -831,10 +863,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
url: "https://pub.dev"
source: hosted
version: "6.2.2"
version: "6.3.0"
sdks:
dart: ">=2.19.0 <3.0.0"
dart: ">=3.0.0-417 <4.0.0"
flutter: ">=3.4.0-17.0.pre"

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: 0.12.1+161 # When changing this, update the tag in main() accordingly
version: 0.13.3+167 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.18.2 <3.0.0'
@ -48,21 +48,22 @@ dependencies:
url_launcher: ^6.1.5
permission_handler: ^10.0.0
fluttertoast: ^8.0.9
device_info_plus: ^8.0.0
device_info_plus: ^9.0.0
file_picker: ^5.2.10
animations: ^2.0.4
android_package_installer:
git:
url: https://github.com/ImranR98/android_package_installer
ref: main
share_plus: ^6.0.1
share_plus: ^7.0.0
installed_apps: ^1.3.1
package_archive_info: ^0.1.0
android_alarm_manager_plus: ^2.1.0
android_alarm_manager_plus: ^3.0.0
sqflite: ^2.2.0+3
easy_localization: ^3.0.1
android_intent_plus: ^3.1.5
android_intent_plus: ^4.0.0
flutter_markdown: ^0.6.14
archive: ^3.3.7
dev_dependencies: