mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
38 Commits
v0.12.1-be
...
v0.13.0-be
Author | SHA1 | Date | |
---|---|---|---|
33d3fc2d8e | |||
b07f5dd6b6 | |||
b43e13bb56 | |||
3be5543df4 | |||
91ad9efa43 | |||
ee292146d1 | |||
12867634b6 | |||
2e4fe89b85 | |||
b4642e16ad | |||
8ca5964d31 | |||
30c89fe385 | |||
fb9e66332d | |||
84b512f282 | |||
6f9aa85a72 | |||
639fc20fcb | |||
75631e5c5a | |||
9ec345761e | |||
1f9c2c1699 | |||
cbec486ad1 | |||
85ef60d4a8 | |||
44bde571bf | |||
eaaee5e7cd | |||
e1980f4de2 | |||
be9c671a56 | |||
0404449842 | |||
d6366a145e | |||
0a751cf545 | |||
5885ea57ad | |||
f8b326529f | |||
9f5f1174ba | |||
779de58f74 | |||
76e316422c | |||
36273fe02d | |||
03b592521c | |||
a5ef47a060 | |||
289c801fec | |||
73d04b1564 | |||
9469d56144 |
@ -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)
|
||||
|
@ -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?",
|
||||
@ -224,10 +223,14 @@
|
||||
"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": "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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "App entfernen?",
|
||||
"other": "Apps entfernen?"
|
||||
|
@ -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",
|
||||
@ -228,6 +227,10 @@
|
||||
"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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "Remove App?",
|
||||
"other": "Remove Apps?"
|
||||
|
282
assets/translations/es.json
Normal file
282
assets/translations/es.json
Normal file
@ -0,0 +1,282 @@
|
||||
{
|
||||
"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)",
|
||||
"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."
|
||||
}
|
||||
}
|
@ -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,14 @@
|
||||
"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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "برنامه حذف شود؟",
|
||||
"other": "برنامه ها حذف شوند؟"
|
||||
|
@ -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",
|
||||
@ -228,6 +227,10 @@
|
||||
"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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "Supprimer l'application ?",
|
||||
"other": "Supprimer les applications ?"
|
||||
|
@ -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,14 @@
|
||||
"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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "Eltávolítja az alkalmazást?",
|
||||
"other": "Eltávolítja az alkalmazást?"
|
||||
|
@ -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",
|
||||
@ -228,6 +227,10 @@
|
||||
"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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "Rimuovere l'App?",
|
||||
"other": "Rimuovere le App?"
|
||||
|
@ -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,14 @@
|
||||
"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": "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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "アプリを削除しますか?",
|
||||
"other": "アプリを削除しますか?"
|
||||
|
@ -20,7 +20,6 @@
|
||||
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
||||
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
|
||||
"githubPATFormat": "username:token",
|
||||
"githubPATLinkText": "关于 GitHub 个人访问令牌",
|
||||
"includePrereleases": "包含预发行版",
|
||||
"fallbackToOlderReleases": "将旧发行版作为备选",
|
||||
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
|
||||
@ -228,6 +227,10 @@
|
||||
"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)",
|
||||
"removeAppQuestion": {
|
||||
"one": "是否删除应用?",
|
||||
"other": "是否删除应用?"
|
||||
|
116
lib/app_sources/apkcombo.dart
Normal file
116
lib/app_sources/apkcombo.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
78
lib/app_sources/apkpure.dart
Normal file
78
lib/app_sources/apkpure.dart
Normal 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
|
||||
}
|
||||
|
||||
List<MapEntry<String, String>> apkUrls = [
|
||||
MapEntry('$appId.apk', 'https://d.$host/b/APK/$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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
@ -4,7 +4,91 @@ 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
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return url;
|
||||
@ -16,14 +100,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 +120,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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
68
lib/app_sources/jenkins.dart
Normal file
68
lib/app_sources/jenkins.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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')
|
||||
|
@ -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;
|
||||
|
@ -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'];
|
||||
|
@ -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 =
|
||||
|
101
lib/app_sources/sourcehut.dart
Normal file
101
lib/app_sources/sourcehut.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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 =
|
||||
|
@ -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 = [];
|
||||
|
@ -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')
|
||||
|
@ -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'));
|
||||
}
|
||||
|
@ -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.0';
|
||||
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 !=
|
||||
|
@ -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 =
|
||||
|
@ -166,7 +166,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
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 +248,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) {
|
||||
@ -300,8 +311,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 +340,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 25,
|
||||
),
|
||||
height: 16,
|
||||
)
|
||||
]);
|
||||
|
||||
bool shouldShowSearchBar() =>
|
||||
@ -370,20 +383,18 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
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 +468,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
|
||||
|
@ -185,6 +185,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) {
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
|
||||
|
@ -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,26 @@ 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,
|
||||
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 +248,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 +275,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 +287,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 +299,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 +325,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 +341,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 +353,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,
|
||||
|
@ -125,10 +125,13 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
downloadFile(String url, String fileName, Function? onProgress,
|
||||
{bool useExisting = true}) async {
|
||||
{bool useExisting = true, Map<String, String>? headers}) async {
|
||||
var destDir = (await getExternalCacheDirectories())!.first.path;
|
||||
StreamedResponse response =
|
||||
await Client().send(Request('GET', Uri.parse(url)));
|
||||
var req = Request('GET', Uri.parse(url));
|
||||
if (headers != null) {
|
||||
req.headers.addAll(headers);
|
||||
}
|
||||
StreamedResponse response = await Client().send(req);
|
||||
File downloadedFile = File('$destDir/$fileName');
|
||||
if (!(downloadedFile.existsSync() && useExisting)) {
|
||||
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
||||
@ -170,15 +173,16 @@ class AppsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
try {
|
||||
String downloadUrl = await SourceProvider()
|
||||
.getSource(app.url, overrideSource: app.overrideSource)
|
||||
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value);
|
||||
AppSource source = SourceProvider()
|
||||
.getSource(app.url, overrideSource: app.overrideSource);
|
||||
String downloadUrl = await source.apkUrlPrefetchModifier(
|
||||
app.apkUrls[app.preferredApkIndex].value, app.url);
|
||||
var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
|
||||
var notif = DownloadNotification(app.finalName, 100);
|
||||
notificationsProvider?.cancel(notif.id);
|
||||
int? prevProg;
|
||||
File downloadedFile =
|
||||
await downloadFile(downloadUrl, fileName, (double? progress) {
|
||||
File downloadedFile = await downloadFile(downloadUrl, fileName,
|
||||
headers: source.requestHeaders, (double? progress) {
|
||||
int? prog = progress?.ceil();
|
||||
if (apps[app.id] != null) {
|
||||
apps[app.id]!.downloadProgress = progress;
|
||||
@ -212,7 +216,7 @@ class AppsProvider with ChangeNotifier {
|
||||
var fn = file.path.split('/').last;
|
||||
if (fn.startsWith('${app.id}-') &&
|
||||
fn.endsWith('.apk') &&
|
||||
fn != fileName) {
|
||||
fn != downloadedFile.path.split('/').last) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
@ -154,6 +154,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 +225,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 +235,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);
|
||||
}
|
||||
|
@ -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(),
|
||||
|
36
pubspec.lock
36
pubspec.lock
@ -38,10 +38,10 @@ packages:
|
||||
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:
|
||||
@ -94,10 +94,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:
|
||||
@ -256,10 +256,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 +282,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:
|
||||
@ -338,10 +338,10 @@ packages:
|
||||
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:
|
||||
@ -623,10 +623,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
|
||||
sha256: acf091c6e55c50d00b30b8532b2dd23e393cf775861665ebd0f15cdd6ebfb079
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.8"
|
||||
version: "2.2.8+1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -791,10 +791,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,10 +807,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: c94d242d8cbe1012c06ba7ac790c46d6e6b68723b7d34f8c74ed19f68d166f49
|
||||
sha256: "61f33512810bf1ee9ac89761a4b02663ff64e8227b7dc80654642acd660fd49d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
version: "3.4.2"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -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.0+164 # When changing this, update the tag in main() accordingly
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.2 <3.0.0'
|
||||
|
Reference in New Issue
Block a user