mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 04:53:28 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			v1.1.3
			...
			experiment
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | ab43856b90 | 
							
								
								
									
										2
									
								
								.flutter
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								.flutter
									
									
									
									
									
								
							 Submodule .flutter updated: 300451adae...ba39319843
									
								
							
							
								
								
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,15 +15,6 @@ jobs: | ||||
|        | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: subosito/flutter-action@v2 | ||||
|       - uses: actions/setup-java@v4 | ||||
|         with: | ||||
|           distribution: 'temurin' # See 'Supported distributions' for available options | ||||
|           java-version: '17' | ||||
|  | ||||
|       - name: Flutter Doctor | ||||
|         id: flutter_doctor | ||||
|         run: | | ||||
|           flutter doctor -v | ||||
|  | ||||
|       - name: Import GPG key | ||||
|         id: import_pgp_key | ||||
| @@ -51,6 +42,7 @@ jobs: | ||||
|           if [ ${{ inputs.beta }} == true ]; then BETA=true; else BETA=false; fi | ||||
|           echo "beta=$BETA" >> $GITHUB_OUTPUT | ||||
|           TAG="v$VERSION" | ||||
|           if [ $BETA == true ]; then TAG="$TAG"-beta; fi | ||||
|           echo "tag=$TAG" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Build APKs | ||||
|   | ||||
| @@ -18,6 +18,7 @@ Currently supported App sources: | ||||
|   - [F-Droid](https://f-droid.org/) | ||||
|   - Third Party F-Droid Repos | ||||
|   - [IzzyOnDroid](https://android.izzysoft.de/) | ||||
|   - [SourceForge](https://sourceforge.net/) | ||||
|   - [SourceHut](https://git.sr.ht/) | ||||
| - Other - General: | ||||
|   - [APKPure](https://apkpure.net/) | ||||
|   | ||||
| @@ -1,9 +1,3 @@ | ||||
| plugins { | ||||
|     id "com.android.application" | ||||
|     id "kotlin-android" | ||||
|     id "dev.flutter.flutter-gradle-plugin" | ||||
| } | ||||
|  | ||||
| def localProperties = new Properties() | ||||
| def localPropertiesFile = rootProject.file('local.properties') | ||||
| if (localPropertiesFile.exists()) { | ||||
| @@ -12,6 +6,11 @@ if (localPropertiesFile.exists()) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| def flutterRoot = localProperties.getProperty('flutter.sdk') | ||||
| if (flutterRoot == null) { | ||||
|     throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") | ||||
| } | ||||
|  | ||||
| def flutterVersionCode = localProperties.getProperty('flutter.versionCode') | ||||
| if (flutterVersionCode == null) { | ||||
|     flutterVersionCode = '1' | ||||
| @@ -22,6 +21,11 @@ if (flutterVersionName == null) { | ||||
|     flutterVersionName = '1.0' | ||||
| } | ||||
|  | ||||
| apply plugin: 'com.android.application' | ||||
| apply plugin: 'kotlin-android' | ||||
| apply plugin: 'dev.rikka.tools.refine' | ||||
| apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" | ||||
|  | ||||
| def keystoreProperties = new Properties() | ||||
| def keystorePropertiesFile = rootProject.file('key.properties') | ||||
| if (keystorePropertiesFile.exists()) { | ||||
| @@ -29,8 +33,7 @@ if (keystorePropertiesFile.exists()) { | ||||
| } | ||||
|  | ||||
| android { | ||||
|     namespace "dev.imranr.obtainium" | ||||
|     compileSdk flutter.compileSdkVersion | ||||
|     compileSdkVersion rootProject.ext.compileSdkVersion | ||||
|     ndkVersion flutter.ndkVersion | ||||
|  | ||||
|     compileOptions { | ||||
| @@ -51,7 +54,7 @@ android { | ||||
|         // You can update the following values to match your application needs. | ||||
|         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. | ||||
|         minSdkVersion 24 | ||||
|         targetSdkVersion flutter.targetSdkVersion | ||||
|         targetSdkVersion rootProject.ext.targetSdkVersion | ||||
|         versionCode flutterVersionCode.toInteger() | ||||
|         versionName flutterVersionName | ||||
|     } | ||||
|   | ||||
| @@ -1,3 +1,22 @@ | ||||
| buildscript { | ||||
|     ext.kotlin_version = '1.8.10' | ||||
|     ext { | ||||
|         compileSdkVersion   = 34                // or latest | ||||
|         targetSdkVersion    = 34                // or latest | ||||
|         appCompatVersion    = "1.4.2"           // or latest | ||||
|     } | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|     } | ||||
|  | ||||
|     dependencies { | ||||
|         classpath "com.android.tools.build:gradle:7.4.2" | ||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||||
|         classpath "dev.rikka.tools.refine:gradle-plugin:4.3.1" | ||||
|     } | ||||
| } | ||||
|  | ||||
| allprojects { | ||||
|     repositories { | ||||
|         google() | ||||
|   | ||||
| @@ -1,25 +1,11 @@ | ||||
| pluginManagement { | ||||
|     def flutterSdkPath = { | ||||
| include ':app' | ||||
|  | ||||
| def localPropertiesFile = new File(rootProject.projectDir, "local.properties") | ||||
| def properties = new Properties() | ||||
|         file("local.properties").withInputStream { properties.load(it) } | ||||
|  | ||||
| assert localPropertiesFile.exists() | ||||
| localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } | ||||
|  | ||||
| def flutterSdkPath = properties.getProperty("flutter.sdk") | ||||
| assert flutterSdkPath != null, "flutter.sdk not set in local.properties" | ||||
|         return flutterSdkPath | ||||
|     }() | ||||
|  | ||||
|     includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") | ||||
|  | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|         gradlePluginPortal() | ||||
|     } | ||||
| } | ||||
|  | ||||
| plugins { | ||||
|     id "dev.flutter.flutter-plugin-loader" version "1.0.0" | ||||
|     id "com.android.application" version "7.4.2" apply false | ||||
|     id "org.jetbrains.kotlin.android" version "1.8.10" apply false | ||||
| } | ||||
|  | ||||
| include ":app" | ||||
| apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Onemogući detekciju verzije", | ||||
|     "noVersionDetectionExplanation": "Ova opcija bi se trebala koristiti samo za aplikacije gdje detekcija verzije ne radi ispravno.", | ||||
|     "downloadingX": "Preuzimanje {}", | ||||
|     "downloadX": "Download {}", | ||||
|     "downloadedX": "Downloaded {}", | ||||
|     "releaseAsset": "Release Asset", | ||||
|     "downloadNotifDescription": "Obavještava korisnika o napretku u preuzimanju aplikacije", | ||||
|     "noAPKFound": "APK nije pronađen", | ||||
|     "noVersionDetection": "Nema detekcije verzije", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Note", | ||||
|     "selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.", | ||||
|     "badDownload": "The APK could not be parsed (incompatible or partial download)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)", | ||||
|     "appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Želite li ukloniti aplikaciju?", | ||||
|         "other": "Želite li ukloniti aplikacije?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Deaktivovat detekci verze", | ||||
|     "noVersionDetectionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně.", | ||||
|     "downloadingX": "Stáhnout {}", | ||||
|     "downloadX": "Stáhnout {}", | ||||
|     "downloadedX": "Staženo {}", | ||||
|     "releaseAsset": "Vydání aktiva", | ||||
|     "downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace", | ||||
|     "noAPKFound": "Žádná APK nebyla nalezena", | ||||
|     "noVersionDetection": "Žádná detekce verze", | ||||
| @@ -221,7 +218,7 @@ | ||||
|     "dontShowTrackOnlyWarnings": "Nezobrazovat varování pro 'Jen sledované'", | ||||
|     "dontShowAPKOriginWarnings": "Nezobrazovat varování pro původ APK", | ||||
|     "moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace", | ||||
|     "gitlabPATLabel": "Osobní přístupový token GitLab", | ||||
|     "gitlabPATLabel": "GitLab Personal Access Token", | ||||
|     "about": "O", | ||||
|     "requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)", | ||||
|     "checkOnStart": "Zkontrolovat jednou při spuštění", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Poznámka", | ||||
|     "selfHostedNote": "Rozbalovací seznam \"{}\" lze použít k dosažení vlastních/obvyklých instancí libovolného zdroje.", | ||||
|     "badDownload": "APK nelze analyzovat (nekompatibilní nebo částečné stažení)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Sdílení nových aplikací s aplikací AppVerifier (pokud je k dispozici)", | ||||
|     "appVerifierInstructionToast": "Sdílejte do aplikace AppVerifier a po dokončení se sem vraťte.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Odstranit Apku?", | ||||
|         "other": "Odstranit Apky?" | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|     "noDescription": "Keine Beschreibung", | ||||
|     "cancel": "Abbrechen", | ||||
|     "continue": "Weiter", | ||||
|     "requiredInBrackets": "(wird benötigt)", | ||||
|     "requiredInBrackets": "(Benötigt)", | ||||
|     "dropdownNoOptsError": "FEHLER: DROPDOWN MUSS MINDESTENS EINE OPTION HABEN", | ||||
|     "colour": "Farbe", | ||||
|     "githubStarredRepos": "GitHub Starred Repos", | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Versionsermittlung deaktivieren", | ||||
|     "noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.", | ||||
|     "downloadingX": "Lade {} herunter", | ||||
|     "downloadX": "{} herunterladen", | ||||
|     "downloadedX": "{} heruntergeladen", | ||||
|     "releaseAsset": "release Asset", | ||||
|     "downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App", | ||||
|     "noAPKFound": "Keine APK gefunden", | ||||
|     "noVersionDetection": "Keine Versionserkennung", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Hinweis", | ||||
|     "selfHostedNote": "Das „{}“-Dropdown-Menü kann verwendet werden, um selbst gehostete/angepasste Instanzen einer beliebigen Quelle zu erreichen.", | ||||
|     "badDownload": "Die APK konnte nicht geparst werden (inkompatibler oder teilweiser Download)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Neue Apps mit AppVerifier teilen (falls verfügbar)", | ||||
|     "appVerifierInstructionToast": "Geben Sie die Daten an AppVerifier weiter und kehren Sie dann hierher zurück, wenn Sie fertig sind.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "App entfernen?", | ||||
|         "other": "Apps entfernen?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Disable Version Detection", | ||||
|     "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", | ||||
|     "downloadingX": "Downloading {}", | ||||
|     "downloadX": "Download {}", | ||||
|     "downloadedX": "Downloaded {}", | ||||
|     "releaseAsset": "Release Asset", | ||||
|     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", | ||||
|     "noAPKFound": "No APK found", | ||||
|     "noVersionDetection": "No version detection", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Note", | ||||
|     "selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.", | ||||
|     "badDownload": "The APK could not be parsed (incompatible or partial download)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)", | ||||
|     "appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Remove App?", | ||||
|         "other": "Remove Apps?" | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| { | ||||
|     "invalidURLForSource": "El URL de la aplicación {} no es válido", | ||||
|     "invalidURLForSource": "URL de la aplicación {} no es válida", | ||||
|     "noReleaseFound": "No se ha podido encontrar una versión válida", | ||||
|     "noVersionFound": "No se ha podido determinar la versión", | ||||
|     "urlMatchesNoSource": "El URL no coincide con ninguna fuente conocida", | ||||
|     "urlMatchesNoSource": "La URL no coincide con ninguna fuente conocida", | ||||
|     "cantInstallOlderVersion": "No se puede instalar una versión previa de la aplicación", | ||||
|     "appIdMismatch": "El id. del paquete descargado no coincide con la ID de la aplicación instalada", | ||||
|     "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": "Aceptar", | ||||
|     "ok": "OK", | ||||
|     "and": "y", | ||||
|     "githubPATLabel": "Token de acceso personal a GitHub\n(reduce tiempos de espera)", | ||||
|     "includePrereleases": "Incluir versiones preliminares", | ||||
| @@ -34,75 +34,75 @@ | ||||
|     "cancelled": "Cancelado", | ||||
|     "appAlreadyAdded": "Aplicación añadida anteriormente", | ||||
|     "alreadyUpToDateQuestion": "¿Aplicación actualizada previamente?", | ||||
|     "addApp": "Añadir aplicación", | ||||
|     "addApp": "Añadir Aplicación", | ||||
|     "appSourceURL": "URL de origen de la aplicación", | ||||
|     "error": "Error", | ||||
|     "add": "Añadir", | ||||
|     "searchSomeSourcesLabel": "Buscar (solo algunas fuentes)", | ||||
|     "search": "Buscar", | ||||
|     "additionalOptsFor": "Opciones adicionales para {}", | ||||
|     "supportedSources": "Fuentes admitidas", | ||||
|     "additionalOptsFor": "Opciones Adicionales para {}", | ||||
|     "supportedSources": "Fuentes Soportadas", | ||||
|     "trackOnlyInBrackets": "(Solo seguimiento)", | ||||
|     "searchableInBrackets": "(permite búsqueda)", | ||||
|     "searchableInBrackets": "(soporta búsqueda)", | ||||
|     "appsString": "Aplicaciones", | ||||
|     "noApps": "Sin Aplicaciones", | ||||
|     "noAppsForFilter": "Sin aplicaciones para filtrar", | ||||
|     "byX": "por: {}", | ||||
|     "percentProgress": "Progreso: {} %", | ||||
|     "pleaseWait": "Espere un momento", | ||||
|     "updateAvailable": "Actualización disponible", | ||||
|     "notInstalled": "No instalado", | ||||
|     "percentProgress": "Progreso: {}%", | ||||
|     "pleaseWait": "Por favor, espere", | ||||
|     "updateAvailable": "Actualización Disponible", | ||||
|     "notInstalled": "No Instalado", | ||||
|     "pseudoVersion": "pseudoversión", | ||||
|     "selectAll": "Seleccionar todo", | ||||
|     "selectAll": "Seleccionar Todo", | ||||
|     "deselectX": "Deseleccionar {}", | ||||
|     "xWillBeRemovedButRemainInstalled": "{} se eliminará de Obtainium pero continuará instalada en el dispositivo.", | ||||
|     "xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.", | ||||
|     "removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?", | ||||
|     "removeSelectedApps": "Eliminar aplicaciones seleccionadas", | ||||
|     "removeSelectedApps": "Eliminar Aplicaciones Seleccionadas", | ||||
|     "updateX": "Actualizar {}", | ||||
|     "installX": "Instalar {}", | ||||
|     "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo seguimiento)\ncomo actualizada", | ||||
|     "changeX": "Cambiar {}", | ||||
|     "installUpdateApps": "Instalar/actualizar aplicaciones", | ||||
|     "installUpdateSelectedApps": "Instalar/actualizar aplicaciones seleccionadas", | ||||
|     "installUpdateApps": "Instalar/Actualizar aplicaciones", | ||||
|     "installUpdateSelectedApps": "Instalar/Actualizar aplicaciones seleccionadas", | ||||
|     "markXSelectedAppsAsUpdated": "¿Marcar {} aplicaciones seleccionadas como actualizadas?", | ||||
|     "no": "No", | ||||
|     "yes": "Sí", | ||||
|     "markSelectedAppsUpdated": "Marcar aplicaciones seleccionadas como actualizadas", | ||||
|     "pinToTop": "Anclar al principio", | ||||
|     "unpinFromTop": "Desanclar del principio", | ||||
|     "pinToTop": "Fijar arriba", | ||||
|     "unpinFromTop": "Desfijar de arriba", | ||||
|     "resetInstallStatusForSelectedAppsQuestion": "¿Restuarar estado de instalación para las aplicaciones seleccionadas?", | ||||
|     "installStatusOfXWillBeResetExplanation": "Se restaurará el estado de instalación de las aplicaciones seleccionadas.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.", | ||||
|     "customLinkMessage": "Estos enlaces funcionan en dispositivos con Obtainium instalado", | ||||
|     "shareAppConfigLinks": "Compartir la configuración de la aplicación como enlace HTML", | ||||
|     "shareSelectedAppURLs": "Compartir URL de las aplicaciones seleccionadas", | ||||
|     "shareSelectedAppURLs": "Compartir URLs de las aplicaciones seleccionadas", | ||||
|     "resetInstallStatus": "Restaurar estado de instalación", | ||||
|     "more": "Más", | ||||
|     "removeOutdatedFilter": "Eliminar filtro de aplicaciones desactualizado", | ||||
|     "removeOutdatedFilter": "Elimiar filtro de aplicaciones desactualizado", | ||||
|     "showOutdatedOnly": "Mostrar solo aplicaciones desactualizadas", | ||||
|     "filter": "Filtrar", | ||||
|     "filterApps": "Filtrar actualizaciones", | ||||
|     "filterApps": "Filtrar Actualizaciones", | ||||
|     "appName": "Nombre de la aplicación", | ||||
|     "author": "Autor", | ||||
|     "upToDateApps": "Aplicaciones actualizadas", | ||||
|     "nonInstalledApps": "Aplicaciones no instaladas", | ||||
|     "importExport": "Importar/exportar", | ||||
|     "upToDateApps": "Aplicaciones Actualizadas", | ||||
|     "nonInstalledApps": "Aplicaciones No Instaladas", | ||||
|     "importExport": "Importar/Exportar", | ||||
|     "settings": "Ajustes", | ||||
|     "exportedTo": "Exportado a {}", | ||||
|     "obtainiumExport": "Exportar Obtainium", | ||||
|     "invalidInput": "Entrada no válida", | ||||
|     "invalidInput": "Input incorrecto", | ||||
|     "importedX": "Importado {}", | ||||
|     "obtainiumImport": "Importar Obtainium", | ||||
|     "importFromURLList": "Importar desde lista de URL", | ||||
|     "searchQuery": "Término de búsqueda", | ||||
|     "appURLList": "Lista de URL de aplicaciones", | ||||
|     "importFromURLList": "Importar desde lista de URLs", | ||||
|     "searchQuery": "Consulta de Búsqueda", | ||||
|     "appURLList": "Lista de URLs de Aplicaciones", | ||||
|     "line": "Línea", | ||||
|     "searchX": "Buscar {}", | ||||
|     "noResults": "No se encontró ningún resultado", | ||||
|     "noResults": "Resultados no encontrados", | ||||
|     "importX": "Importar desde {}", | ||||
|     "importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como «No instalada».\nPara solucionarlo, reinstálelas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a los URL y a los métodos de importación mediante terceros.", | ||||
|     "importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como \"No Instalada\".\nPara solucionarlo, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.", | ||||
|     "importErrors": "Errores de Importación", | ||||
|     "importedXOfYApps": "{} de {} aplicaciones importadas.", | ||||
|     "followingURLsHadErrors": "Los URL siguientes han tenido problemas:", | ||||
|     "importedXOfYApps": "{} de {} Aplicaciones importadas.", | ||||
|     "followingURLsHadErrors": "Las siguientes URLs han tenido problemas:", | ||||
|     "selectURL": "Seleccionar URL", | ||||
|     "selectURLs": "Seleccionar URLs", | ||||
|     "pick": "Escoger", | ||||
| @@ -110,9 +110,9 @@ | ||||
|     "dark": "Oscuro", | ||||
|     "light": "Claro", | ||||
|     "followSystem": "Seguir al sistema", | ||||
|     "useBlackTheme": "Negro puro en tema oscuro", | ||||
|     "appSortBy": "Ordenar aplicaciones por", | ||||
|     "authorName": "Autor/nombre", | ||||
|     "useBlackTheme": "Negro puro en tema Oscuro", | ||||
|     "appSortBy": "Ordenar Apps Por", | ||||
|     "authorName": "Autor/Nombre", | ||||
|     "nameAuthor": "Nombre/Autor", | ||||
|     "asAdded": "Según se Añadieron", | ||||
|     "appSortOrder": "Orden de Clasificación", | ||||
| @@ -122,22 +122,22 @@ | ||||
|     "neverManualOnly": "Nunca, solo manual", | ||||
|     "appearance": "Apariencia", | ||||
|     "showWebInAppView": "Mostrar vista de la web de origen", | ||||
|     "pinUpdates": "Anclar actualizaciones al principio", | ||||
|     "pinUpdates": "Fijar actualizaciones al principio", | ||||
|     "updates": "Actualizaciones", | ||||
|     "sourceSpecific": "Fuente específica", | ||||
|     "sourceSpecific": "Fuente Específica", | ||||
|     "appSource": "Obtainium en GitHub", | ||||
|     "noLogs": "Ningún registro", | ||||
|     "appLogs": "Registros", | ||||
|     "noLogs": "Sin Logs", | ||||
|     "appLogs": "Logs", | ||||
|     "close": "Cerrar", | ||||
|     "share": "Compartir", | ||||
|     "appNotFound": "Aplicación no encontrada", | ||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-exportación", | ||||
|     "pickAnAPK": "Seleccione una APK", | ||||
|     "appHasMoreThanOnePackage": "{} tiene más de un paquete:", | ||||
|     "deviceSupportsXArch": "Su dispositivo admite las siguientes arquitecturas de procesador: {}.", | ||||
|     "deviceSupportsFollowingArchs": "Su dispositivo admite las siguientes arquitecturas de procesador:", | ||||
|     "deviceSupportsXArch": "Su dispositivo soporta las siguientes arquitecturas de procesador: {}.", | ||||
|     "deviceSupportsFollowingArchs": "Su 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?", | ||||
|     "sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?", | ||||
|     "updatesAvailable": "Actualizaciones Disponibles", | ||||
|     "updatesAvailableNotifDescription": "Notifica al usuario de que hay actualizaciones para una o más aplicaciones monitoreadas por Obtainium", | ||||
|     "noNewUpdates": "No hay nuevas actualizaciones.", | ||||
| @@ -145,17 +145,17 @@ | ||||
|     "appsUpdated": "Aplicaciones actualizadas", | ||||
|     "appsUpdatedNotifDescription": "Notifica al usuario de que una o más aplicaciones han sido actualizadas en segundo plano", | ||||
|     "xWasUpdatedToY": "{} ha sido actualizada a {}.", | ||||
|     "errorCheckingUpdates": "Error al buscar actualizaciones", | ||||
|     "errorCheckingUpdates": "Error buscando ectualizaciones", | ||||
|     "errorCheckingUpdatesNotifDescription": "Una notificación que muestra cuándo la comprobación de actualizaciones en segundo plano falla", | ||||
|     "appsRemoved": "Aplicaciones eliminadas", | ||||
|     "appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas", | ||||
|     "xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}", | ||||
|     "completeAppInstallation": "Instalación completa de la aplicación", | ||||
|     "obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierto para instalar aplicaciones", | ||||
|     "completeAppInstallationNotifDescription": "Le pide volver a Obtainium para terminar de instalar una aplicación", | ||||
|     "completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para terminar de instalar una aplicación", | ||||
|     "checkingForUpdates": "Buscando actualizaciones...", | ||||
|     "checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones", | ||||
|     "pleaseAllowInstallPerm": "Permita que Obtainium instale aplicaciones", | ||||
|     "pleaseAllowInstallPerm": "Por favor, permita que Obtainium instale aplicaciones", | ||||
|     "trackOnly": "Solo para seguimiento", | ||||
|     "errorWithHttpStatusCode": "Error {}", | ||||
|     "versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)", | ||||
| @@ -173,8 +173,8 @@ | ||||
|     "appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre", | ||||
|     "reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones", | ||||
|     "fdroidThirdPartyRepo": "Repositorio de tercera parte F-Droid", | ||||
|     "steamMobile": "Steam para móviles", | ||||
|     "steamChat": "Chat de Steam", | ||||
|     "steamMobile": "Móvil de vapor", | ||||
|     "steamChat": "Chat de vapor", | ||||
|     "install": "Instalar", | ||||
|     "markInstalled": "Marcar como instalada", | ||||
|     "update": "Actualizar", | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Desactivar la detección de versiones", | ||||
|     "noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda que no funcionar correctamente.", | ||||
|     "downloadingX": "Descargando {}", | ||||
|     "downloadX": "Descargar {}", | ||||
|     "downloadedX": "Descargado {}", | ||||
|     "releaseAsset": "Liberar activos", | ||||
|     "downloadNotifDescription": "Notifica al usuario del progreso de descarga de una aplicación", | ||||
|     "noAPKFound": "No se encontró el paquete de instalación APK", | ||||
|     "noVersionDetection": "Sin detección de versiones", | ||||
| @@ -195,14 +192,14 @@ | ||||
|     "noCategory": "Sin categoría", | ||||
|     "noCategories": "Sin categorías", | ||||
|     "deleteCategoriesQuestion": "¿Eliminar categorías?", | ||||
|     "categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas se marcarán como «Sin categoría».", | ||||
|     "categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin categoría'.", | ||||
|     "addCategory": "Añadir categoría", | ||||
|     "label": "Nombre", | ||||
|     "language": "Idioma", | ||||
|     "copiedToClipboard": "Se copió en el portapapeles", | ||||
|     "copiedToClipboard": "Copiado al portapapeles", | ||||
|     "storagePermissionDenied": "Permiso de almacenamiento rechazado", | ||||
|     "selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaciones seleccionadas.", | ||||
|     "filterAPKsByRegEx": "Filtrar por APK", | ||||
|     "filterAPKsByRegEx": "Filtrar por APKs", | ||||
|     "removeFromObtainium": "Eliminar de Obtainium", | ||||
|     "uninstallFromDevice": "Desinstalar del dispositivo", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.", | ||||
| @@ -215,8 +212,8 @@ | ||||
|     "versionDetection": "Detección de versiones", | ||||
|     "standardVersionDetection": "Por versión", | ||||
|     "groupByCategory": "Agrupar por categoría", | ||||
|     "autoApkFilterByArch": "Filtrar APK por arquitectura del procesador (si es posible)", | ||||
|     "overrideSource": "Anular fuente", | ||||
|     "autoApkFilterByArch": "Filtrar APKs por arquitectura del procesador (si es posible)", | ||||
|     "overrideSource": "Sobrescribir Fuente", | ||||
|     "dontShowAgain": "No mostrar de nuevo", | ||||
|     "dontShowTrackOnlyWarnings": "No mostrar avisos sobre apps en 'solo seguimiento'", | ||||
|     "dontShowAPKOriginWarnings": "No mostrar avisos sobre las fuentes de las APKs", | ||||
| @@ -265,7 +262,7 @@ | ||||
|     "takeFirstLink": "Usar primer enlace", | ||||
|     "skipSort": "Omitir orden", | ||||
|     "debugMenu": "Menu Depurar", | ||||
|     "bgTaskStarted": "Iniciada tarea en segundo plano; revise los registros.", | ||||
|     "bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.", | ||||
|     "runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano", | ||||
|     "versionExtractWholePage": "Aplicar la versión de extracción regex a la página entera", | ||||
|     "installing": "Instalando", | ||||
| @@ -300,25 +297,23 @@ | ||||
|     "latest": "Versión más reciente", | ||||
|     "invertRegEx": "Invertir expresión regular", | ||||
|     "note": "Nota", | ||||
|     "selfHostedNote": "El desplegable «{}» puede utilizarse para acceder a instancias autoalojadas/personalizadas de cualquier fuente.", | ||||
|     "selfHostedNote": "El desplegable \"{}\" puede utilizarse para acceder a instancias autoalojadas/personalizadas de cualquier fuente.", | ||||
|     "badDownload": "No se ha podido analizar el APK (incompatible o descarga parcial)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Compartir nuevas aplicaciones con AppVerifier (si está disponible)", | ||||
|     "appVerifierInstructionToast": "Comparta con AppVerifier y vuelva aquí cuando esté listo.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "¿Eliminar aplicación?", | ||||
|         "other": "¿Eliminar aplicaciones?" | ||||
|         "one": "¿Eliminar Aplicación?", | ||||
|         "other": "¿Eliminar Aplicaciones?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Muchas peticiones (limitado); pruebe de nuevo en {} minuto", | ||||
|         "other": "Muchas peticiones (limitado); pruebe de nuevo en {} minutos" | ||||
|         "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" | ||||
|         "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 le notificará si es necesario", | ||||
|         "other": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualizaciones; se le notificará si es necesario" | ||||
|         "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", | ||||
| @@ -341,16 +336,16 @@ | ||||
|         "other": "{} días" | ||||
|     }, | ||||
|     "clearedNLogsBeforeXAfterY": { | ||||
|         "one": "Eliminado {n} registro (previo a = {before}, posterior a = {after})", | ||||
|         "other": "Eliminados {n} registros (previos a = {before}, posteriores a = {after})" | ||||
|         "one": "Eliminado {n} log (previo a = {before}, posterior a = {after})", | ||||
|         "other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesAvailable": { | ||||
|         "one": "{} y 1 aplicación más tiene actualizaciones.", | ||||
|         "other": "{} y {} aplicaciones más tienen actualizaciones." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} y 1 aplicación más se han actualizado.", | ||||
|         "other": "{} y {} aplicaciones más se han actualizado." | ||||
|         "one": "{} y 1 aplicación más han sido actualizadas.", | ||||
|         "other": "{} y {} aplicaciones más han sido actualizadas." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} y 1 aplicación más podría haber sido actualizada.", | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "غیرفعال کردن تشخیص نسخه", | ||||
|     "noVersionDetectionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند.", | ||||
|     "downloadingX": "در حال دانلود {}", | ||||
|     "downloadX": "Download {}", | ||||
|     "downloadedX": "Downloaded {}", | ||||
|     "releaseAsset": "Release Asset", | ||||
|     "downloadNotifDescription": "کاربر را از پیشرفت دانلود یک برنامه مطلع می کند", | ||||
|     "noAPKFound": "APK پیدا نشد فایل", | ||||
|     "noVersionDetection": "بدون تشخیص نسخه", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Note", | ||||
|     "selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.", | ||||
|     "badDownload": "The APK could not be parsed (incompatible or partial download)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)", | ||||
|     "appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "برنامه حذف شود؟", | ||||
|         "other": "برنامه ها حذف شوند؟" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Désactiver la détection de version", | ||||
|     "noVersionDetectionExplanation": "Cette option ne doit être utilisée que pour les applications où la détection de version ne fonctionne pas correctement.", | ||||
|     "downloadingX": "Téléchargement {}", | ||||
|     "downloadX": "Télécharger {}", | ||||
|     "downloadedX": "Téléchargé {}", | ||||
|     "releaseAsset": "Actif libéré", | ||||
|     "downloadNotifDescription": "Avertit l'utilisateur de la progression du téléchargement d'une application", | ||||
|     "noAPKFound": "Aucun APK trouvé", | ||||
|     "noVersionDetection": "Pas de détection de version", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Note", | ||||
|     "selfHostedNote": "La liste déroulante \"{}\" peut être utilisée pour accéder aux instances auto-hébergées/personnalisées de n'importe quelle source.", | ||||
|     "badDownload": "L'APK n'a pas pu être analysé (téléchargement incompatible ou partiel)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Partager les nouvelles applications avec AppVerifier (si disponible)", | ||||
|     "appVerifierInstructionToast": "Partagez avec AppVerifier, puis revenez ici lorsque vous êtes prêt.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Supprimer l'application ?", | ||||
|         "other": "Supprimer les applications ?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Verzió érzékelés letiltása", | ||||
|     "noVersionDetectionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzióérzékelés nem működik megfelelően.", | ||||
|     "downloadingX": "{} letöltés", | ||||
|     "downloadX": "Letöltés {}", | ||||
|     "downloadedX": "Letöltés {}", | ||||
|     "releaseAsset": "Kiadási tartalom", | ||||
|     "downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról", | ||||
|     "noAPKFound": "Nem található APK", | ||||
|     "noVersionDetection": "Nincs verzió érzékelés", | ||||
| @@ -221,7 +218,7 @@ | ||||
|     "dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést", | ||||
|     "dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket", | ||||
|     "moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára", | ||||
|     "gitlabPATLabel": "GitLab személyes hozzáférési token", | ||||
|     "gitlabPATLabel": "GitLab Personal Access Token", | ||||
|     "about": "Rólunk", | ||||
|     "requiresCredentialsInSettings": "{}: Ehhez további hitelesítő adatokra van szükség (a Beállításokban)", | ||||
|     "checkOnStart": "Egyszer az alkalmazás indításakor is", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Megjegyzés:", | ||||
|     "selfHostedNote": "A \"{}\" legördülő menü használható bármely forrás saját üzemeltetésű/egyéni példányainak eléréséhez.", | ||||
|     "badDownload": "Az APK-t nem lehetett elemezni (inkompatibilis vagy részleges letöltés)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Új alkalmazások megosztása az AppVerifierrel (ha elérhető)", | ||||
|     "appVerifierInstructionToast": "Ossza meg az AppVerifierrel, majd térjen vissza ide, ha kész.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Eltávolítja az alkalmazást?", | ||||
|         "other": "Eltávolítja az alkalmazásokat?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Disattiva il rilevamento della versione", | ||||
|     "noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le app la cui versione non viene rilevata correttamente.", | ||||
|     "downloadingX": "Scaricamento di {} in corso", | ||||
|     "downloadX": "Scarica {}", | ||||
|     "downloadedX": "Scaricato {}", | ||||
|     "releaseAsset": "Rilascio Asset", | ||||
|     "downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'app", | ||||
|     "noAPKFound": "Nessun APK trovato", | ||||
|     "noVersionDetection": "Disattiva rilevamento di versione", | ||||
| @@ -221,7 +218,7 @@ | ||||
|     "dontShowTrackOnlyWarnings": "Non mostrare gli avvisi 'Solo-Monitoraggio'", | ||||
|     "dontShowAPKOriginWarnings": "Non mostrare gli avvisi di origine dell'APK", | ||||
|     "moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista", | ||||
|     "gitlabPATLabel": "GitLab Token di accesso personale", | ||||
|     "gitlabPATLabel": "GitLab Personal Access Token", | ||||
|     "about": "Informazioni", | ||||
|     "requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)", | ||||
|     "checkOnStart": "Controlla una volta all'avvio", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Nota", | ||||
|     "selfHostedNote": "Il menu a tendina \"{}\" può essere usato per raggiungere istanze autogestite/personali di qualsiasi fonte.", | ||||
|     "badDownload": "Non è stato possibile analizzare l'APK (download incompatibile o parziale).", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Condividere le nuove applicazioni con AppVerifier (se disponibile)", | ||||
|     "appVerifierInstructionToast": "Condividete con AppVerifier, quindi tornate qui quando siete pronti.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Rimuovere l'app?", | ||||
|         "other": "Rimuovere le app?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "バージョン検出を無効にする", | ||||
|     "noVersionDetectionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリにのみ使用する必要があります。", | ||||
|     "downloadingX": "{} をダウンロード中", | ||||
|     "downloadX": "ダウンロード", | ||||
|     "downloadedX": "ダウンロード", | ||||
|     "releaseAsset": "リリース資産", | ||||
|     "downloadNotifDescription": "アプリのダウンロード状況を通知する", | ||||
|     "noAPKFound": "APKが見つかりません", | ||||
|     "noVersionDetection": "バージョン検出を行わない", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "注", | ||||
|     "selfHostedNote": "ドロップダウン\"{}\"を使用すると、あらゆるソースのセルフホスト/カスタムインスタンスにアクセスできます。", | ||||
|     "badDownload": "APK を解析できませんでした(互換性がないか、部分的にダウンロードされています)。", | ||||
|     "beforeNewInstallsShareToAppVerifier": "AppVerifierで新しいアプリを共有する(利用可能な場合)", | ||||
|     "appVerifierInstructionToast": "AppVerifierに共有し、準備ができたらここに戻ってください。", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "アプリを削除しますか?", | ||||
|         "other": "アプリを削除しますか?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Versieherkenning uitschakelen", | ||||
|     "noVersionDetectionExplanation": "Deze optie moet alleen worden gebruikt voor apps waar versieherkenning niet correct werkt.", | ||||
|     "downloadingX": "Downloaden {}", | ||||
|     "downloadX": "Downloaden", | ||||
|     "downloadedX": "Gedownload {}", | ||||
|     "releaseAsset": "Release Activa", | ||||
|     "downloadNotifDescription": "Stelt de gebruiker op de hoogte van de voortgang bij het downloaden van een app", | ||||
|     "noAPKFound": "Geen APK gevonden", | ||||
|     "noVersionDetection": "Geen versieherkenning", | ||||
| @@ -221,7 +218,7 @@ | ||||
|     "dontShowTrackOnlyWarnings": "Geen waarschuwingen voor 'Track-Only' weergeven", | ||||
|     "dontShowAPKOriginWarnings": "APK-herkomstwaarschuwingen niet weergeven", | ||||
|     "moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave", | ||||
|     "gitlabPATLabel": "GitLab persoonlijk toegangskenmerk", | ||||
|     "gitlabPATLabel": "GitLab Personal Access Token", | ||||
|     "about": "Over", | ||||
|     "requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)", | ||||
|     "checkOnStart": "Controleren op updates bij opstarten", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Opmerking", | ||||
|     "selfHostedNote": "De \"{}\" dropdown kan gebruikt worden om zelf gehoste/aangepaste instanties van elke bron te bereiken.", | ||||
|     "badDownload": "De APK kon niet worden verwerkt (incompatibele of gedeeltelijke download)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Nieuwe Apps delen met AppVerifier (indien beschikbaar)", | ||||
|     "appVerifierInstructionToast": "Deel naar AppVerifier en keer hier terug als je klaar bent.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "App verwijderen?", | ||||
|         "other": "Apps verwijderen?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Wyłącz wykrywanie wersji", | ||||
|     "noVersionDetectionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie.", | ||||
|     "downloadingX": "Pobieranie {}", | ||||
|     "downloadX": "Pobierz {}", | ||||
|     "downloadedX": "Pobrano {}", | ||||
|     "releaseAsset": "Release Asset", | ||||
|     "downloadNotifDescription": "Informuje o postępach w pobieraniu aplikacji", | ||||
|     "noAPKFound": "Nie znaleziono pakietu APK", | ||||
|     "noVersionDetection": "Bez wykrywania wersji", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Uwaga", | ||||
|     "selfHostedNote": "Lista rozwijana \"{}\" może być używana do uzyskiwania dostępu do samodzielnie hostowanych / niestandardowych instancji dowolnego źródła.", | ||||
|     "badDownload": "Nie można przeanalizować pliku APK (niekompatybilny lub częściowo pobrany).", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Udostępnianie nowych aplikacji za pomocą AppVerifier (jeśli dostępne)", | ||||
|     "appVerifierInstructionToast": "Udostępnij w AppVerifier, a następnie wróć tutaj, gdy będziesz gotowy.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Usunąć aplikację?", | ||||
|         "few": "Usunąć aplikacje?", | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Desativar detecção de versão", | ||||
|     "noVersionDetectionExplanation": "Essa opção deve apenas ser usada por aplicativos onde a detecção de versão não funciona corretamente.", | ||||
|     "downloadingX": "Baixando {}", | ||||
|     "downloadX": "Descarregar {}", | ||||
|     "downloadedX": "Descarregado {}", | ||||
|     "releaseAsset": "Libertação de activos", | ||||
|     "downloadNotifDescription": "Notifica o usuário o progresso do download de um aplicativo", | ||||
|     "noAPKFound": "APK não encontrado", | ||||
|     "noVersionDetection": "Sem detecção de versão", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Nota", | ||||
|     "selfHostedNote": "O menu suspenso \"{}\" pode ser usado para acessar instâncias auto-hospedadas/personalizadas de qualquer fonte.", | ||||
|     "badDownload": "Não foi possível analisar o APK (transferência incompatível ou parcial)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Partilhar novas aplicações com o AppVerifier (se disponível)", | ||||
|     "appVerifierInstructionToast": "Partilhe com o AppVerifier e, em seguida, regresse aqui quando estiver pronto.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Remover aplicativo?", | ||||
|         "other": "Remover aplicativos?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Отключить обнаружение версии", | ||||
|     "noVersionDetectionExplanation": "Эта настройка должна использоваться только для приложений, где обнаружение версии не работает корректно", | ||||
|     "downloadingX": "Загрузка {}", | ||||
|     "downloadX": "Скачать {}", | ||||
|     "downloadedX": "Загружено {}", | ||||
|     "releaseAsset": "Освобождение актива", | ||||
|     "downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения", | ||||
|     "noAPKFound": "APK не найден", | ||||
|     "noVersionDetection": "Обнаружение версий отключено", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Примечание", | ||||
|     "selfHostedNote": "Выпадающий список \"{}\" можно использовать для доступа к самостоятельно размещенным/настроенным экземплярам любого источника.", | ||||
|     "badDownload": "APK не удалось разобрать (несовместимая или неполная загрузка)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Поделитесь новыми приложениями с AppVerifier (если доступно)", | ||||
|     "appVerifierInstructionToast": "Поделитесь с AppVerifier, а затем вернитесь сюда, когда будете готовы.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Удалить приложение?", | ||||
|         "other": "Удалить приложения?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Inaktivera versionsdetektering", | ||||
|     "noVersionDetectionExplanation": "Det här alternativet bör endast användas för appar där versionsidentifiering inte fungerar korrekt.", | ||||
|     "downloadingX": "Laddar ner {}", | ||||
|     "downloadX": "Ladda ner {}", | ||||
|     "downloadedX": "Nedladdad {}", | ||||
|     "releaseAsset": "Frigör tillgång", | ||||
|     "downloadNotifDescription": "Meddelar användaren om framstegen med att ladda ner en app", | ||||
|     "noAPKFound": "Ingen APK funnen", | ||||
|     "noVersionDetection": "Ingen versiondetektering", | ||||
| @@ -221,7 +218,7 @@ | ||||
|     "dontShowTrackOnlyWarnings": "Visa inte 'Följ-Endast' varningar", | ||||
|     "dontShowAPKOriginWarnings": "Visa inte APK-ursprung varningar", | ||||
|     "moveNonInstalledAppsToBottom": "Flytta icke-installerade appar till botten av appvyn", | ||||
|     "gitlabPATLabel": "Personligt åtkomsttoken för GitLab", | ||||
|     "gitlabPATLabel": "GitLab Personal Access Token", | ||||
|     "about": "Om", | ||||
|     "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)", | ||||
|     "checkOnStart": "Kolla efter uppdateringar vid start", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Anmärkning", | ||||
|     "selfHostedNote": "Rullgardinsmenyn \"{}\" kan användas för att nå självhostade/anpassade instanser av valfri källa.", | ||||
|     "badDownload": "APK kunde inte analyseras (inkompatibel eller partiell nedladdning)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Dela nya appar med AppVerifier (om tillgängligt)", | ||||
|     "appVerifierInstructionToast": "Dela till AppVerifier och återvänd sedan hit när du är klar.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Ta Bort App?", | ||||
|         "other": "Ta Bort Appar?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Sürüm Algılama Devre Dışı", | ||||
|     "noVersionDetectionExplanation": "Bu seçenek, sürüm algılamanın doğru çalışmadığı uygulamalar için kullanılmalıdır.", | ||||
|     "downloadingX": "{} İndiriliyor", | ||||
|     "downloadX": "İndir {}", | ||||
|     "downloadedX": "İndirildi {}", | ||||
|     "releaseAsset": "Varlık Serbest Bırakma", | ||||
|     "downloadNotifDescription": "Bir uygulamanın indirme sürecinde ilerlemeyi bildiren bir bildirim", | ||||
|     "noAPKFound": "APK bulunamadı", | ||||
|     "noVersionDetection": "Sürüm Algılanamıyor", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Not", | ||||
|     "selfHostedNote": "\"{}\" açılır menüsü, herhangi bir kaynağın kendi kendine barındırılan/özel örneklerine ulaşmak için kullanılabilir.", | ||||
|     "badDownload": "APK ayrıştırılamadı (uyumsuz veya kısmi indirme)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Yeni Uygulamaları AppVerifier ile paylaşın (varsa)", | ||||
|     "appVerifierInstructionToast": "AppVerifier ile paylaşın, hazır olduğunuzda buraya dönün.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Uygulamayı Kaldır?", | ||||
|         "other": "Uygulamaları Kaldır?" | ||||
|   | ||||
| @@ -28,13 +28,13 @@ | ||||
|     "xIsTrackOnly": "{} - тільки відстежування", | ||||
|     "source": "Джерело", | ||||
|     "app": "застосунок", | ||||
|     "appsFromSourceAreTrackOnly": "Застосунки з цього джерела є лише для відстежування.", | ||||
|     "appsFromSourceAreTrackOnly": "Додатки з цього джерела є лише для відстежування.", | ||||
|     "youPickedTrackOnly": "Ви вибрали опцію лише для відстежування.", | ||||
|     "trackOnlyAppDescription": "Застосунок буде відстежуватися для оновлень, але Obtainium не зможе його завантажити або встановити.", | ||||
|     "cancelled": "Скасовано", | ||||
|     "appAlreadyAdded": "Застосунок вже додано", | ||||
|     "alreadyUpToDateQuestion": "Застосунок вже оновлено?", | ||||
|     "addApp": "Додати", | ||||
|     "addApp": "Додати Застосунок", | ||||
|     "appSourceURL": "URL-адреса джерела застосунку", | ||||
|     "error": "Помилка", | ||||
|     "add": "Додати", | ||||
| @@ -44,10 +44,10 @@ | ||||
|     "supportedSources": "Підтримувані джерела", | ||||
|     "trackOnlyInBrackets": "(Тільки для відстеження)", | ||||
|     "searchableInBrackets": "(Можливий пошук)", | ||||
|     "appsString": "Застосунки", | ||||
|     "noApps": "Застосунків немає", | ||||
|     "noAppsForFilter": "Застосунків для фільтрації немає", | ||||
|     "byX": "Від {}", | ||||
|     "appsString": "Додатки", | ||||
|     "noApps": "Додатків немає", | ||||
|     "noAppsForFilter": "Додатків для фільтрації немає", | ||||
|     "byX": "За {}", | ||||
|     "percentProgress": "Прогрес: {}%", | ||||
|     "pleaseWait": "Будь ласка, зачекайте", | ||||
|     "updateAvailable": "Доступно оновлення", | ||||
| @@ -56,35 +56,35 @@ | ||||
|     "selectAll": "Вибрати все", | ||||
|     "deselectX": "Скасувати вибір {}", | ||||
|     "xWillBeRemovedButRemainInstalled": "{} буде видалено з Obtainium, але залишиться встановленим на пристрої.", | ||||
|     "removeSelectedAppsQuestion": "Видалити вибрані застосунки?", | ||||
|     "removeSelectedApps": "Видалити вибрані застосунки", | ||||
|     "removeSelectedAppsQuestion": "Видалити вибрані додатки?", | ||||
|     "removeSelectedApps": "Видалити вибрані додатки", | ||||
|     "updateX": "Оновити {}", | ||||
|     "installX": "Встановити {}", | ||||
|     "markXTrackOnlyAsUpdated": "Позначити {}\n(Тільки відстежування)\nяк оновлено", | ||||
|     "changeX": "Змінити {}", | ||||
|     "installUpdateApps": "Встановити/Оновити застосунки", | ||||
|     "installUpdateSelectedApps": "Встановити/Оновити вибрані застосунки", | ||||
|     "markXSelectedAppsAsUpdated": "Позначити {} вибрані застосунки як оновлені?", | ||||
|     "installUpdateApps": "Встановити/Оновити додатки", | ||||
|     "installUpdateSelectedApps": "Встановити/Оновити вибрані додатки", | ||||
|     "markXSelectedAppsAsUpdated": "Позначити {} вибрані додатки як оновлені?", | ||||
|     "no": "Ні", | ||||
|     "yes": "Так", | ||||
|     "markSelectedAppsUpdated": "Позначити вибрані застосунки як оновлені", | ||||
|     "markSelectedAppsUpdated": "Позначити вибрані додатки як оновлені", | ||||
|     "pinToTop": "Закріпити угорі", | ||||
|     "unpinFromTop": "Відкріпити зверху", | ||||
|     "resetInstallStatusForSelectedAppsQuestion": "Скинути статус встановлення для вибраних застосунків?", | ||||
|     "installStatusOfXWillBeResetExplanation": "Статус встановлення будь-яких вибраних застосунків буде скинутий.\n\nЦе може допомогти, коли версія застосунку, відображена в Obtainium, є неправильною через невдалі оновлення або інші проблеми.", | ||||
|     "resetInstallStatusForSelectedAppsQuestion": "Скинути статус встановлення для вибраних додатків?", | ||||
|     "installStatusOfXWillBeResetExplanation": "Статус встановлення будь-яких вибраних додатків буде скинутий.\n\nЦе може допомогти, коли версія застосунку, відображена в Obtainium, є неправильною через невдалі оновлення або інші проблеми.", | ||||
|     "customLinkMessage": "Ці посилання працюють на пристроях з встановленим Obtainium", | ||||
|     "shareAppConfigLinks": "Поділитися посиланнями на конфігурацію Застосунку як HTML", | ||||
|     "shareSelectedAppURLs": "Поділитися вибраними URL-адресами застосунків", | ||||
|     "shareSelectedAppURLs": "Поділитися вибраними URL-адресами додатків", | ||||
|     "resetInstallStatus": "Скинути статус встановлення", | ||||
|     "more": "Більше", | ||||
|     "removeOutdatedFilter": "Видалити фільтр застарілих застосунків", | ||||
|     "showOutdatedOnly": "Показати лише застарілі застосунки", | ||||
|     "removeOutdatedFilter": "Видалити фільтр застарілих додатків", | ||||
|     "showOutdatedOnly": "Показати лише застарілі додатки", | ||||
|     "filter": "Фільтр", | ||||
|     "filterApps": "Фільтрувати застосунки", | ||||
|     "filterApps": "Фільтрувати додатки", | ||||
|     "appName": "Назва застосунку", | ||||
|     "author": "Автор", | ||||
|     "upToDateApps": "Актуальні застосунки", | ||||
|     "nonInstalledApps": "Невстановлені застосунки", | ||||
|     "upToDateApps": "Актуальні додатки", | ||||
|     "nonInstalledApps": "Невстановлені додатки", | ||||
|     "importExport": "Імпорт/Експорт", | ||||
|     "settings": "Налаштування", | ||||
|     "exportedTo": "Експортовано в {}", | ||||
| @@ -94,14 +94,14 @@ | ||||
|     "obtainiumImport": "Імпорт в Obtainium", | ||||
|     "importFromURLList": "Імпорт зі списку URL-адрес", | ||||
|     "searchQuery": "Пошуковий запит", | ||||
|     "appURLList": "Список URL-адрес застосунків", | ||||
|     "appURLList": "Список URL-адрес додатків", | ||||
|     "line": "Лінія", | ||||
|     "searchX": "Пошук {}", | ||||
|     "noResults": "Результати відсутні", | ||||
|     "importX": "Імпорт {}", | ||||
|     "importedAppsIdDisclaimer": "Імпортовані застосунки можуть неправильно відображатися як \"Не встановлені\".\nДля виправлення цього перевстановіть їх через Obtainium.\nЦе не повинно вплинути на дані застосунків.\n\nПов'язано лише з URL-адресами та імпортом від третіх сторін.", | ||||
|     "importedAppsIdDisclaimer": "Імпортовані додатки можуть неправильно відображатися як \"Не встановлені\".\nДля виправлення цього перевстановіть їх через Obtainium.\nЦе не повинно вплинути на дані додатків.\n\nПов'язано лише з URL-адресами та імпортом від третіх сторін.", | ||||
|     "importErrors": "Помилки імпорту", | ||||
|     "importedXOfYApps": "Імпортовано {} з {} застосунків.", | ||||
|     "importedXOfYApps": "Імпортовано {} з {} додатків.", | ||||
|     "followingURLsHadErrors": "Помилки в наступних URL-адресах:", | ||||
|     "selectURL": "Вибрати URL", | ||||
|     "selectURLs": "Вибрати URL-адреси", | ||||
| @@ -110,19 +110,19 @@ | ||||
|     "dark": "Темна", | ||||
|     "light": "Світла", | ||||
|     "followSystem": "Дотримуватися системи", | ||||
|     "useBlackTheme": "Використовувати чорну тему (Amoled)", | ||||
|     "appSortBy": "Сортувати застосунки за", | ||||
|     "useBlackTheme": "Використовувати чисто чорну темну тему", | ||||
|     "appSortBy": "Сортувати додатки за", | ||||
|     "authorName": "Автор/Назва", | ||||
|     "nameAuthor": "Назва/Автор", | ||||
|     "asAdded": "За додаванням", | ||||
|     "appSortOrder": "Порядок сортування застосунків", | ||||
|     "appSortOrder": "Порядок сортування додатків", | ||||
|     "ascending": "За зростанням", | ||||
|     "descending": "За спаданням", | ||||
|     "bgUpdateCheckInterval": "Інтервал перевірки оновлень у фоновому режимі", | ||||
|     "neverManualOnly": "Ніколи - Тільки вручну", | ||||
|     "appearance": "Вигляд", | ||||
|     "showWebInAppView": "Показати джерело застосунку у вигляді веб-сторінки", | ||||
|     "pinUpdates": "Закріпити оновлення у верхній частині вигляду застосунків", | ||||
|     "pinUpdates": "Закріпити оновлення у верхній частині вигляду додатків", | ||||
|     "updates": "Оновлення", | ||||
|     "sourceSpecific": "Певне джерело", | ||||
|     "appSource": "Джерело застосунку", | ||||
| @@ -139,23 +139,23 @@ | ||||
|     "warning": "Попередження", | ||||
|     "sourceIsXButPackageFromYPrompt": "Джерело застосунку - '{}' але пакет випуску походить з '{}'. Продовжити?", | ||||
|     "updatesAvailable": "Доступні оновлення", | ||||
|     "updatesAvailableNotifDescription": "Повідомляє користувача, що доступні оновлення для одного чи декількох застосунків, які відстежує Obtainium", | ||||
|     "updatesAvailableNotifDescription": "Повідомляє користувача, що доступні оновлення для одного чи декількох додатків, які відстежує Obtainium", | ||||
|     "noNewUpdates": "Немає нових оновлень.", | ||||
|     "xHasAnUpdate": "{} має оновлення.", | ||||
|     "appsUpdated": "Застосунки оновлено", | ||||
|     "appsUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного чи декількох застосунків було застосовано в фоновому режимі", | ||||
|     "appsUpdated": "Додатки оновлено", | ||||
|     "appsUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного чи декількох додатків було застосовано в фоновому режимі", | ||||
|     "xWasUpdatedToY": "{} було оновлено до {}.", | ||||
|     "errorCheckingUpdates": "Помилка перевірки оновлень", | ||||
|     "errorCheckingUpdatesNotifDescription": "Повідомлення, яке з'являється, коли перевірка оновлень в фоновому режимі завершується невдачею", | ||||
|     "appsRemoved": "Застосунки видалено", | ||||
|     "appsRemovedNotifDescription": "Повідомляє користувача, що один чи декілька застосунків були видалені через помилки при завантаженні", | ||||
|     "appsRemoved": "Додатки видалено", | ||||
|     "appsRemovedNotifDescription": "Повідомляє користувача, що один чи декілька додатків були видалені через помилки при завантаженні", | ||||
|     "xWasRemovedDueToErrorY": "{} було видалено через цю помилку: {}", | ||||
|     "completeAppInstallation": "Завершення установки застосунку", | ||||
|     "obtainiumMustBeOpenToInstallApps": "Для встановлення застосунків Obtainium має бути відкритий", | ||||
|     "obtainiumMustBeOpenToInstallApps": "Для встановлення додатків Obtainium має бути відкритий", | ||||
|     "completeAppInstallationNotifDescription": "Прохання користувача повернутися до Obtainium для завершення установки застосунку", | ||||
|     "checkingForUpdates": "Перевірка оновлень", | ||||
|     "checkingForUpdatesNotifDescription": "Тимчасове повідомлення, яке з'являється при перевірці оновлень", | ||||
|     "pleaseAllowInstallPerm": "Будь ласка, дозвольте Obtainium встановлювати застосунки", | ||||
|     "pleaseAllowInstallPerm": "Будь ласка, дозвольте Obtainium встановлювати додатки", | ||||
|     "trackOnly": "Тільки відстеження", | ||||
|     "errorWithHttpStatusCode": "Помилка {} HTTP-коду", | ||||
|     "versionCorrectionDisabled": "Виправлення версії вимкнено (здається, плагін не працює)", | ||||
| @@ -171,7 +171,7 @@ | ||||
|     "appIdOrName": "Ідентифікатор або назва застосунку", | ||||
|     "appId": "Ідентифікатор застосунку", | ||||
|     "appWithIdOrNameNotFound": "Застосунок з таким ідентифікатором або назвою не знайдено", | ||||
|     "reposHaveMultipleApps": "Сховища можуть містити кілька застосунків", | ||||
|     "reposHaveMultipleApps": "Сховища можуть містити кілька додатків", | ||||
|     "fdroidThirdPartyRepo": "F-Droid Стороннє сховище", | ||||
|     "steamMobile": "Мобільний Steam", | ||||
|     "steamChat": "Чат Steam", | ||||
| @@ -181,11 +181,8 @@ | ||||
|     "markUpdated": "Позначити як оновлене", | ||||
|     "additionalOptions": "Додаткові опції", | ||||
|     "disableVersionDetection": "Вимкнути визначення версії", | ||||
|     "noVersionDetectionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно.", | ||||
|     "noVersionDetectionExplanation": "Цю опцію слід використовувати лише для додатків, де визначення версії працює неправильно.", | ||||
|     "downloadingX": "Завантаження {}", | ||||
|     "downloadX": "Завантажити {}", | ||||
|     "downloadedX": "Завантажено {}", | ||||
|     "releaseAsset": "Звільнити актив", | ||||
|     "downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку", | ||||
|     "noAPKFound": "APK не знайдено", | ||||
|     "noVersionDetection": "Визначення версії відключено", | ||||
| @@ -195,19 +192,19 @@ | ||||
|     "noCategory": "Без категорії", | ||||
|     "noCategories": "Немає категорій", | ||||
|     "deleteCategoriesQuestion": "Видалити категорії?", | ||||
|     "categoryDeleteWarning": "Усі застосунки у видалених категоріях будуть переведені у некатегоризовані.", | ||||
|     "categoryDeleteWarning": "Усі додатки у видалених категоріях будуть переведені у некатегоризовані.", | ||||
|     "addCategory": "Додати категорію", | ||||
|     "label": "Мітка", | ||||
|     "language": "Мова", | ||||
|     "copiedToClipboard": "Скопійовано в буфер обміну", | ||||
|     "storagePermissionDenied": "Відмовлено у дозволі на доступ до сховища", | ||||
|     "selectedCategorizeWarning": "Це замінить будь-які існуючі налаштування категорій для вибраних застосунків.", | ||||
|     "selectedCategorizeWarning": "Це замінить будь-які існуючі налаштування категорій для вибраних додатків.", | ||||
|     "filterAPKsByRegEx": "Фільтрувати APK за регулярним виразом", | ||||
|     "removeFromObtainium": "Видалити з Obtainium", | ||||
|     "uninstallFromDevice": "Видалити з пристрою", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Працює лише з застосунками з вимкненим визначенням версії.", | ||||
|     "releaseDateAsVersion": "Використовувати дату випуску як рядок версії", | ||||
|     "releaseDateAsVersionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно, але є дата випуску.", | ||||
|     "releaseDateAsVersionExplanation": "Цю опцію слід використовувати лише для додатків, де визначення версії працює неправильно, але є дата випуску.", | ||||
|     "changes": "Зміни", | ||||
|     "releaseDate": "Дата випуску", | ||||
|     "importFromURLsInFile": "Імпорт з URL-адрес у файлі (наприклад, OPML)", | ||||
| @@ -220,13 +217,13 @@ | ||||
|     "dontShowAgain": "Не показувати це знову", | ||||
|     "dontShowTrackOnlyWarnings": "Не показувати попередження про 'Тільки відстеження'", | ||||
|     "dontShowAPKOriginWarnings": "Не показувати попередження про походження APK", | ||||
|     "moveNonInstalledAppsToBottom": "Перемістити невстановлені застосунки вниз у перегляді застосунків", | ||||
|     "moveNonInstalledAppsToBottom": "Перемістити невстановлені додатки вниз у перегляді додатків", | ||||
|     "gitlabPATLabel": "Особистий токен GitLab (Увімкнення пошуку та краще виявлення APK)", | ||||
|     "about": "Про програму", | ||||
|     "requiresCredentialsInSettings": "{} потребує додаткових облікових даних (у налаштуваннях)", | ||||
|     "checkOnStart": "Перевірити наявність оновлень при запуску", | ||||
|     "tryInferAppIdFromCode": "Спробувати вивести ідентифікатор застосунку з вихідного коду", | ||||
|     "removeOnExternalUninstall": "Автоматично видаляти застосунки, які було видалено зовнішнім чином", | ||||
|     "removeOnExternalUninstall": "Автоматично видаляти додатки, які було видалено зовнішнім чином", | ||||
|     "pickHighestVersionCode": "Автоматично вибрати APK з найвищим кодом версії", | ||||
|     "checkUpdateOnDetailPage": "Перевіряти наявність оновлень при відкритті сторінки деталей застосунку", | ||||
|     "disablePageTransitions": "Вимкнути анімації переходів між сторінками", | ||||
| @@ -235,14 +232,15 @@ | ||||
|     "addInfoBelow": "Додати цю інформацію нижче.", | ||||
|     "addInfoInSettings": "Додати цю інформацію у налаштуваннях.", | ||||
|     "githubSourceNote": "Лімітування швидкості GitHub можна уникнути, використовуючи ключ API.", | ||||
|     "gitlabSourceNote": "Вилучення APK з GitLab може не працювати без ключа API.", | ||||
|     "sortByLastLinkSegment": "Сортувати лише за останнім сегментом посилання", | ||||
|     "filterReleaseNotesByRegEx": "Фільтрувати примітки до релізу за регулярним виразом", | ||||
|     "customLinkFilterRegex": "Фільтр кастомного посилання на APK за регулярним виразом (за замовчуванням '.apk$')", | ||||
|     "appsPossiblyUpdated": "Спроб оновлення застосунків", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного або декількох застосунків можливо були застосовані в фоновому режимі", | ||||
|     "appsPossiblyUpdated": "Оновлення додатків спробовано", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного або декількох додатків можливо були застосовані в фоновому режимі", | ||||
|     "xWasPossiblyUpdatedToY": "{} можливо було оновлено до {}.", | ||||
|     "enableBackgroundUpdates": "Увімкнути оновлення в фоновому режимі", | ||||
|     "backgroundUpdateReqsExplanation": "Оновлення в фоновому режимі може бути неможливим для всіх застосунків.", | ||||
|     "backgroundUpdateReqsExplanation": "Оновлення в фоновому режимі може бути неможливим для всіх додатків.", | ||||
|     "backgroundUpdateLimitsExplanation": "Успіх фонової установки може бути визначений лише після відкриття Obtainium.", | ||||
|     "verifyLatestTag": "Перевірити тег 'latest'", | ||||
|     "intermediateLinkRegex": "Фільтр для 'Проміжного' Посилання для Відвідування", | ||||
| @@ -271,14 +269,14 @@ | ||||
|     "installing": "Встановлення", | ||||
|     "skipUpdateNotifications": "Пропустити сповіщення про оновлення", | ||||
|     "updatesAvailableNotifChannel": "Доступні оновлення", | ||||
|     "appsUpdatedNotifChannel": "Застосунки оновлені", | ||||
|     "appsPossiblyUpdatedNotifChannel": "Спроба оновлення застосунків", | ||||
|     "appsUpdatedNotifChannel": "Додатки оновлені", | ||||
|     "appsPossiblyUpdatedNotifChannel": "Спроба оновлення додатків", | ||||
|     "errorCheckingUpdatesNotifChannel": "Помилка перевірки оновлень", | ||||
|     "appsRemovedNotifChannel": "Застосунки видалені", | ||||
|     "appsRemovedNotifChannel": "Додатки видалені", | ||||
|     "downloadingXNotifChannel": "Завантаження {}", | ||||
|     "completeAppInstallationNotifChannel": "Завершення встановлення застосунку", | ||||
|     "checkingForUpdatesNotifChannel": "Перевірка оновлень", | ||||
|     "onlyCheckInstalledOrTrackOnlyApps": "Перевіряти лише встановлені та застосунки, які відстежуються для оновлень", | ||||
|     "onlyCheckInstalledOrTrackOnlyApps": "Перевіряти лише встановлені та додатки, які відстежуються для оновлень", | ||||
|     "supportFixedAPKURL": "Підтримка фіксованих посилань на APK", | ||||
|     "selectX": "Вибрати {}", | ||||
|     "parallelDownloads": "Дозволити паралельні завантаження", | ||||
| @@ -302,11 +300,9 @@ | ||||
|     "note": "Примітка", | ||||
|     "selfHostedNote": "Випадаючий список \"{}\" може використовуватися для доступу до власних/призначених для самостійного використання екземплярів будь-якого джерела.", | ||||
|     "badDownload": "APK не вдалося розпарсити (несумісний або часткове завантаження)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Діліться новими додатками з AppVerifier (якщо доступно)", | ||||
|     "appVerifierInstructionToast": "Надішліть на AppVerifier, а потім поверніться сюди, коли будете готові.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Видалити застосунок?", | ||||
|         "other": "Видалити застосунки?" | ||||
|         "one": "Видалити Застосунок?", | ||||
|         "other": "Видалити додатки?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Забагато запитів (обмеження швидкості) - повторіть спробу через {} хвилину", | ||||
| @@ -317,44 +313,44 @@ | ||||
|         "other": "Помилка перевірки оновлень у фоновому режимі - спробую знову через {} хвилин" | ||||
|     }, | ||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||
|         "one": "Фонова перевірка оновлень знайшла {} оновлення - сповістити користувача, якщо це необхідно", | ||||
|         "other": "Фонова перевірка оновлень знайшла {} оновлень - сповістити користувача, якщо це необхідно" | ||||
|         "one": "Фонова перевірка оновлень знайшла {} оновлення - сповістить користувача, якщо це необхідно", | ||||
|         "other": "Фонова перевірка оновлень знайшла {} оновлень - сповістить користувача, якщо це необхідно" | ||||
|     }, | ||||
|     "apps": { | ||||
|         "one": "{} застосунок", | ||||
|         "other": "{} застосунки" | ||||
|         "one": "{} Застосунок", | ||||
|         "other": "{} Додатки" | ||||
|     }, | ||||
|     "url": { | ||||
|         "one": "{} URL-адреса", | ||||
|         "one": "{} URL", | ||||
|         "other": "{} URL-адреси" | ||||
|     }, | ||||
|     "minute": { | ||||
|         "one": "{} хвилина", | ||||
|         "other": "{} хвилин" | ||||
|         "one": "{} Хвилина", | ||||
|         "other": "{} Хвилин" | ||||
|     }, | ||||
|     "hour": { | ||||
|         "one": "{} година", | ||||
|         "other": "{} годин" | ||||
|         "one": "{} Година", | ||||
|         "other": "{} Годин" | ||||
|     }, | ||||
|     "day": { | ||||
|         "one": "{} день", | ||||
|         "other": "{} днів" | ||||
|         "one": "{} День", | ||||
|         "other": "{} Днів" | ||||
|     }, | ||||
|     "clearedNLogsBeforeXAfterY": { | ||||
|         "one": "Очищено {n} журнал (до = {before}, після = {after})", | ||||
|         "other": "Очищено {n} журналів (до = {before}, після = {after})" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesAvailable": { | ||||
|         "one": "{} та ще 1 застосунок мають оновлення.", | ||||
|         "other": "{} та ще {} застосунки мають оновлення." | ||||
|         "one": "{} і 1 інше Застосунок мають оновлення.", | ||||
|         "other": "{} і {} інших додатки мають оновлення." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} та ще 1 застосунок було оновлено.", | ||||
|         "other": "{} та ще {} застосунків було оновлено." | ||||
|         "one": "{} і 1 інше Застосунок було оновлено.", | ||||
|         "other": "{} і {} інших додатків було оновлено." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} та ще 1 застосунок можливо було оновлено.", | ||||
|         "other": "{} та ще {} застосунків можливо було оновлено." | ||||
|         "one": "{} і 1 інше Застосунок можливо було оновлено.", | ||||
|         "other": "{} і {} інших додатків можливо було оновлено." | ||||
|     }, | ||||
|     "apk": { | ||||
|         "one": "{} APK", | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|     "unexpectedError": "Lỗi không mong đợi", | ||||
|     "ok": "OK", | ||||
|     "and": "và", | ||||
|     "githubPATLabel": "Token truy cập cá nhân GitHub (Cải thiện tốc độ giới hạn)", | ||||
|     "githubPATLabel": "GitHub Token (Tăng tốc độ, giới hạn)", | ||||
|     "includePrereleases": "Bao gồm các bản phát hành trước", | ||||
|     "fallbackToOlderReleases": "Dự phòng về bản phát hành cũ hơn", | ||||
|     "filterReleaseTitlesByRegEx": "Lọc tiêu đề bản phát hành theo biểu thức chính quy", | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "Tắt tính năng phát hiện phiên bản", | ||||
|     "noVersionDetectionExplanation": "Chỉ nên sử dụng tùy chọn này cho Ứng dụng mà tính năng phát hiện phiên bản không hoạt động chính xác.", | ||||
|     "downloadingX": "Đang tải xuống {}", | ||||
|     "downloadX": "Download {}", | ||||
|     "downloadedX": "Downloaded {}", | ||||
|     "releaseAsset": "Release Asset", | ||||
|     "downloadNotifDescription": "Thông báo cho người dùng về tiến trình tải xuống Ứng dụng", | ||||
|     "noAPKFound": "Không tìm thấy APK", | ||||
|     "noVersionDetection": "Không phát hiện phiên bản", | ||||
| @@ -221,7 +218,7 @@ | ||||
|     "dontShowTrackOnlyWarnings": "Không hiển thị cảnh báo 'Chỉ theo dõi'", | ||||
|     "dontShowAPKOriginWarnings": "Không hiển thị cảnh báo nguồn gốc APK", | ||||
|     "moveNonInstalledAppsToBottom": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách", | ||||
|     "gitlabPATLabel": "Token truy cập cá nhân GitLab", | ||||
|     "gitlabPATLabel": "GitLab Token", | ||||
|     "about": "Giới thiệu", | ||||
|     "requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Thiết đặt)", | ||||
|     "checkOnStart": "Kiểm tra các bản cập nhật khi khởi động", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "Ghi chú", | ||||
|     "selfHostedNote": "Trình đơn thả xuống \"{}\" có thể được dùng để tiếp cận các phiên bản tự lưu trữ/tùy chỉnh của bất kỳ nguồn nào.", | ||||
|     "badDownload": "Không thể phân tích cú pháp APK (tải xuống một phần hoặc không tương thích)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "Chia sẻ ứng dụng mới với AppVerifier (nếu có)", | ||||
|     "appVerifierInstructionToast": "Chia sẻ lên AppVerifier, sau đó quay lại đây khi sẵn sàng.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Gỡ ứng dụng?", | ||||
|         "other": "Gỡ ứng dụng?" | ||||
|   | ||||
| @@ -183,9 +183,6 @@ | ||||
|     "disableVersionDetection": "禁用版本检测", | ||||
|     "noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。", | ||||
|     "downloadingX": "正在下载“{}”", | ||||
|     "downloadX": "下载 {}", | ||||
|     "downloadedX": "下载 {}", | ||||
|     "releaseAsset": "释放资产", | ||||
|     "downloadNotifDescription": "提示应用的下载进度", | ||||
|     "noAPKFound": "未找到 APK 文件", | ||||
|     "noVersionDetection": "禁用版本检测", | ||||
| @@ -302,8 +299,6 @@ | ||||
|     "note": "备注", | ||||
|     "selfHostedNote": "可以通过“{}”下拉菜单来指向任意来源的自托管/自定义实例。", | ||||
|     "badDownload": "无法解析 APK 文件(不兼容或文件不完整)", | ||||
|     "beforeNewInstallsShareToAppVerifier": "与 AppVerifier 共享新应用程序(如有)", | ||||
|     "appVerifierInstructionToast": "分享到 AppVerifier,准备就绪后返回此处。", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "是否删除应用?", | ||||
|         "other": "是否删除应用?" | ||||
|   | ||||
| @@ -45,7 +45,7 @@ class APKCombo extends AppSource { | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var html = parse(res.body); | ||||
|     var html = parse(res.data); | ||||
|     return html | ||||
|         .querySelectorAll('#variants-tab > div > ul > li') | ||||
|         .map((e) { | ||||
| @@ -96,7 +96,7 @@ class APKCombo extends AppSource { | ||||
|     if (preres.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(preres); | ||||
|     } | ||||
|     var res = parse(preres.body); | ||||
|     var res = parse(preres.data); | ||||
|     String? version = res.querySelector('div.version')?.text.trim(); | ||||
|     if (version == null) { | ||||
|       throw NoVersionError(); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| @@ -62,7 +62,7 @@ class APKMirror extends AppSource { | ||||
|             : null; | ||||
|     Response res = await sourceRequest('$standardUrl/feed', additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var items = parse(res.body).querySelectorAll('item'); | ||||
|       var items = parse(res.data).querySelectorAll('item'); | ||||
|       dynamic targetRelease; | ||||
|       for (int i = 0; i < items.length; i++) { | ||||
|         if (!fallbackToOlderReleases && i > 0) break; | ||||
|   | ||||
| @@ -61,8 +61,8 @@ class APKPure extends AppSource { | ||||
|     var res = await sourceRequest('$standardUrl/download', additionalSettings); | ||||
|     var resChangelog = await sourceRequest(standardUrl, additionalSettings); | ||||
|     if (res.statusCode == 200 && resChangelog.statusCode == 200) { | ||||
|       var html = parse(res.body); | ||||
|       var htmlChangelog = parse(resChangelog.body); | ||||
|       var html = parse(res.data); | ||||
|       var htmlChangelog = parse(resChangelog.data); | ||||
|       String? version = html.querySelector('span.info-sdk span')?.text.trim(); | ||||
|       if (version == null) { | ||||
|         throw NoVersionError(); | ||||
|   | ||||
| @@ -38,10 +38,10 @@ class Aptoide extends AppSource { | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body); | ||||
|     var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.data); | ||||
|     String? id; | ||||
|     if (idMatch != null) { | ||||
|       id = res.body.substring(idMatch.start + 12, idMatch.end); | ||||
|       id = res.data.substring(idMatch.start + 12, idMatch.end); | ||||
|     } else { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
| @@ -50,7 +50,7 @@ class Aptoide extends AppSource { | ||||
|     if (res2.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     return jsonDecode(res2.body)?['nodes']?['meta']?['data']; | ||||
|     return jsonDecode(res2.data)?['nodes']?['meta']?['data']; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/app_sources/gitlab.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| @@ -82,7 +82,7 @@ class FDroid extends AppSource { | ||||
|         var res = await sourceRequest( | ||||
|             'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml', | ||||
|             additionalSettings); | ||||
|         var lines = res.body.split('\n'); | ||||
|         var lines = res.data.split('\n'); | ||||
|         var authorLines = lines.where((l) => l.startsWith('AuthorName: ')); | ||||
|         if (authorLines.isNotEmpty) { | ||||
|           details.names.author = | ||||
| @@ -112,7 +112,7 @@ class FDroid extends AppSource { | ||||
|             details.changeLog = (await sourceRequest( | ||||
|                     details.changeLog!.replaceFirst('/blob/', '/raw/'), | ||||
|                     additionalSettings)) | ||||
|                 .body; | ||||
|                 .data; | ||||
|           } | ||||
|         } | ||||
|       } catch (e) { | ||||
| @@ -132,7 +132,7 @@ class FDroid extends AppSource { | ||||
|         'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {}); | ||||
|     if (res.statusCode == 200) { | ||||
|       Map<String, List<String>> urlsWithDescriptions = {}; | ||||
|       parse(res.body).querySelectorAll('.package-header').forEach((e) { | ||||
|       parse(res.data).querySelectorAll('.package-header').forEach((e) { | ||||
|         String? url = e.attributes['href']; | ||||
|         if (url != null) { | ||||
|           try { | ||||
| @@ -172,7 +172,7 @@ class FDroid extends AppSource { | ||||
|             ? additionalSettings['apkFilterRegEx'] | ||||
|             : null; | ||||
|     if (res.statusCode == 200) { | ||||
|       var response = jsonDecode(res.body); | ||||
|       var response = jsonDecode(res.data); | ||||
|       List<dynamic> releases = response['packages'] ?? []; | ||||
|       if (apkFilterRegEx != null) { | ||||
|         releases = releases.where((rel) { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| @@ -61,9 +61,10 @@ class FDroidRepo extends AppSource { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     url = removeQueryParamsFromUrl(standardizeUrl(url)); | ||||
|     var res = await sourceRequestWithURLVariants(url, {}); | ||||
|     var ress = await sourceRequestWithURLVariants(url, {}); | ||||
|     var res = ress.value; | ||||
|     if (res.statusCode == 200) { | ||||
|       var body = parse(res.body); | ||||
|       var body = parse(res.data); | ||||
|       Map<String, List<String>> results = {}; | ||||
|       body.querySelectorAll('application').toList().forEach((app) { | ||||
|         String appId = app.attributes['id']!; | ||||
| @@ -74,7 +75,7 @@ class FDroidRepo extends AppSource { | ||||
|             appName.contains(query) || | ||||
|             appDesc.contains(query)) { | ||||
|           results[ | ||||
|               '${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [ | ||||
|               '${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [ | ||||
|             appName, | ||||
|             appDesc | ||||
|           ]; | ||||
| @@ -107,24 +108,24 @@ class FDroidRepo extends AppSource { | ||||
|     return app; | ||||
|   } | ||||
|  | ||||
|   Future<Response> sourceRequestWithURLVariants( | ||||
|   Future<MapEntry<String, Response>> sourceRequestWithURLVariants( | ||||
|     String url, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     var res = await sourceRequest( | ||||
|         '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}', | ||||
|         additionalSettings); | ||||
|     var finalUrl = '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}'; | ||||
|     var res = await sourceRequest(finalUrl, additionalSettings); | ||||
|     if (res.statusCode != 200) { | ||||
|       var base = url.endsWith('/index.xml') | ||||
|           ? url.split('/').reversed.toList().sublist(1).reversed.join('/') | ||||
|           : url; | ||||
|       res = await sourceRequest('$base/repo/index.xml', additionalSettings); | ||||
|       finalUrl = '$base/repo/index.xml'; | ||||
|       res = await sourceRequest(finalUrl, additionalSettings); | ||||
|       if (res.statusCode != 200) { | ||||
|         res = await sourceRequest( | ||||
|             '$base/fdroid/repo/index.xml', additionalSettings); | ||||
|         finalUrl = '$base/fdroid/repo/index.xml'; | ||||
|         res = await sourceRequest(finalUrl, additionalSettings); | ||||
|       } | ||||
|     } | ||||
|     return res; | ||||
|     return MapEntry(finalUrl, res); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -142,10 +143,11 @@ class FDroidRepo extends AppSource { | ||||
|     if (appIdOrName == null) { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     var res = | ||||
|     var ress = | ||||
|         await sourceRequestWithURLVariants(standardUrl, additionalSettings); | ||||
|     var res = ress.value; | ||||
|     if (res.statusCode == 200) { | ||||
|       var body = parse(res.body); | ||||
|       var body = parse(res.data); | ||||
|       var foundApps = body.querySelectorAll('application').where((element) { | ||||
|         return element.attributes['id'] == appIdOrName; | ||||
|       }).toList(); | ||||
| @@ -193,7 +195,7 @@ class FDroidRepo extends AppSource { | ||||
|       } | ||||
|       List<String> apkUrls = latestVersionReleases | ||||
|           .map((e) => | ||||
|               '${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}') | ||||
|               '${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}') | ||||
|           .toList(); | ||||
|       return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls), | ||||
|           AppNames(authorName, appName), | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'package:dio/dio.dart'; | ||||
| 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'; | ||||
| @@ -112,12 +112,12 @@ class GitHub extends AppSource { | ||||
|     ]; | ||||
|     for (var path in possibleBuildGradleLocations) { | ||||
|       try { | ||||
|         var res = await sourceRequest( | ||||
|             '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path', | ||||
|             additionalSettings); | ||||
|         var finalUrl = | ||||
|             '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path'; | ||||
|         var res = await sourceRequest(finalUrl, additionalSettings); | ||||
|         if (res.statusCode == 200) { | ||||
|           try { | ||||
|             var body = jsonDecode(res.body); | ||||
|             var body = jsonDecode(res.data); | ||||
|             var trimmedLines = utf8 | ||||
|                 .decode(base64 | ||||
|                     .decode(body['content'].toString().split('\n').join(''))) | ||||
| @@ -143,7 +143,7 @@ class GitHub extends AppSource { | ||||
|             } | ||||
|           } catch (err) { | ||||
|             LogsProvider().add( | ||||
|                 'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}'); | ||||
|                 'Error parsing build.gradle from $finalUrl: ${err.toString()}'); | ||||
|           } | ||||
|         } | ||||
|       } catch (err) { | ||||
| @@ -256,11 +256,11 @@ class GitHub extends AppSource { | ||||
|         } | ||||
|         throw getObtainiumHttpError(res); | ||||
|       } | ||||
|       latestRelease = jsonDecode(res.body); | ||||
|       latestRelease = jsonDecode(res.data); | ||||
|     } | ||||
|     Response res = await sourceRequest(requestUrl, additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var releases = jsonDecode(res.body) as List<dynamic>; | ||||
|       var releases = jsonDecode(res.data) as List<dynamic>; | ||||
|       if (latestRelease != null) { | ||||
|         var latestTag = latestRelease['tag_name'] ?? latestRelease['name']; | ||||
|         if (releases | ||||
| @@ -271,14 +271,17 @@ class GitHub extends AppSource { | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       List<MapEntry<String, String>> getReleaseAssetUrls(dynamic release) => | ||||
|           (release['assets'] as List<dynamic>?)?.map((e) { | ||||
|       List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => | ||||
|           (release['assets'] as List<dynamic>?) | ||||
|               ?.map((e) { | ||||
|                 return (e['name'] != null) && | ||||
|                         ((e['url'] ?? e['browser_download_url']) != null) | ||||
|                     ? MapEntry(e['name'] as String, | ||||
|                         (e['url'] ?? e['browser_download_url']) as String) | ||||
|                     : const MapEntry('', ''); | ||||
|           }).toList() ?? | ||||
|               }) | ||||
|               .where((element) => element.key.toLowerCase().endsWith('.apk')) | ||||
|               .toList() ?? | ||||
|           []; | ||||
|  | ||||
|       DateTime? getPublishDateFromRelease(dynamic rel) => | ||||
| @@ -380,11 +383,7 @@ class GitHub extends AppSource { | ||||
|                 .hasMatch(((releases[i]['body'] as String?) ?? '').trim())) { | ||||
|           continue; | ||||
|         } | ||||
|         var allAssetUrls = getReleaseAssetUrls(releases[i]); | ||||
|         List<MapEntry<String, String>> apkUrls = allAssetUrls | ||||
|             .where((element) => element.key.toLowerCase().endsWith('.apk')) | ||||
|             .toList(); | ||||
|  | ||||
|         var apkUrls = getReleaseAPKUrls(releases[i]); | ||||
|         apkUrls = filterApks(apkUrls, additionalSettings['apkFilterRegEx'], | ||||
|             additionalSettings['invertAPKFilter']); | ||||
|         if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) { | ||||
| @@ -392,25 +391,12 @@ class GitHub extends AppSource { | ||||
|         } | ||||
|         targetRelease = releases[i]; | ||||
|         targetRelease['apkUrls'] = apkUrls; | ||||
|         targetRelease['version'] = | ||||
|             targetRelease['tag_name'] ?? targetRelease['name']; | ||||
|         if (targetRelease['tarball_url'] != null) { | ||||
|           allAssetUrls.add(MapEntry( | ||||
|               (targetRelease['version'] ?? 'source') + '.tar.gz', | ||||
|               targetRelease['tarball_url'])); | ||||
|         } | ||||
|         if (targetRelease['zipball_url'] != null) { | ||||
|           allAssetUrls.add(MapEntry( | ||||
|               (targetRelease['version'] ?? 'source') + '.zip', | ||||
|               targetRelease['zipball_url'])); | ||||
|         } | ||||
|         targetRelease['allAssetUrls'] = allAssetUrls; | ||||
|         break; | ||||
|       } | ||||
|       if (targetRelease == null) { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|       String? version = targetRelease['version']; | ||||
|       String? version = targetRelease['tag_name'] ?? targetRelease['name']; | ||||
|       DateTime? releaseDate = getReleaseDateFromRelease( | ||||
|           targetRelease, useLatestAssetDateAsReleaseDate); | ||||
|       if (version == null) { | ||||
| @@ -422,9 +408,7 @@ class GitHub extends AppSource { | ||||
|           targetRelease['apkUrls'] as List<MapEntry<String, String>>, | ||||
|           getAppNames(standardUrl), | ||||
|           releaseDate: releaseDate, | ||||
|           changeLog: changeLog.isEmpty ? null : changeLog, | ||||
|           allAssetUrls: | ||||
|               targetRelease['allAssetUrls'] as List<MapEntry<String, String>>); | ||||
|           changeLog: changeLog.isEmpty ? null : changeLog); | ||||
|     } else { | ||||
|       if (onHttpErrorCode != null) { | ||||
|         onHttpErrorCode(res); | ||||
| @@ -482,7 +466,7 @@ class GitHub extends AppSource { | ||||
|           ? int.parse(querySettings['minStarCount']) | ||||
|           : 0; | ||||
|       Map<String, List<String>> urlsWithDescriptions = {}; | ||||
|       for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) { | ||||
|       for (var e in (jsonDecode(res.data)[rootProp] as List<dynamic>)) { | ||||
|         if ((e['stargazers_count'] ?? e['stars_count'] ?? 0) >= minStarCount) { | ||||
|           urlsWithDescriptions.addAll({ | ||||
|             e['html_url'] as String: [ | ||||
| @@ -516,11 +500,13 @@ class GitHub extends AppSource { | ||||
|   } | ||||
|  | ||||
|   rateLimitErrorCheck(Response res) { | ||||
|     if (res.headers['x-ratelimit-remaining'] == '0') { | ||||
|     String? rateLimitHeader; | ||||
|     if (res.headers.map['x-ratelimit-remaining']?.isNotEmpty == true) { | ||||
|       rateLimitHeader = res.headers.map['x-ratelimit-remaining']![0]; | ||||
|     } | ||||
|     if (rateLimitHeader == '0') { | ||||
|       throw RateLimitError( | ||||
|           (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / | ||||
|                   60000000) | ||||
|               .round()); | ||||
|           (int.parse(rateLimitHeader ?? '1800000000') / 60000000).round()); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.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'; | ||||
| @@ -81,7 +81,7 @@ class GitLab extends AppSource { | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var json = jsonDecode(res.body) as List<dynamic>; | ||||
|     var json = jsonDecode(res.data) as List<dynamic>; | ||||
|     Map<String, List<String>> results = {}; | ||||
|     for (var element in json) { | ||||
|       results['https://${hosts[0]}/${element['path_with_namespace']}'] = [ | ||||
| @@ -121,11 +121,9 @@ class GitLab extends AppSource { | ||||
|     String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {}); | ||||
|     String optionalAuth = (PAT != null) ? 'private_token=$PAT' : ''; | ||||
|  | ||||
|     bool trackOnly = additionalSettings['trackOnly'] == true; | ||||
|  | ||||
|     // Request data from REST API | ||||
|     Response res = await sourceRequest( | ||||
|         'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth', | ||||
|         'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/releases?$optionalAuth', | ||||
|         additionalSettings); | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
| @@ -133,7 +131,7 @@ class GitLab extends AppSource { | ||||
|  | ||||
|     // Extract .apk details from received data | ||||
|     Iterable<APKDetails> apkDetailsList = []; | ||||
|     var json = jsonDecode(res.body) as List<dynamic>; | ||||
|     var json = jsonDecode(res.data) as List<dynamic>; | ||||
|     apkDetailsList = json.map((e) { | ||||
|       var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? []) | ||||
|           .map((e) { | ||||
| @@ -165,31 +163,20 @@ class GitLab extends AppSource { | ||||
|     if (apkDetailsList.isEmpty) { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     var finalResult = apkDetailsList.first; | ||||
|  | ||||
|     // Fallback procedure | ||||
|     bool fallbackToOlderReleases = | ||||
|         additionalSettings['fallbackToOlderReleases'] == true; | ||||
|     if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) { | ||||
|     if (fallbackToOlderReleases) { | ||||
|       if (additionalSettings['trackOnly'] != true) { | ||||
|         apkDetailsList = | ||||
|             apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList(); | ||||
|       finalResult = apkDetailsList.first; | ||||
|       } | ||||
|       if (apkDetailsList.isEmpty) { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (finalResult.apkUrls.isEmpty && !trackOnly) { | ||||
|       throw NoAPKError(); | ||||
|     } | ||||
|  | ||||
|     finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) { | ||||
|       if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+\$') | ||||
|           .hasMatch(apkUrl.value)) { | ||||
|         return MapEntry( | ||||
|             apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/')); | ||||
|       } else { | ||||
|         return apkUrl; | ||||
|       } | ||||
|     }).toList(); | ||||
|  | ||||
|     return finalResult; | ||||
|     return apkDetailsList.first; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| @@ -213,11 +213,11 @@ class HTML extends AppSource { | ||||
|   // Given an HTTP response, grab some links according to the common additional settings | ||||
|   // (those that apply to intermediate and final steps) | ||||
|   Future<List<MapEntry<String, String>>> grabLinksCommon( | ||||
|       Response res, Map<String, dynamic> additionalSettings) async { | ||||
|       Response res, Uri url, Map<String, dynamic> additionalSettings) async { | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var html = parse(res.body); | ||||
|     var html = parse(res.data); | ||||
|     List<MapEntry<String, String>> allLinks = html | ||||
|         .querySelectorAll('a') | ||||
|         .map((element) => MapEntry( | ||||
| @@ -226,13 +226,12 @@ class HTML extends AppSource { | ||||
|                 ? element.text | ||||
|                 : (element.attributes['href'] ?? '').split('/').last)) | ||||
|         .where((element) => element.key.isNotEmpty) | ||||
|         .map((e) => | ||||
|             MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value)) | ||||
|         .map((e) => MapEntry(ensureAbsoluteUrl(e.key, url), e.value)) | ||||
|         .toList(); | ||||
|     if (allLinks.isEmpty) { | ||||
|       allLinks = RegExp( | ||||
|               r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?') | ||||
|           .allMatches(res.body) | ||||
|           .allMatches(res.data) | ||||
|           .map((match) => | ||||
|               MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? '')) | ||||
|           .toList(); | ||||
| @@ -285,6 +284,7 @@ class HTML extends AppSource { | ||||
|     for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) { | ||||
|       var intLinks = await grabLinksCommon( | ||||
|           await sourceRequest(currentUrl, additionalSettings), | ||||
|           Uri.parse(currentUrl), | ||||
|           additionalSettings['intermediateLink'][i]); | ||||
|       if (intLinks.isEmpty) { | ||||
|         throw NoReleasesError(); | ||||
| @@ -298,8 +298,9 @@ class HTML extends AppSource { | ||||
|     if (additionalSettings['directAPKLink'] != true) { | ||||
|       Response res = await sourceRequest(currentUrl, additionalSettings); | ||||
|       versionExtractionWholePageString = | ||||
|           res.body.split('\r\n').join('\n').split('\n').join('\\n'); | ||||
|       links = await grabLinksCommon(res, additionalSettings); | ||||
|           res.data.split('\r\n').join('\n').split('\n').join('\\n'); | ||||
|       links = | ||||
|           await grabLinksCommon(res, Uri.parse(currentUrl), additionalSettings); | ||||
|       links = filterApks(links, additionalSettings['apkFilterRegEx'], | ||||
|           additionalSettings['invertAPKFilter']); | ||||
|       if (links.isEmpty) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -57,8 +57,8 @@ class HuaweiAppGallery extends AppSource { | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||
|     String dlUrl = getDlUrl(standardUrl); | ||||
|     Response res = await requestAppdlRedirect(dlUrl, additionalSettings); | ||||
|     return res.headers['location'] != null | ||||
|         ? appIdFromRedirectDlUrl(res.headers['location']!) | ||||
|     return res.headers.map['location']?.isNotEmpty == true | ||||
|         ? appIdFromRedirectDlUrl(res.headers.map['location']![0]) | ||||
|         : null; | ||||
|   } | ||||
|  | ||||
| @@ -72,9 +72,12 @@ class HuaweiAppGallery extends AppSource { | ||||
|     if (res.headers['location'] == null) { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     String appId = appIdFromRedirectDlUrl(res.headers['location']!); | ||||
|     var relDateStr = | ||||
|         res.headers['location']?.split('?')[0].split('.').reversed.toList()[1]; | ||||
|     String appId = appIdFromRedirectDlUrl(res.headers.map['location']![0]); | ||||
|     var relDateStr = res.headers.map['location']?[0] | ||||
|         .split('?')[0] | ||||
|         .split('.') | ||||
|         .reversed | ||||
|         .toList()[1]; | ||||
|     var relDateStrAdj = relDateStr?.split(''); | ||||
|     var tempLen = relDateStrAdj?.length ?? 0; | ||||
|     var i = 2; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -33,7 +33,7 @@ class Jenkins extends AppSource { | ||||
|     Response res = await sourceRequest( | ||||
|         '$standardUrl/lastSuccessfulBuild/api/json', additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var json = jsonDecode(res.body); | ||||
|       var json = jsonDecode(res.data); | ||||
|       var releaseDate = json['timestamp'] == null | ||||
|           ? null | ||||
|           : DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:dio/dio.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/source_provider.dart'; | ||||
| @@ -33,7 +33,7 @@ class Mullvad extends AppSource { | ||||
|     Response res = await sourceRequest( | ||||
|         '$standardUrl/en/download/android', additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var versions = parse(res.body) | ||||
|       var versions = parse(res.data) | ||||
|           .querySelectorAll('p') | ||||
|           .map((e) => e.innerHtml) | ||||
|           .where((p) => p.contains('Latest version: ')) | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -83,7 +83,7 @@ class NeutronCode extends AppSource { | ||||
|   ) async { | ||||
|     Response res = await sourceRequest(standardUrl, additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var http = parse(res.body); | ||||
|       var http = parse(res.data); | ||||
|       var name = http.querySelector('.pd-title')?.innerHtml; | ||||
|       var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml; | ||||
|       if (filename == null) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'dart:convert'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -21,7 +21,7 @@ class Signal extends AppSource { | ||||
|     Response res = await sourceRequest( | ||||
|         'https://updates.${hosts[0]}/android/latest.json', additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var json = jsonDecode(res.body); | ||||
|       var json = jsonDecode(res.data); | ||||
|       String? apkUrl = json['url']; | ||||
|       List<String> apkUrls = apkUrl == null ? [] : [apkUrl]; | ||||
|       String? version = json['versionName']; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -49,7 +49,7 @@ class SourceForge extends AppSource { | ||||
|         '${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/', | ||||
|         additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var parsedHtml = parse(res.body); | ||||
|       var parsedHtml = parse(res.data); | ||||
|       var allDownloadLinks = parsedHtml | ||||
|           .querySelectorAll('guid') | ||||
|           .map((e) => e.innerHtml) | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/html.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| @@ -55,7 +55,7 @@ class SourceHut extends AppSource { | ||||
|     Response res = | ||||
|         await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var parsedHtml = parse(res.body); | ||||
|       var parsedHtml = parse(res.data); | ||||
|       List<APKDetails> apkDetailsList = []; | ||||
|       int ind = 0; | ||||
|  | ||||
| @@ -85,7 +85,7 @@ class SourceHut extends AppSource { | ||||
|         var res2 = await sourceRequest(releasePage, additionalSettings); | ||||
|         List<MapEntry<String, String>> apkUrls = []; | ||||
|         if (res2.statusCode == 200) { | ||||
|           apkUrls = getApkUrlsFromUrls(parse(res2.body) | ||||
|           apkUrls = getApkUrlsFromUrls(parse(res2.data) | ||||
|               .querySelectorAll('a') | ||||
|               .map((e) => e.attributes['href'] ?? '') | ||||
|               .where((e) => e.toLowerCase().endsWith('.apk')) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| @@ -38,7 +38,7 @@ class SteamMobile extends AppSource { | ||||
|       } | ||||
|       String apkInURLRegexPattern = | ||||
|           '/$apkNamePrefix-([0-9]+\\.)*[0-9]+\\.apk\$'; | ||||
|       var links = parse(res.body) | ||||
|       var links = parse(res.data) | ||||
|           .querySelectorAll('a') | ||||
|           .map((e) => e.attributes['href'] ?? '') | ||||
|           .where((e) => RegExp('https://.*$apkInURLRegexPattern').hasMatch(e)) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| 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'; | ||||
|  | ||||
| @@ -23,7 +23,7 @@ class TelegramApp extends AppSource { | ||||
|     Response res = | ||||
|         await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var http = parse(res.body); | ||||
|       var http = parse(res.data); | ||||
|       var messages = | ||||
|           http.querySelectorAll('.tgme_widget_message_text.js-message_text'); | ||||
|       var version = messages.isNotEmpty | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class Uptodown extends AppSource { | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var html = parse(res.body); | ||||
|     var html = parse(res.data); | ||||
|     String? version = html.querySelector('div.version')?.innerHtml; | ||||
|     String? apkUrl = | ||||
|         '${standardUrl.split('/').reversed.toList().sublist(1).reversed.join('/')}/post-download'; | ||||
| @@ -94,7 +94,7 @@ class Uptodown extends AppSource { | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var html = parse(res.body); | ||||
|     var html = parse(res.data); | ||||
|     var finalUrlKey = | ||||
|         html.querySelector('.post-download')?.attributes['data-url']; | ||||
|     if (finalUrlKey == null) { | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| 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/main.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class VLC extends AppSource { | ||||
| @@ -29,7 +30,7 @@ class VLC extends AppSource { | ||||
|       String standardUrl, Map<String, dynamic> additionalSettings) async { | ||||
|     Response res = await sourceRequest(dwUrlBase, additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var dwLinks = parse(res.body) | ||||
|       var dwLinks = parse(res.data) | ||||
|           .querySelectorAll('a') | ||||
|           .where((element) => element.attributes['href'] != 'last/') | ||||
|           .map((e) => e.attributes['href']?.split('/')[0]) | ||||
| @@ -49,11 +50,11 @@ 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 dio.get('https://www.videolan.org/vlc/download-android.html'); | ||||
|     if (res.statusCode == 200) { | ||||
|       var dwUrlBase = 'get.videolan.org/vlc-android'; | ||||
|       var dwLinks = parse(res.body) | ||||
|       var dwLinks = parse(res.data) | ||||
|           .querySelectorAll('a') | ||||
|           .where((element) => | ||||
|               element.attributes['href']?.contains(dwUrlBase) ?? false) | ||||
| @@ -84,14 +85,14 @@ class VLC extends AppSource { | ||||
|     Response res = await sourceRequest(apkUrl, additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       String? apkUrl = | ||||
|           parse(res.body).querySelector('#alt_link')?.attributes['href']; | ||||
|           parse(res.data).querySelector('#alt_link')?.attributes['href']; | ||||
|       if (apkUrl == null) { | ||||
|         throw NoAPKError(); | ||||
|       } | ||||
|       return apkUrl; | ||||
|     } else if (res.statusCode == 500 && | ||||
|         res.body.toLowerCase().indexOf('mirror') > 0) { | ||||
|       var html = parse(res.body); | ||||
|         res.data.toLowerCase().indexOf('mirror') > 0) { | ||||
|       var html = parse(res.data); | ||||
|       var err = ''; | ||||
|       html.body?.nodes.forEach((element) { | ||||
|         if (element.text != null) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -20,7 +20,7 @@ class WhatsApp extends AppSource { | ||||
|     Response res = | ||||
|         await sourceRequest('$standardUrl/android', additionalSettings); | ||||
|     if (res.statusCode == 200) { | ||||
|       var targetLinks = parse(res.body) | ||||
|       var targetLinks = parse(res.data) | ||||
|           .querySelectorAll('a') | ||||
|           .map((e) => e.attributes['href'] ?? '') | ||||
|           .where((e) => e.isNotEmpty) | ||||
|   | ||||
| @@ -245,8 +245,8 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|   void someValueChanged({bool isBuilding = false, bool forceInvalid = false}) { | ||||
|     Map<String, dynamic> returnValues = values; | ||||
|     var valid = true; | ||||
|     for (int r = 0; r < formInputs.length; r++) { | ||||
|       for (int i = 0; i < formInputs[r].length; i++) { | ||||
|     for (int r = 0; r < widget.items.length; r++) { | ||||
|       for (int i = 0; i < widget.items[r].length; i++) { | ||||
|         if (formInputs[r][i] is TextFormField) { | ||||
|           valid = valid && validateTextField(formInputs[r][i] as TextFormField); | ||||
|         } | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:easy_localization/src/easy_localization_controller.dart'; | ||||
| // ignore: implementation_imports | ||||
| import 'package:easy_localization/src/localization.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
|  | ||||
| List<MapEntry<Locale, String>> supportedLocales = const [ | ||||
|   MapEntry(Locale('en'), 'English'), | ||||
| @@ -46,6 +47,9 @@ var fdroid = false; | ||||
|  | ||||
| final globalNavigatorKey = GlobalKey<NavigatorState>(); | ||||
|  | ||||
| final dio = Dio(BaseOptions( | ||||
|     responseType: ResponseType.plain, receiveDataWhenStatusError: true)); | ||||
|  | ||||
| Future<void> loadTranslations() async { | ||||
|   // See easy_localization/issues/210 | ||||
|   await EasyLocalizationController.initEasyLocation(); | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class GitHubStars implements MassAppUrlSource { | ||||
| @@ -15,13 +16,12 @@ class GitHubStars implements MassAppUrlSource { | ||||
|  | ||||
|   Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( | ||||
|       String username, int page) async { | ||||
|     Response res = await get( | ||||
|         Uri.parse( | ||||
|             'https://api.github.com/users/$username/starred?per_page=100&page=$page'), | ||||
|         headers: await GitHub().getRequestHeaders({})); | ||||
|     Response res = await dio.get( | ||||
|         'https://api.github.com/users/$username/starred?per_page=100&page=$page', | ||||
|         options: Options(headers: await GitHub().getRequestHeaders({}))); | ||||
|     if (res.statusCode == 200) { | ||||
|       Map<String, List<String>> urlsWithDescriptions = {}; | ||||
|       for (var e in (jsonDecode(res.body) as List<dynamic>)) { | ||||
|       for (var e in (jsonDecode(res.data) as List<dynamic>)) { | ||||
|         urlsWithDescriptions.addAll({ | ||||
|           e['html_url'] as String: [ | ||||
|             e['full_name'] as String, | ||||
|   | ||||
| @@ -155,8 +155,7 @@ class AddAppPageState extends State<AddAppPage> { | ||||
|           // Only download the APK here if you need to for the package ID | ||||
|           if (isTempId(app) && app.additionalSettings['trackOnly'] != true) { | ||||
|             // ignore: use_build_context_synchronously | ||||
|             var apkUrl = | ||||
|                 await appsProvider.confirmAppFileUrl(app, context, false); | ||||
|             var apkUrl = await appsProvider.confirmApkUrl(app, context); | ||||
|             if (apkUrl == null) { | ||||
|               throw ObtainiumError(tr('cancelled')); | ||||
|             } | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; | ||||
| import 'package:obtainium/components/generated_form_modal.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/pages/apps.dart'; | ||||
| import 'package:obtainium/pages/settings.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| @@ -109,7 +108,6 @@ class _AppPageState extends State<AppPage> { | ||||
|         infoLines = | ||||
|             '$infoLines\n${app?.app.apkUrls.length == 1 ? app?.app.apkUrls[0].key : plural('apk', app?.app.apkUrls.length ?? 0)}'; | ||||
|       } | ||||
|       var changeLogFn = app != null ? getChangeLogFn(context, app.app) : null; | ||||
|       return Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
| @@ -127,26 +125,13 @@ class _AppPageState extends State<AppPage> { | ||||
|                         .textTheme | ||||
|                         .bodyLarge! | ||||
|                         .copyWith(fontWeight: FontWeight.bold)), | ||||
|                 changeLogFn != null || app?.app.releaseDate != null | ||||
|                     ? GestureDetector( | ||||
|                         onTap: changeLogFn, | ||||
|                         child: Text( | ||||
|                 app?.app.releaseDate == null | ||||
|                               ? tr('changes') | ||||
|                               : app!.app.releaseDate.toString(), | ||||
|                     ? const SizedBox.shrink() | ||||
|                     : Text( | ||||
|                         app!.app.releaseDate.toString(), | ||||
|                         textAlign: TextAlign.center, | ||||
|                           style: | ||||
|                               Theme.of(context).textTheme.labelSmall!.copyWith( | ||||
|                                     decoration: changeLogFn != null | ||||
|                                         ? TextDecoration.underline | ||||
|                                         : null, | ||||
|                                     fontStyle: changeLogFn != null | ||||
|                                         ? FontStyle.italic | ||||
|                                         : null, | ||||
|                         style: Theme.of(context).textTheme.labelSmall, | ||||
|                       ), | ||||
|                         ), | ||||
|                       ) | ||||
|                     : const SizedBox.shrink(), | ||||
|                 const SizedBox( | ||||
|                   height: 8, | ||||
|                 ), | ||||
| @@ -158,29 +143,6 @@ class _AppPageState extends State<AppPage> { | ||||
|             textAlign: TextAlign.center, | ||||
|             style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), | ||||
|           ), | ||||
|           if (app?.app.apkUrls.isNotEmpty == true || | ||||
|               app?.app.otherAssetUrls.isNotEmpty == true) | ||||
|             GestureDetector( | ||||
|               onTap: app?.app == null || updating | ||||
|                   ? null | ||||
|                   : () async { | ||||
|                       try { | ||||
|                         await appsProvider | ||||
|                             .downloadAppAssets([app!.app.id], context); | ||||
|                       } catch (e) { | ||||
|                         showError(e, context); | ||||
|                       } | ||||
|                     }, | ||||
|               child: Text( | ||||
|                 tr('downloadX', args: [tr('releaseAsset').toLowerCase()]), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: Theme.of(context).textTheme.labelSmall!.copyWith( | ||||
|                       decoration: | ||||
|                           changeLogFn != null ? TextDecoration.underline : null, | ||||
|                       fontStyle: changeLogFn != null ? FontStyle.italic : null, | ||||
|                     ), | ||||
|               ), | ||||
|             ), | ||||
|           const SizedBox( | ||||
|             height: 48, | ||||
|           ), | ||||
| @@ -399,9 +361,6 @@ class _AppPageState extends State<AppPage> { | ||||
|                 !areDownloadsRunning | ||||
|             ? () async { | ||||
|                 try { | ||||
|                   var successMessage = app?.app.installedVersion == null | ||||
|                       ? tr('installed') | ||||
|                       : tr('appsUpdated'); | ||||
|                   HapticFeedback.heavyImpact(); | ||||
|                   var res = await appsProvider.downloadAndInstallLatestApps( | ||||
|                     app?.app.id != null ? [app!.app.id] : [], | ||||
| @@ -409,7 +368,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                   ); | ||||
|                   if (res.isNotEmpty && !trackOnly) { | ||||
|                     // ignore: use_build_context_synchronously | ||||
|                     showMessage(successMessage, context); | ||||
|                     showMessage(tr('appsUpdated'), context); | ||||
|                   } | ||||
|                   if (res.isNotEmpty && mounted) { | ||||
|                     Navigator.of(context).pop(); | ||||
|   | ||||
| @@ -26,92 +26,6 @@ class AppsPage extends StatefulWidget { | ||||
|   State<AppsPage> createState() => AppsPageState(); | ||||
| } | ||||
|  | ||||
| showChangeLogDialog(BuildContext context, App app, String? changesUrl, | ||||
|     AppSource appSource, String changeLog) { | ||||
|   showDialog( | ||||
|       context: context, | ||||
|       builder: (BuildContext context) { | ||||
|         return GeneratedFormModal( | ||||
|           title: tr('changes'), | ||||
|           items: const [], | ||||
|           message: app.latestVersion, | ||||
|           additionalWidgets: [ | ||||
|             changesUrl != null | ||||
|                 ? GestureDetector( | ||||
|                     child: Text( | ||||
|                       changesUrl, | ||||
|                       style: const TextStyle( | ||||
|                           decoration: TextDecoration.underline, | ||||
|                           fontStyle: FontStyle.italic), | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       launchUrlString(changesUrl, | ||||
|                           mode: LaunchMode.externalApplication); | ||||
|                     }, | ||||
|                   ) | ||||
|                 : const SizedBox.shrink(), | ||||
|             changesUrl != null | ||||
|                 ? const SizedBox( | ||||
|                     height: 16, | ||||
|                   ) | ||||
|                 : const SizedBox.shrink(), | ||||
|             appSource.changeLogIfAnyIsMarkDown | ||||
|                 ? SizedBox( | ||||
|                     width: MediaQuery.of(context).size.width, | ||||
|                     height: MediaQuery.of(context).size.height - 350, | ||||
|                     child: Markdown( | ||||
|                       data: changeLog, | ||||
|                       onTapLink: (text, href, title) { | ||||
|                         if (href != null) { | ||||
|                           launchUrlString( | ||||
|                               href.startsWith('http://') || | ||||
|                                       href.startsWith('https://') | ||||
|                                   ? href | ||||
|                                   : '${Uri.parse(app.url).origin}/$href', | ||||
|                               mode: LaunchMode.externalApplication); | ||||
|                         } | ||||
|                       }, | ||||
|                       extensionSet: md.ExtensionSet( | ||||
|                         md.ExtensionSet.gitHubFlavored.blockSyntaxes, | ||||
|                         [ | ||||
|                           md.EmojiSyntax(), | ||||
|                           ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes | ||||
|                         ], | ||||
|                       ), | ||||
|                     )) | ||||
|                 : Text(changeLog), | ||||
|           ], | ||||
|           singleNullReturnButton: tr('ok'), | ||||
|         ); | ||||
|       }); | ||||
| } | ||||
|  | ||||
| getChangeLogFn(BuildContext context, App app) { | ||||
|   AppSource appSource = | ||||
|       SourceProvider().getSource(app.url, overrideSource: app.overrideSource); | ||||
|   String? changesUrl = appSource.changeLogPageFromStandardUrl(app.url); | ||||
|   String? changeLog = app.changeLog; | ||||
|   if (changeLog?.split('\n').length == 1) { | ||||
|     if (RegExp( | ||||
|             '(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?') | ||||
|         .hasMatch(changeLog!)) { | ||||
|       if (changesUrl == null) { | ||||
|         changesUrl = changeLog; | ||||
|         changeLog = null; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return (changeLog == null && changesUrl == null) | ||||
|       ? null | ||||
|       : () { | ||||
|           if (changeLog != null) { | ||||
|             showChangeLogDialog(context, app, changesUrl, appSource, changeLog); | ||||
|           } else { | ||||
|             launchUrlString(changesUrl!, mode: LaunchMode.externalApplication); | ||||
|           } | ||||
|         }; | ||||
| } | ||||
|  | ||||
| class AppsPageState extends State<AppsPage> { | ||||
|   AppsFilter filter = AppsFilter(); | ||||
|   final AppsFilter neutralFilter = AppsFilter(); | ||||
| @@ -348,6 +262,66 @@ class AppsPageState extends State<AppsPage> { | ||||
|         .where((a) => selectedAppIds.contains(a.id)) | ||||
|         .toSet(); | ||||
|  | ||||
|     showChangeLogDialog( | ||||
|         String? changesUrl, AppSource appSource, String changeLog, int index) { | ||||
|       showDialog( | ||||
|           context: context, | ||||
|           builder: (BuildContext context) { | ||||
|             return GeneratedFormModal( | ||||
|               title: tr('changes'), | ||||
|               items: const [], | ||||
|               message: listedApps[index].app.latestVersion, | ||||
|               additionalWidgets: [ | ||||
|                 changesUrl != null | ||||
|                     ? GestureDetector( | ||||
|                         child: Text( | ||||
|                           changesUrl, | ||||
|                           style: const TextStyle( | ||||
|                               decoration: TextDecoration.underline, | ||||
|                               fontStyle: FontStyle.italic), | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           launchUrlString(changesUrl, | ||||
|                               mode: LaunchMode.externalApplication); | ||||
|                         }, | ||||
|                       ) | ||||
|                     : const SizedBox.shrink(), | ||||
|                 changesUrl != null | ||||
|                     ? const SizedBox( | ||||
|                         height: 16, | ||||
|                       ) | ||||
|                     : const SizedBox.shrink(), | ||||
|                 appSource.changeLogIfAnyIsMarkDown | ||||
|                     ? SizedBox( | ||||
|                         width: MediaQuery.of(context).size.width, | ||||
|                         height: MediaQuery.of(context).size.height - 350, | ||||
|                         child: Markdown( | ||||
|                           data: changeLog, | ||||
|                           onTapLink: (text, href, title) { | ||||
|                             if (href != null) { | ||||
|                               launchUrlString( | ||||
|                                   href.startsWith('http://') || | ||||
|                                           href.startsWith('https://') | ||||
|                                       ? href | ||||
|                                       : '${Uri.parse(listedApps[index].app.url).origin}/$href', | ||||
|                                   mode: LaunchMode.externalApplication); | ||||
|                             } | ||||
|                           }, | ||||
|                           extensionSet: md.ExtensionSet( | ||||
|                             md.ExtensionSet.gitHubFlavored.blockSyntaxes, | ||||
|                             [ | ||||
|                               md.EmojiSyntax(), | ||||
|                               ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes | ||||
|                             ], | ||||
|                           ), | ||||
|                         )) | ||||
|                     : Text(changeLog), | ||||
|               ], | ||||
|               singleNullReturnButton: tr('ok'), | ||||
|             ); | ||||
|           }); | ||||
|     } | ||||
|  | ||||
|     getLoadingWidgets() { | ||||
|       return [ | ||||
|         if (listedApps.isEmpty) | ||||
| @@ -377,6 +351,35 @@ class AppsPageState extends State<AppsPage> { | ||||
|       ]; | ||||
|     } | ||||
|  | ||||
|     getChangeLogFn(int appIndex) { | ||||
|       AppSource appSource = SourceProvider().getSource( | ||||
|           listedApps[appIndex].app.url, | ||||
|           overrideSource: listedApps[appIndex].app.overrideSource); | ||||
|       String? changesUrl = | ||||
|           appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url); | ||||
|       String? changeLog = listedApps[appIndex].app.changeLog; | ||||
|       if (changeLog?.split('\n').length == 1) { | ||||
|         if (RegExp( | ||||
|                 '(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?') | ||||
|             .hasMatch(changeLog!)) { | ||||
|           if (changesUrl == null) { | ||||
|             changesUrl = changeLog; | ||||
|             changeLog = null; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return (changeLog == null && changesUrl == null) | ||||
|           ? null | ||||
|           : () { | ||||
|               if (changeLog != null) { | ||||
|                 showChangeLogDialog(changesUrl, appSource, changeLog, appIndex); | ||||
|               } else { | ||||
|                 launchUrlString(changesUrl!, | ||||
|                     mode: LaunchMode.externalApplication); | ||||
|               } | ||||
|             }; | ||||
|     } | ||||
|  | ||||
|     getUpdateButton(int appIndex) { | ||||
|       return IconButton( | ||||
|           visualDensity: VisualDensity.compact, | ||||
| @@ -441,7 +444,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|     } | ||||
|  | ||||
|     getSingleAppHorizTile(int index) { | ||||
|       var showChangesFn = getChangeLogFn(context, listedApps[index].app); | ||||
|       var showChangesFn = getChangeLogFn(index); | ||||
|       var hasUpdate = listedApps[index].app.installedVersion != null && | ||||
|           listedApps[index].app.installedVersion != | ||||
|               listedApps[index].app.latestVersion; | ||||
| @@ -854,18 +857,29 @@ class AppsPageState extends State<AppsPage> { | ||||
|               scrollable: true, | ||||
|               content: Padding( | ||||
|                 padding: const EdgeInsets.only(top: 6), | ||||
|                 child: Column( | ||||
|                 child: Row( | ||||
|                     mainAxisAlignment: MainAxisAlignment.spaceAround, | ||||
|                     children: [ | ||||
|                       TextButton( | ||||
|                       IconButton( | ||||
|                           onPressed: appsProvider.areDownloadsRunning() | ||||
|                               ? null | ||||
|                               : showMassMarkDialog, | ||||
|                           tooltip: tr('markSelectedAppsUpdated'), | ||||
|                           icon: const Icon(Icons.done)), | ||||
|                       IconButton( | ||||
|                         onPressed: pinSelectedApps, | ||||
|                           child: Text(selectedApps | ||||
|                         tooltip: selectedApps | ||||
|                                 .where((element) => element.pinned) | ||||
|                                 .isEmpty | ||||
|                             ? tr('pinToTop') | ||||
|                               : tr('unpinFromTop'))), | ||||
|                       const Divider(), | ||||
|                       TextButton( | ||||
|                             : tr('unpinFromTop'), | ||||
|                         icon: Icon(selectedApps | ||||
|                                 .where((element) => element.pinned) | ||||
|                                 .isEmpty | ||||
|                             ? Icons.bookmark_outline_rounded | ||||
|                             : Icons.bookmark_remove_outlined), | ||||
|                       ), | ||||
|                       IconButton( | ||||
|                         onPressed: () { | ||||
|                           String urls = ''; | ||||
|                           for (var a in selectedApps) { | ||||
| @@ -876,9 +890,10 @@ class AppsPageState extends State<AppsPage> { | ||||
|                               subject: 'Obtainium - ${tr('appsString')}'); | ||||
|                           Navigator.of(context).pop(); | ||||
|                         }, | ||||
|                           child: Text(tr('shareSelectedAppURLs'))), | ||||
|                       const Divider(), | ||||
|                       TextButton( | ||||
|                         tooltip: tr('shareSelectedAppURLs'), | ||||
|                         icon: const Icon(Icons.share_rounded), | ||||
|                       ), | ||||
|                       IconButton( | ||||
|                         onPressed: selectedAppIds.isEmpty | ||||
|                             ? null | ||||
|                             : () { | ||||
| @@ -900,32 +915,11 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                 urls += | ||||
|                                     '</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>'; | ||||
|                                 Share.share(urls, | ||||
|                                       subject: | ||||
|                                           'Obtainium - ${tr('appsString')}'); | ||||
|                                     subject: 'Obtainium - ${tr('appsString')}'); | ||||
|                               }, | ||||
|                           child: Text(tr('shareAppConfigLinks'))), | ||||
|                       const Divider(), | ||||
|                       TextButton( | ||||
|                           onPressed: () { | ||||
|                             appsProvider | ||||
|                                 .downloadAppAssets( | ||||
|                                     selectedApps.map((e) => e.id).toList(), | ||||
|                                     globalNavigatorKey.currentContext ?? | ||||
|                                         context) | ||||
|                                 .catchError((e) => showError( | ||||
|                                     e, | ||||
|                                     globalNavigatorKey.currentContext ?? | ||||
|                                         context)); | ||||
|                             Navigator.of(context).pop(); | ||||
|                           }, | ||||
|                           child: Text(tr('downloadX', | ||||
|                               args: [tr('releaseAsset').toLowerCase()]))), | ||||
|                       const Divider(), | ||||
|                       TextButton( | ||||
|                           onPressed: appsProvider.areDownloadsRunning() | ||||
|                               ? null | ||||
|                               : showMassMarkDialog, | ||||
|                           child: Text(tr('markSelectedAppsUpdated'))), | ||||
|                         tooltip: tr('shareAppConfigLinks'), | ||||
|                         icon: const Icon(Icons.ios_share), | ||||
|                       ), | ||||
|                     ]), | ||||
|               ), | ||||
|             ); | ||||
|   | ||||
| @@ -351,22 +351,6 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
|                                 Flexible( | ||||
|                                     child: | ||||
|                                         Text(tr('removeOnExternalUninstall'))), | ||||
|                                 Switch( | ||||
|                                     value: settingsProvider | ||||
|                                         .removeOnExternalUninstall, | ||||
|                                     onChanged: (value) { | ||||
|                                       settingsProvider | ||||
|                                           .removeOnExternalUninstall = value; | ||||
|                                     }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
| @@ -379,43 +363,6 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                                     }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
|                                 Flexible( | ||||
|                                     child: Column( | ||||
|                                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                   mainAxisSize: MainAxisSize.min, | ||||
|                                   children: [ | ||||
|                                     Text(tr( | ||||
|                                         'beforeNewInstallsShareToAppVerifier')), | ||||
|                                     GestureDetector( | ||||
|                                         onTap: () { | ||||
|                                           launchUrlString( | ||||
|                                               'https://github.com/soupslurpr/AppVerifier', | ||||
|                                               mode: LaunchMode | ||||
|                                                   .externalApplication); | ||||
|                                         }, | ||||
|                                         child: Text( | ||||
|                                           tr('about'), | ||||
|                                           style: const TextStyle( | ||||
|                                               decoration: | ||||
|                                                   TextDecoration.underline, | ||||
|                                               fontSize: 12), | ||||
|                                         )), | ||||
|                                   ], | ||||
|                                 )), | ||||
|                                 Switch( | ||||
|                                     value: settingsProvider | ||||
|                                         .beforeNewInstallsShareToAppVerifier, | ||||
|                                     onChanged: (value) { | ||||
|                                       settingsProvider | ||||
|                                               .beforeNewInstallsShareToAppVerifier = | ||||
|                                           value; | ||||
|                                     }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             installMethodDropdown, | ||||
|                             height32, | ||||
|                             Text( | ||||
| @@ -527,6 +474,22 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
|                                 Flexible( | ||||
|                                     child: | ||||
|                                         Text(tr('removeOnExternalUninstall'))), | ||||
|                                 Switch( | ||||
|                                     value: settingsProvider | ||||
|                                         .removeOnExternalUninstall, | ||||
|                                     onChanged: (value) { | ||||
|                                       settingsProvider | ||||
|                                           .removeOnExternalUninstall = value; | ||||
|                                     }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
|   | ||||
| @@ -5,15 +5,15 @@ import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'dart:math'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:http/http.dart' as http; | ||||
| import 'package:crypto/crypto.dart'; | ||||
| import 'package:http/http.dart' as http; | ||||
|  | ||||
| import 'package:android_intent_plus/flag.dart'; | ||||
| import 'package:android_package_installer/android_package_installer.dart'; | ||||
| import 'package:android_package_manager/android_package_manager.dart'; | ||||
| import 'package:connectivity_plus/connectivity_plus.dart'; | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -29,10 +29,8 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:flutter_fgbg/flutter_fgbg.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:android_intent_plus/android_intent.dart'; | ||||
| import 'package:flutter_archive/flutter_archive.dart'; | ||||
| import 'package:share_plus/share_plus.dart'; | ||||
| import 'package:shared_storage/shared_storage.dart' as saf; | ||||
| import 'native_provider.dart'; | ||||
|  | ||||
| @@ -148,10 +146,10 @@ Future<File> downloadFileWithRetry( | ||||
|     Map<String, String>? headers, | ||||
|     int retries = 3}) async { | ||||
|   try { | ||||
|     return await downloadFile(url, fileNameNoExt, onProgress, destDir, | ||||
|     return await downloadApk(url, fileNameNoExt, onProgress, destDir, | ||||
|         useExisting: useExisting, headers: headers); | ||||
|   } catch (e) { | ||||
|     if (retries > 0 && e is ClientException) { | ||||
|     if (retries > 0 && e is DioException) { | ||||
|       await Future.delayed(const Duration(seconds: 5)); | ||||
|       return await downloadFileWithRetry( | ||||
|           url, fileNameNoExt, onProgress, destDir, | ||||
| @@ -185,36 +183,11 @@ Future<String> checkPartialDownloadHashDynamic(String url, | ||||
|   throw NoVersionError(); | ||||
| } | ||||
|  | ||||
| Future<String> checkPartialDownloadHash(String url, int bytesToGrab, | ||||
|     {Map<String, String>? headers}) async { | ||||
|   var req = Request('GET', Uri.parse(url)); | ||||
|   if (headers != null) { | ||||
|     req.headers.addAll(headers); | ||||
|   } | ||||
|   req.headers[HttpHeaders.rangeHeader] = 'bytes=0-$bytesToGrab'; | ||||
|   var client = http.Client(); | ||||
|   var response = await client.send(req); | ||||
|   if (response.statusCode < 200 || response.statusCode > 299) { | ||||
|     throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError')); | ||||
|   } | ||||
|   List<List<int>> bytes = await response.stream.take(bytesToGrab).toList(); | ||||
|   return hashListOfLists(bytes); | ||||
| } | ||||
|  | ||||
| Future<File> downloadFile( | ||||
| Future<File> downloadApk( | ||||
|     String url, String fileNameNoExt, Function? onProgress, String destDir, | ||||
|     {bool useExisting = true, Map<String, String>? headers}) async { | ||||
|   // Send the initial request but cancel it as soon as you have the headers | ||||
|   var reqHeaders = headers ?? {}; | ||||
|   var req = Request('GET', Uri.parse(url)); | ||||
|   req.headers.addAll(reqHeaders); | ||||
|   var client = http.Client(); | ||||
|   StreamedResponse response = await client.send(req); | ||||
|   var resHeaders = response.headers; | ||||
|   var resHeaders = await getHeaders(url, headers: headers); | ||||
|  | ||||
|   // Use the headers to decide what the file extension is, and | ||||
|   // whether it supports partial downloads (range request), and | ||||
|   // what the total size of the file is (if provided) | ||||
|   String ext = resHeaders['content-disposition']?.split('.').last ?? 'apk'; | ||||
|   if (ext.endsWith('"') || ext.endsWith("other")) { | ||||
|     ext = ext.substring(0, ext.length - 1); | ||||
| @@ -222,91 +195,129 @@ Future<File> downloadFile( | ||||
|   if (url.toLowerCase().endsWith('.apk') && ext != 'apk') { | ||||
|     ext = 'apk'; | ||||
|   } | ||||
|   File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); | ||||
|   File file = File('$destDir/$fileNameNoExt.$ext'); | ||||
|  | ||||
|   bool rangeFeatureEnabled = false; | ||||
|   if (resHeaders['accept-ranges']?.isNotEmpty == true) { | ||||
|     rangeFeatureEnabled = | ||||
|         resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes'; | ||||
|   } | ||||
|   final contentLength = await getContentLengthIfRangeSupported(resHeaders); | ||||
|  | ||||
|   // If you have an existing file that is usable, | ||||
|   // decide whether you can use it (either return full or resume partial) | ||||
|   var fullContentLength = response.contentLength; | ||||
|   if (useExisting && downloadedFile.existsSync()) { | ||||
|     var length = downloadedFile.lengthSync(); | ||||
|     if (fullContentLength == null) { | ||||
|       // Assume full | ||||
|       client.close(); | ||||
|       return downloadedFile; | ||||
|   if (useExisting && file.existsSync()) { | ||||
|     var length = file.lengthSync(); | ||||
|     if (contentLength == null) { | ||||
|       return file; | ||||
|     } else { | ||||
|       // Check if resume needed/possible | ||||
|       if (length == fullContentLength) { | ||||
|         client.close(); | ||||
|         return downloadedFile; | ||||
|       if (length == contentLength) { | ||||
|         return file; | ||||
|       } | ||||
|       if (length > fullContentLength) { | ||||
|       if (length > contentLength) { | ||||
|         useExisting = false; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Download to a '.temp' file (to distinguish btn. complete/incomplete files) | ||||
|   File tempDownloadedFile = File('${downloadedFile.path}.part'); | ||||
|   double progress = -1; | ||||
|  | ||||
|   // If the range feature is not available (or you need to start a ranged req from 0), | ||||
|   // complete the already-started request, else cancel it and start a ranged request, | ||||
|   // and open the file for writing in the appropriate mode | ||||
|   var targetFileLength = useExisting && tempDownloadedFile.existsSync() | ||||
|       ? tempDownloadedFile.lengthSync() | ||||
|       : null; | ||||
|   try { | ||||
|     if (contentLength == null) { | ||||
|       Response response = await dio.download( | ||||
|         url, | ||||
|         file.path, | ||||
|         options: Options(headers: headers), | ||||
|         onReceiveProgress: (count, total) { | ||||
|           progress = (total > 0 ? count / total * 100 : 30); | ||||
|           if (onProgress != null) { | ||||
|             onProgress(progress); | ||||
|           } | ||||
|         }, | ||||
|       ); | ||||
|       if ((response.statusCode ?? 200) < 200 || | ||||
|           (response.statusCode ?? 200) > 299) { | ||||
|         throw response.statusMessage ?? tr('unexpectedError'); | ||||
|       } | ||||
|     } else { | ||||
|       var targetFileLength = | ||||
|           useExisting && file.existsSync() ? file.lengthSync() : null; | ||||
|       int bufferSize = 1024 * 1024; // 1 Megabyte | ||||
|       final sink = file.openWrite( | ||||
|         mode: useExisting ? FileMode.writeOnlyAppend : FileMode.writeOnly, | ||||
|       ); | ||||
|       int rangeStart = targetFileLength ?? 0; | ||||
|   IOSink? sink; | ||||
|   if (rangeFeatureEnabled && fullContentLength != null && rangeStart > 0) { | ||||
|     client.close(); | ||||
|     client = http.Client(); | ||||
|     req = Request('GET', Uri.parse(url)); | ||||
|     req.headers.addAll(reqHeaders); | ||||
|     req.headers.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'}); | ||||
|     response = await client.send(req); | ||||
|     sink = tempDownloadedFile.openWrite(mode: FileMode.writeOnlyAppend); | ||||
|   } else if (tempDownloadedFile.existsSync()) { | ||||
|     tempDownloadedFile.deleteSync(recursive: true); | ||||
|       int rangeEnd = min( | ||||
|         rangeStart + bufferSize - 1, | ||||
|         contentLength - 1, | ||||
|       ); | ||||
|       if (onProgress != null) { | ||||
|         progress = ((rangeStart / contentLength) * 100); | ||||
|         onProgress(progress); | ||||
|       } | ||||
|   sink ??= tempDownloadedFile.openWrite(mode: FileMode.writeOnly); | ||||
|       while (true) { | ||||
|         var headersCurrent = headers ?? {}; | ||||
|         headersCurrent['range'] = 'bytes=$rangeStart-$rangeEnd'; | ||||
|         Response response = await dio.get( | ||||
|           url, | ||||
|           onReceiveProgress: (count, total) { | ||||
|             if (onProgress != null) { | ||||
|               final newProgress = | ||||
|                   (((rangeStart + count) / contentLength) * 100); | ||||
|               if (newProgress != progress) { | ||||
|                 progress = newProgress; | ||||
|                 onProgress(progress); | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           options: Options( | ||||
|             headers: headersCurrent, | ||||
|             responseType: ResponseType.bytes, | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|   // Perform the download | ||||
|   var received = 0; | ||||
|   double? progress; | ||||
|   if (rangeStart > 0 && fullContentLength != null) { | ||||
|     received = rangeStart; | ||||
|         if ((response.statusCode ?? 200) < 200 || | ||||
|             (response.statusCode ?? 200) > 299) { | ||||
|           throw response.statusMessage ?? tr('unexpectedError'); | ||||
|         } | ||||
|   await response.stream.map((s) { | ||||
|     received += s.length; | ||||
|     progress = | ||||
|         (fullContentLength != null ? (received / fullContentLength) * 100 : 30); | ||||
|     if (onProgress != null) { | ||||
|       onProgress(progress); | ||||
|  | ||||
|         final Uint8List data = response.data; | ||||
|         sink.add(data); | ||||
|         if (rangeEnd == contentLength - 1) { | ||||
|           break; | ||||
|         } | ||||
|     return s; | ||||
|   }).pipe(sink); | ||||
|         rangeStart = rangeEnd + 1; | ||||
|         rangeEnd = min( | ||||
|           rangeStart + bufferSize - 1, | ||||
|           contentLength - 1, | ||||
|         ); | ||||
|       } | ||||
|       await sink.flush(); | ||||
|       await sink.close(); | ||||
|   bool likelyCorruptFile = (progress ?? 0) > 101; | ||||
|   progress = null; | ||||
|     } | ||||
|   } finally { | ||||
|     if (onProgress != null) { | ||||
|     onProgress(progress); | ||||
|       onProgress(null); | ||||
|     } | ||||
|   if (response.statusCode < 200 || | ||||
|       response.statusCode > 299 || | ||||
|       likelyCorruptFile) { | ||||
|     tempDownloadedFile.deleteSync(recursive: true); | ||||
|     throw response.reasonPhrase ?? tr('unexpectedError'); | ||||
|   } | ||||
|   if (tempDownloadedFile.existsSync()) { | ||||
|     tempDownloadedFile.renameSync(downloadedFile.path); | ||||
|   return file; | ||||
| } | ||||
|  | ||||
| Future<int?> getContentLengthIfRangeSupported( | ||||
|     Map<String, String> headers) async { | ||||
|   try { | ||||
|     int? contentLength; | ||||
|     { | ||||
|       var contentLengthHeaderValue = headers['content-length']; | ||||
|       if (contentLengthHeaderValue?.isNotEmpty == true) { | ||||
|         contentLength = int.tryParse(contentLengthHeaderValue!); | ||||
|       } | ||||
|     } | ||||
|     bool rangeFeatureEnabled = false; | ||||
|     if (headers['accept-ranges']?.isNotEmpty == true) { | ||||
|       rangeFeatureEnabled = | ||||
|           headers['accept-ranges']!.trim().toLowerCase() == 'bytes'; | ||||
|     } | ||||
|     if (!rangeFeatureEnabled) { | ||||
|       contentLength = null; | ||||
|     } | ||||
|     return contentLength; | ||||
|   } catch (e) { | ||||
|     return null; | ||||
|   } | ||||
|   client.close(); | ||||
|   return downloadedFile; | ||||
| } | ||||
|  | ||||
| Future<Map<String, String>> getHeaders(String url, | ||||
| @@ -325,6 +336,58 @@ Future<Map<String, String>> getHeaders(String url, | ||||
|   return returnHeaders; | ||||
| } | ||||
|  | ||||
| Future<String> checkPartialDownloadHash(String url, int bytesToGrab, | ||||
|     {Map<String, String>? headers}) async { | ||||
|   var req = http.Request('GET', Uri.parse(url)); | ||||
|   if (headers != null) { | ||||
|     req.headers.addAll(headers); | ||||
|   } | ||||
|   req.headers[HttpHeaders.rangeHeader] = 'bytes=0-$bytesToGrab'; | ||||
|   var client = http.Client(); | ||||
|   var response = await client.send(req); | ||||
|   if (response.statusCode < 200 || response.statusCode > 299) { | ||||
|     throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError')); | ||||
|   } | ||||
|   List<List<int>> bytes = await response.stream.take(bytesToGrab).toList(); | ||||
|   return hashListOfLists(bytes); | ||||
| } | ||||
|  | ||||
| // Future<File> downloadFile( | ||||
| //     String url, String fileNameNoExt, Function? onProgress, String destDir, | ||||
| //     {bool useExisting = true, Map<String, String>? headers}) async { | ||||
| //   var resHead = await dio.head(url); | ||||
| //   String ext = | ||||
| //       resHead.headers.map['content-disposition']?[0].split('.').last ?? 'apk'; | ||||
| //   if (ext.endsWith('"') || ext.endsWith("other")) { | ||||
| //     ext = ext.substring(0, ext.length - 1); | ||||
| //   } | ||||
| //   if (url.toLowerCase().endsWith('.apk') && ext != 'apk') { | ||||
| //     ext = 'apk'; | ||||
| //   } | ||||
| //   File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); | ||||
| //   if (!(downloadedFile.existsSync() && useExisting)) { | ||||
| //     double? progress; | ||||
| //     var response = await dio.download( | ||||
| //       url, | ||||
| //       downloadedFile.path, | ||||
| //       options: Options(headers: headers), | ||||
| //       onReceiveProgress: (count, total) { | ||||
| //         progress = (total > 0 ? count / total * 100 : 30); | ||||
| //         if (onProgress != null) { | ||||
| //           onProgress(progress); | ||||
| //         } | ||||
| //       }, | ||||
| //     ); | ||||
| //     if (onProgress != null) { | ||||
| //       onProgress(null); | ||||
| //     } | ||||
| //     if (response.statusCode != 200) { | ||||
| //       throw response.statusMessage ?? tr('unexpectedError'); | ||||
| //     } | ||||
| //   } | ||||
| //   return downloadedFile; | ||||
| // } | ||||
|  | ||||
| Future<PackageInfo?> getInstalledInfo(String? packageName, | ||||
|     {bool printErr = true}) async { | ||||
|   if (packageName != null) { | ||||
| @@ -360,7 +423,7 @@ class AppsProvider with ChangeNotifier { | ||||
|     foregroundStream = FGBGEvents.stream.asBroadcastStream(); | ||||
|     foregroundSubscription = foregroundStream?.listen((event) async { | ||||
|       isForeground = event == FGBGType.foreground; | ||||
|       if (isForeground) loadApps(); | ||||
|       if (isForeground) await loadApps(); | ||||
|     }); | ||||
|     () async { | ||||
|       await settingsProvider.initializeSettings(); | ||||
| @@ -566,14 +629,13 @@ class AppsProvider with ChangeNotifier { | ||||
|         zipFile: File(filePath), destinationDir: Directory(destinationPath)); | ||||
|   } | ||||
|  | ||||
|   Future<bool> installXApkDir( | ||||
|       DownloadedXApkDir dir, BuildContext? firstTimeWithContext, | ||||
|   Future<void> installXApkDir(DownloadedXApkDir dir, | ||||
|       {bool needsBGWorkaround = false}) async { | ||||
|     // We don't know which APKs in an XAPK are supported by the user's device | ||||
|     // So we try installing all of them and assume success if at least one installed | ||||
|     // If 0 APKs installed, throw the first install error encountered | ||||
|     var somethingInstalled = false; | ||||
|     try { | ||||
|       var somethingInstalled = false; | ||||
|       MultiAppMultiError errors = MultiAppMultiError(); | ||||
|       for (var file in dir.extracted | ||||
|           .listSync(recursive: true, followLinks: false) | ||||
| @@ -581,8 +643,7 @@ class AppsProvider with ChangeNotifier { | ||||
|         if (file.path.toLowerCase().endsWith('.apk')) { | ||||
|           try { | ||||
|             somethingInstalled = somethingInstalled || | ||||
|                 await installApk( | ||||
|                     DownloadedApk(dir.appId, file), firstTimeWithContext, | ||||
|                 await installApk(DownloadedApk(dir.appId, file), | ||||
|                     needsBGWorkaround: needsBGWorkaround); | ||||
|           } catch (e) { | ||||
|             logs.add( | ||||
| @@ -601,22 +662,10 @@ class AppsProvider with ChangeNotifier { | ||||
|     } finally { | ||||
|       dir.extracted.delete(recursive: true); | ||||
|     } | ||||
|     return somethingInstalled; | ||||
|   } | ||||
|  | ||||
|   Future<bool> installApk( | ||||
|       DownloadedApk file, BuildContext? firstTimeWithContext, | ||||
|   Future<bool> installApk(DownloadedApk file, | ||||
|       {bool needsBGWorkaround = false}) async { | ||||
|     if (firstTimeWithContext != null && | ||||
|         settingsProvider.beforeNewInstallsShareToAppVerifier && | ||||
|         (await getInstalledInfo('dev.soupslurpr.appverifier')) != null) { | ||||
|       XFile f = XFile.fromData(file.file.readAsBytesSync(), | ||||
|           mimeType: 'application/vnd.android.package-archive'); | ||||
|       Fluttertoast.showToast( | ||||
|           msg: tr('appVerifierInstructionToast'), | ||||
|           toastLength: Toast.LENGTH_LONG); | ||||
|       await Share.shareXFiles([f]); | ||||
|     } | ||||
|     var newInfo = | ||||
|         await pm.getPackageArchiveInfo(archiveFilePath: file.file.path); | ||||
|     if (newInfo == null) { | ||||
| @@ -664,13 +713,7 @@ class AppsProvider with ChangeNotifier { | ||||
|     } | ||||
|     bool installed = false; | ||||
|     if (code != null && code != 0 && code != 3) { | ||||
|       try { | ||||
|         file.file.deleteSync(recursive: true); | ||||
|       } catch (e) { | ||||
|         // | ||||
|       } finally { | ||||
|       throw InstallError(code); | ||||
|       } | ||||
|     } else if (code == 0) { | ||||
|       installed = true; | ||||
|       apps[file.appId]!.app.installedVersion = | ||||
| @@ -705,28 +748,23 @@ class AppsProvider with ChangeNotifier { | ||||
|     await intent.launch(); | ||||
|   } | ||||
|  | ||||
|   Future<MapEntry<String, String>?> confirmAppFileUrl( | ||||
|       App app, BuildContext? context, bool pickAnyAsset) async { | ||||
|     var urlsToSelectFrom = app.apkUrls; | ||||
|     if (pickAnyAsset) { | ||||
|       urlsToSelectFrom = [...urlsToSelectFrom, ...app.otherAssetUrls]; | ||||
|     } | ||||
|   Future<MapEntry<String, String>?> confirmApkUrl( | ||||
|       App app, BuildContext? context) async { | ||||
|     // If the App has more than one APK, the user should pick one (if context provided) | ||||
|     MapEntry<String, String>? appFileUrl = urlsToSelectFrom[ | ||||
|         app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0]; | ||||
|     MapEntry<String, String>? apkUrl = | ||||
|         app.apkUrls[app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0]; | ||||
|     // get device supported architecture | ||||
|     List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis; | ||||
|  | ||||
|     if (urlsToSelectFrom.length > 1 && context != null) { | ||||
|     if (app.apkUrls.length > 1 && context != null) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       appFileUrl = await showDialog( | ||||
|       apkUrl = await showDialog( | ||||
|           context: context, | ||||
|           builder: (BuildContext ctx) { | ||||
|             return AppFilePicker( | ||||
|             return APKPicker( | ||||
|               app: app, | ||||
|               initVal: appFileUrl, | ||||
|               initVal: apkUrl, | ||||
|               archs: archs, | ||||
|               pickAnyAsset: pickAnyAsset, | ||||
|             ); | ||||
|           }); | ||||
|     } | ||||
| @@ -736,8 +774,8 @@ class AppsProvider with ChangeNotifier { | ||||
|     } | ||||
|  | ||||
|     // If the picked APK comes from an origin different from the source, get user confirmation (if context provided) | ||||
|     if (appFileUrl != null && | ||||
|         getHost(appFileUrl.value) != getHost(app.url) && | ||||
|     if (apkUrl != null && | ||||
|         getHost(apkUrl.value) != getHost(app.url) && | ||||
|         context != null) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       if (!(settingsProvider.hideAPKOriginWarning) && | ||||
| @@ -746,13 +784,13 @@ class AppsProvider with ChangeNotifier { | ||||
|                   context: context, | ||||
|                   builder: (BuildContext ctx) { | ||||
|                     return APKOriginWarningDialog( | ||||
|                         sourceUrl: app.url, apkUrl: appFileUrl!.value); | ||||
|                         sourceUrl: app.url, apkUrl: apkUrl!.value); | ||||
|                   }) != | ||||
|               true) { | ||||
|         appFileUrl = null; | ||||
|         apkUrl = null; | ||||
|       } | ||||
|     } | ||||
|     return appFileUrl; | ||||
|     return apkUrl; | ||||
|   } | ||||
|  | ||||
|   // Given a list of AppIds, uses stored info about the apps to download APKs and install them | ||||
| @@ -779,7 +817,7 @@ class AppsProvider with ChangeNotifier { | ||||
|       var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true; | ||||
|       if (!trackOnly) { | ||||
|         // ignore: use_build_context_synchronously | ||||
|         apkUrl = await confirmAppFileUrl(apps[id]!.app, context, false); | ||||
|         apkUrl = await confirmApkUrl(apps[id]!.app, context); | ||||
|       } | ||||
|       if (apkUrl != null) { | ||||
|         int urlInd = apps[id]! | ||||
| @@ -856,24 +894,17 @@ class AppsProvider with ChangeNotifier { | ||||
|         notifyListeners(); | ||||
|         try { | ||||
|           if (!skipInstalls) { | ||||
|             bool sayInstalled = true; | ||||
|             var contextIfNewInstall = | ||||
|                 apps[id]?.installedInfo == null ? context : null; | ||||
|             if (downloadedFile != null) { | ||||
|               if (willBeSilent && context == null) { | ||||
|                 installApk(downloadedFile, contextIfNewInstall, | ||||
|                     needsBGWorkaround: true); | ||||
|                 installApk(downloadedFile, needsBGWorkaround: true); | ||||
|               } else { | ||||
|                 sayInstalled = | ||||
|                     await installApk(downloadedFile, contextIfNewInstall); | ||||
|                 await installApk(downloadedFile); | ||||
|               } | ||||
|             } else { | ||||
|               if (willBeSilent && context == null) { | ||||
|                 installXApkDir(downloadedDir!, contextIfNewInstall, | ||||
|                     needsBGWorkaround: true); | ||||
|                 installXApkDir(downloadedDir!, needsBGWorkaround: true); | ||||
|               } else { | ||||
|                 sayInstalled = | ||||
|                     await installXApkDir(downloadedDir!, contextIfNewInstall); | ||||
|                 await installXApkDir(downloadedDir!); | ||||
|               } | ||||
|             } | ||||
|             if (willBeSilent && context == null) { | ||||
| @@ -881,10 +912,8 @@ class AppsProvider with ChangeNotifier { | ||||
|                   [apps[id]!.app], | ||||
|                   id: id.hashCode)); | ||||
|             } | ||||
|             if (sayInstalled) { | ||||
|             installedIds.add(id); | ||||
|           } | ||||
|           } | ||||
|         } finally { | ||||
|           apps[id]?.downloadProgress = null; | ||||
|           notifyListeners(); | ||||
| @@ -916,85 +945,6 @@ class AppsProvider with ChangeNotifier { | ||||
|     return installedIds; | ||||
|   } | ||||
|  | ||||
|   Future<List<String>> downloadAppAssets( | ||||
|       List<String> appIds, BuildContext context, | ||||
|       {bool forceParallelDownloads = false}) async { | ||||
|     NotificationsProvider notificationsProvider = | ||||
|         context.read<NotificationsProvider>(); | ||||
|     List<MapEntry<MapEntry<String, String>, App>> filesToDownload = []; | ||||
|     for (var id in appIds) { | ||||
|       if (apps[id] == null) { | ||||
|         throw ObtainiumError(tr('appNotFound')); | ||||
|       } | ||||
|       MapEntry<String, String>? fileUrl; | ||||
|       if (apps[id]!.app.apkUrls.isNotEmpty || | ||||
|           apps[id]!.app.otherAssetUrls.isNotEmpty) { | ||||
|         // ignore: use_build_context_synchronously | ||||
|         fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true); | ||||
|       } | ||||
|       if (fileUrl != null) { | ||||
|         filesToDownload.add(MapEntry(fileUrl, apps[id]!.app)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Prepare to download+install Apps | ||||
|     MultiAppMultiError errors = MultiAppMultiError(); | ||||
|     List<String> downloadedIds = []; | ||||
|  | ||||
|     Future<void> downloadFn(MapEntry<String, String> fileUrl, App app) async { | ||||
|       try { | ||||
|         var exportDir = await settingsProvider.getExportDir(); | ||||
|         String downloadPath = '/storage/emulated/0/Download'; | ||||
|         bool downloadsAccessible = false; | ||||
|         try { | ||||
|           downloadsAccessible = Directory(downloadPath).existsSync(); | ||||
|         } catch (e) { | ||||
|           // | ||||
|         } | ||||
|         if (!downloadsAccessible && exportDir != null) { | ||||
|           downloadPath = exportDir.path; | ||||
|         } | ||||
|         await downloadFile( | ||||
|             fileUrl.value, | ||||
|             fileUrl.key | ||||
|                 .split('.') | ||||
|                 .reversed | ||||
|                 .toList() | ||||
|                 .sublist(1) | ||||
|                 .reversed | ||||
|                 .join('.'), (double? progress) { | ||||
|           notificationsProvider | ||||
|               .notify(DownloadNotification(fileUrl.key, progress?.ceil() ?? 0)); | ||||
|         }, downloadPath, | ||||
|             headers: await SourceProvider() | ||||
|                 .getSource(app.url, overrideSource: app.overrideSource) | ||||
|                 .getRequestHeaders(app.additionalSettings, | ||||
|                     forAPKDownload: | ||||
|                         fileUrl.key.endsWith('.apk') ? true : false), | ||||
|             useExisting: false); | ||||
|         notificationsProvider | ||||
|             .notify(DownloadedNotification(fileUrl.key, fileUrl.value)); | ||||
|       } catch (e) { | ||||
|         errors.add(fileUrl.key, e); | ||||
|       } finally { | ||||
|         notificationsProvider.cancel(DownloadNotification(fileUrl.key, 0).id); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (forceParallelDownloads || !settingsProvider.parallelDownloads) { | ||||
|       for (var urlWithApp in filesToDownload) { | ||||
|         await downloadFn(urlWithApp.key, urlWithApp.value); | ||||
|       } | ||||
|     } else { | ||||
|       await Future.wait(filesToDownload | ||||
|           .map((urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value))); | ||||
|     } | ||||
|     if (errors.idsByErrorString.isNotEmpty) { | ||||
|       throw errors; | ||||
|     } | ||||
|     return downloadedIds; | ||||
|   } | ||||
|  | ||||
|   Future<Directory> getAppsDir() async { | ||||
|     Directory appsDir = | ||||
|         Directory('${(await getExternalStorageDirectory())!.path}/app_data'); | ||||
| @@ -1566,49 +1516,38 @@ class AppsProvider with ChangeNotifier { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AppFilePicker extends StatefulWidget { | ||||
|   const AppFilePicker( | ||||
|       {super.key, | ||||
|       required this.app, | ||||
|       this.initVal, | ||||
|       this.archs, | ||||
|       this.pickAnyAsset = false}); | ||||
| class APKPicker extends StatefulWidget { | ||||
|   const APKPicker({super.key, required this.app, this.initVal, this.archs}); | ||||
|  | ||||
|   final App app; | ||||
|   final MapEntry<String, String>? initVal; | ||||
|   final List<String>? archs; | ||||
|   final bool pickAnyAsset; | ||||
|  | ||||
|   @override | ||||
|   State<AppFilePicker> createState() => _AppFilePickerState(); | ||||
|   State<APKPicker> createState() => _APKPickerState(); | ||||
| } | ||||
|  | ||||
| class _AppFilePickerState extends State<AppFilePicker> { | ||||
|   MapEntry<String, String>? fileUrl; | ||||
| class _APKPickerState extends State<APKPicker> { | ||||
|   MapEntry<String, String>? apkUrl; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     fileUrl ??= widget.initVal; | ||||
|     var urlsToSelectFrom = widget.app.apkUrls; | ||||
|     if (widget.pickAnyAsset) { | ||||
|       urlsToSelectFrom = [...urlsToSelectFrom, ...widget.app.otherAssetUrls]; | ||||
|     } | ||||
|     apkUrl ??= widget.initVal; | ||||
|     return AlertDialog( | ||||
|       scrollable: true, | ||||
|       title: Text(widget.pickAnyAsset | ||||
|           ? tr('selectX', args: [tr('releaseAsset').toLowerCase()]) | ||||
|           : tr('pickAnAPK')), | ||||
|       title: Text(tr('pickAnAPK')), | ||||
|       content: Column(children: [ | ||||
|         Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])), | ||||
|         const SizedBox(height: 16), | ||||
|         ...urlsToSelectFrom.map( | ||||
|         ...widget.app.apkUrls.map( | ||||
|           (u) => RadioListTile<String>( | ||||
|               title: Text(u.key), | ||||
|               value: u.value, | ||||
|               groupValue: fileUrl!.value, | ||||
|               groupValue: apkUrl!.value, | ||||
|               onChanged: (String? val) { | ||||
|                 setState(() { | ||||
|                   fileUrl = urlsToSelectFrom.where((e) => e.value == val).first; | ||||
|                   apkUrl = | ||||
|                       widget.app.apkUrls.where((e) => e.value == val).first; | ||||
|                 }); | ||||
|               }), | ||||
|         ), | ||||
| @@ -1635,7 +1574,7 @@ class _AppFilePickerState extends State<AppFilePicker> { | ||||
|         TextButton( | ||||
|             onPressed: () { | ||||
|               HapticFeedback.selectionClick(); | ||||
|               Navigator.of(context).pop(fileUrl); | ||||
|               Navigator.of(context).pop(apkUrl); | ||||
|             }, | ||||
|             child: Text(tr('continue'))) | ||||
|       ], | ||||
| @@ -1818,7 +1757,7 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async { | ||||
|             // Next task interval is based on the error with the longest retry time | ||||
|             int minRetryIntervalForThisApp = err is RateLimitError | ||||
|                 ? (err.remainingMinutes * 60) | ||||
|                 : e is ClientException | ||||
|                 : e is DioException | ||||
|                     ? (15 * 60) | ||||
|                     : (toCheckApp.value + 1); | ||||
|             if (minRetryIntervalForThisApp > maxRetryWaitSeconds) { | ||||
|   | ||||
| @@ -120,18 +120,6 @@ class DownloadNotification extends ObtainiumNotification { | ||||
|             progPercent: progPercent); | ||||
| } | ||||
|  | ||||
| class DownloadedNotification extends ObtainiumNotification { | ||||
|   DownloadedNotification(String fileName, String downloadUrl) | ||||
|       : super( | ||||
|             downloadUrl.hashCode, | ||||
|             tr('downloadedX', args: [fileName]), | ||||
|             '', | ||||
|             'FILE_DOWNLOADED', | ||||
|             tr('downloadedXNotifChannel', args: [tr('app')]), | ||||
|             tr('downloadedX', args: [tr('app')]), | ||||
|             Importance.defaultImportance); | ||||
| } | ||||
|  | ||||
| final completeInstallationNotification = ObtainiumNotification( | ||||
|     1, | ||||
|     tr('completeAppInstallation'), | ||||
|   | ||||
| @@ -479,13 +479,4 @@ class SettingsProvider with ChangeNotifier { | ||||
|     prefs?.setStringList('searchDeselected', list); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   bool get beforeNewInstallsShareToAppVerifier { | ||||
|     return prefs?.getBool('beforeNewInstallsShareToAppVerifier') ?? true; | ||||
|   } | ||||
|  | ||||
|   set beforeNewInstallsShareToAppVerifier(bool val) { | ||||
|     prefs?.setBool('beforeNewInstallsShareToAppVerifier', val); | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,9 +4,9 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/dom.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/apkmirror.dart'; | ||||
| import 'package:obtainium/app_sources/apkpure.dart'; | ||||
| import 'package:obtainium/app_sources/aptoide.dart'; | ||||
| @@ -31,6 +31,7 @@ import 'package:obtainium/app_sources/vlc.dart'; | ||||
| import 'package:obtainium/app_sources/whatsapp.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/mass_app_sources/githubstars.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
|  | ||||
| @@ -47,10 +48,9 @@ class APKDetails { | ||||
|   late AppNames names; | ||||
|   late DateTime? releaseDate; | ||||
|   late String? changeLog; | ||||
|   late List<MapEntry<String, String>> allAssetUrls; | ||||
|  | ||||
|   APKDetails(this.version, this.apkUrls, this.names, | ||||
|       {this.releaseDate, this.changeLog, this.allAssetUrls = const []}); | ||||
|       {this.releaseDate, this.changeLog}); | ||||
| } | ||||
|  | ||||
| stringMapListTo2DList(List<MapEntry<String, String>> mapList) => | ||||
| @@ -224,7 +224,6 @@ class App { | ||||
|   String? installedVersion; | ||||
|   late String latestVersion; | ||||
|   List<MapEntry<String, String>> apkUrls = []; | ||||
|   List<MapEntry<String, String>> otherAssetUrls = []; | ||||
|   late int preferredApkIndex; | ||||
|   late Map<String, dynamic> additionalSettings; | ||||
|   late DateTime? lastUpdateCheck; | ||||
| @@ -250,8 +249,7 @@ class App { | ||||
|       this.releaseDate, | ||||
|       this.changeLog, | ||||
|       this.overrideSource, | ||||
|       this.allowIdChange = false, | ||||
|       this.otherAssetUrls = const []}); | ||||
|       this.allowIdChange = false}); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -283,8 +281,7 @@ class App { | ||||
|       changeLog: changeLog, | ||||
|       releaseDate: releaseDate, | ||||
|       overrideSource: overrideSource, | ||||
|       allowIdChange: allowIdChange, | ||||
|       otherAssetUrls: otherAssetUrls); | ||||
|       allowIdChange: allowIdChange); | ||||
|  | ||||
|   factory App.fromJson(Map<String, dynamic> json) { | ||||
|     json = appJSONCompatibilityModifiers(json); | ||||
| @@ -297,8 +294,8 @@ class App { | ||||
|             ? null | ||||
|             : json['installedVersion'] as String, | ||||
|         (json['latestVersion'] ?? tr('unknown')) as String, | ||||
|       assumed2DlistToStringMapList( | ||||
|           jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))), | ||||
|         assumed2DlistToStringMapList(jsonDecode( | ||||
|             (json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))), | ||||
|         (json['preferredApkIndex'] ?? -1) as int, | ||||
|         jsonDecode(json['additionalSettings']) as Map<String, dynamic>, | ||||
|         json['lastUpdateCheck'] == null | ||||
| @@ -315,12 +312,10 @@ class App { | ||||
|         releaseDate: json['releaseDate'] == null | ||||
|             ? null | ||||
|             : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']), | ||||
|       changeLog: json['changeLog'] == null ? null : json['changeLog'] as String, | ||||
|         changeLog: | ||||
|             json['changeLog'] == null ? null : json['changeLog'] as String, | ||||
|         overrideSource: json['overrideSource'], | ||||
|       allowIdChange: json['allowIdChange'] ?? false, | ||||
|       otherAssetUrls: assumed2DlistToStringMapList( | ||||
|           jsonDecode((json['otherAssetUrls'] ?? '[]'))), | ||||
|     ); | ||||
|         allowIdChange: json['allowIdChange'] ?? false); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
| @@ -331,7 +326,6 @@ class App { | ||||
|         'installedVersion': installedVersion, | ||||
|         'latestVersion': latestVersion, | ||||
|         'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)), | ||||
|         'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)), | ||||
|         'preferredApkIndex': preferredApkIndex, | ||||
|         'additionalSettings': jsonEncode(additionalSettings), | ||||
|         'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, | ||||
| @@ -449,16 +443,9 @@ abstract class AppSource { | ||||
|       String url, Map<String, dynamic> additionalSettings, | ||||
|       {bool followRedirects = true}) async { | ||||
|     var requestHeaders = await getRequestHeaders(additionalSettings); | ||||
|     if (requestHeaders != null || followRedirects == false) { | ||||
|       var req = Request('GET', Uri.parse(url)); | ||||
|       req.followRedirects = followRedirects; | ||||
|       if (requestHeaders != null) { | ||||
|         req.headers.addAll(requestHeaders); | ||||
|       } | ||||
|       return Response.fromStream(await Client().send(req)); | ||||
|     } else { | ||||
|       return get(Uri.parse(url)); | ||||
|     } | ||||
|     return await dio.get(url, | ||||
|         options: | ||||
|             Options(headers: requestHeaders, followRedirects: followRedirects)); | ||||
|   } | ||||
|  | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
| @@ -625,10 +612,10 @@ abstract class AppSource { | ||||
| } | ||||
|  | ||||
| ObtainiumError getObtainiumHttpError(Response res) { | ||||
|   return ObtainiumError((res.reasonPhrase != null && | ||||
|           res.reasonPhrase != null && | ||||
|           res.reasonPhrase!.isNotEmpty) | ||||
|       ? res.reasonPhrase! | ||||
|   return ObtainiumError((res.statusMessage != null && | ||||
|           res.statusMessage != null && | ||||
|           res.statusMessage!.isNotEmpty) | ||||
|       ? res.statusMessage! | ||||
|       : tr('errorWithHttpStatusCode', args: [res.statusCode.toString()])); | ||||
| } | ||||
|  | ||||
| @@ -743,6 +730,7 @@ class SourceProvider { | ||||
|         FDroid(), | ||||
|         FDroidRepo(), | ||||
|         IzzyOnDroid(), | ||||
|         SourceForge(), | ||||
|         SourceHut(), | ||||
|         APKPure(), | ||||
|         Aptoide(), | ||||
| @@ -899,10 +887,8 @@ class SourceProvider { | ||||
|         allowIdChange: currentApp?.allowIdChange ?? | ||||
|             trackOnly || | ||||
|                 (source.appIdInferIsOptional && | ||||
|                     inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install | ||||
|         otherAssetUrls: apk.allAssetUrls | ||||
|             .where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0) | ||||
|             .toList()); | ||||
|                     inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install | ||||
|         ); | ||||
|     return source.endOfGetAppChanges(finalApp); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										132
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										132
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -5,10 +5,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: android_intent_plus | ||||
|       sha256: "2bfdbee8d65e7c26f88b66f0a91f2863da4d3596d8a658b4162c8de5cf04b074" | ||||
|       sha256: e1c62bb41c90e15083b7fb84dc327fe90396cc9c1445b55ff1082144fabfb4d9 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.0.2" | ||||
|     version: "4.0.3" | ||||
|   android_package_installer: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -38,10 +38,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: app_links | ||||
|       sha256: "42dc15aecf2618ace4ffb74a2e58a50e45cd1b9f2c17c8f0cafe4c297f08c815" | ||||
|       sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.0.1" | ||||
|     version: "3.5.1" | ||||
|   archive: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -54,10 +54,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: args | ||||
|       sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" | ||||
|       sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.5.0" | ||||
|     version: "2.4.2" | ||||
|   async: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -70,10 +70,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: background_fetch | ||||
|       sha256: dbffec0317ccdef6e2014cb543e147f52441e29c4fcb53dfd23558c4d92ddece | ||||
|       sha256: eb3af263d390d7e68ecb90f2ae984d2bfd96dceb4c7b4f72418dd5383b49de0a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.2" | ||||
|     version: "1.2.4" | ||||
|   boolean_selector: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -126,18 +126,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: connectivity_plus | ||||
|       sha256: ebe15d94de9dd7c31dc2ac54e42780acdf3384b1497c69290c9f3c5b0279fc57 | ||||
|       sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.0.2" | ||||
|     version: "5.0.2" | ||||
|   connectivity_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: connectivity_plus_platform_interface | ||||
|       sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb | ||||
|       sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|     version: "1.2.4" | ||||
|   convert: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -190,10 +190,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: device_info_plus | ||||
|       sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 | ||||
|       sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.1.0" | ||||
|     version: "9.1.2" | ||||
|   device_info_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -202,6 +202,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.0.0" | ||||
|   dio: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dio | ||||
|       sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.4.1" | ||||
|   dynamic_color: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -254,10 +262,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: file_picker | ||||
|       sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03 | ||||
|       sha256: caa6bc229eab3e32eb2f37b53a5f9d22a6981474afd210c512a7546c1e1a04f6 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.0.0+1" | ||||
|     version: "6.2.0" | ||||
|   fixnum: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -299,18 +307,18 @@ packages: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: flutter_lints | ||||
|       sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" | ||||
|       sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.2" | ||||
|     version: "3.0.1" | ||||
|   flutter_local_notifications: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_local_notifications | ||||
|       sha256: a701df4866f9a38bb8e4450a54c143bbeeb0ce2381e7df5a36e1006f3b43bb28 | ||||
|       sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "17.0.1" | ||||
|     version: "16.3.3" | ||||
|   flutter_local_notifications_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -336,18 +344,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_markdown | ||||
|       sha256: "31c12de79262b5431c5492e9c89948aa789158435f707d3519a7fdef6af28af7" | ||||
|       sha256: cb44f7831b23a6bdd0f501718b0d2e8045cbc625a15f668af37ddb80314821db | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.22+1" | ||||
|     version: "0.6.21" | ||||
|   flutter_plugin_android_lifecycle: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_plugin_android_lifecycle | ||||
|       sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" | ||||
|       sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.19" | ||||
|     version: "2.0.17" | ||||
|   flutter_test: | ||||
|     dependency: "direct dev" | ||||
|     description: flutter | ||||
| @@ -426,10 +434,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: js | ||||
|       sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf | ||||
|       sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.1" | ||||
|     version: "0.6.7" | ||||
|   json_annotation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -538,18 +546,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: path_provider | ||||
|       sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 | ||||
|       sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.3" | ||||
|     version: "2.1.2" | ||||
|   path_provider_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider_android | ||||
|       sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d | ||||
|       sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.4" | ||||
|     version: "2.2.2" | ||||
|   path_provider_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -586,10 +594,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: permission_handler | ||||
|       sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" | ||||
|       sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.3.1" | ||||
|     version: "11.3.0" | ||||
|   permission_handler_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -602,10 +610,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_apple | ||||
|       sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 | ||||
|       sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "9.4.4" | ||||
|     version: "9.4.0" | ||||
|   permission_handler_html: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -618,10 +626,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_platform_interface | ||||
|       sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" | ||||
|       sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2.1" | ||||
|     version: "4.2.0" | ||||
|   permission_handler_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -658,10 +666,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointycastle | ||||
|       sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" | ||||
|       sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.8.0" | ||||
|     version: "3.7.4" | ||||
|   provider: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -674,34 +682,34 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: share_plus | ||||
|       sha256: fb5319f3aab4c5dda5ebb92dca978179ba21f8c783ee4380910ef4c1c6824f51 | ||||
|       sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.0.3" | ||||
|     version: "7.2.2" | ||||
|   share_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: share_plus_platform_interface | ||||
|       sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" | ||||
|       sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.4.0" | ||||
|     version: "3.3.1" | ||||
|   shared_preferences: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 | ||||
|       sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.3" | ||||
|     version: "2.2.2" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" | ||||
|       sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.2" | ||||
|     version: "2.2.1" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -775,18 +783,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: sqflite | ||||
|       sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" | ||||
|       sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.3" | ||||
|     version: "2.3.2" | ||||
|   sqflite_common: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_common | ||||
|       sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" | ||||
|       sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.5.4" | ||||
|     version: "2.5.3" | ||||
|   stack_trace: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -855,18 +863,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: url_launcher | ||||
|       sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" | ||||
|       sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.2.6" | ||||
|     version: "6.2.5" | ||||
|   url_launcher_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" | ||||
|       sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.3.1" | ||||
|     version: "6.3.0" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -919,10 +927,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: uuid | ||||
|       sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" | ||||
|       sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.4.0" | ||||
|     version: "4.3.3" | ||||
|   vector_math: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -959,10 +967,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_android | ||||
|       sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934 | ||||
|       sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.16.0" | ||||
|     version: "3.15.0" | ||||
|   webview_flutter_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -975,18 +983,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_wkwebview | ||||
|       sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 | ||||
|       sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.13.0" | ||||
|     version: "3.12.0" | ||||
|   win32: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: win32 | ||||
|       sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" | ||||
|       sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.4.0" | ||||
|     version: "5.3.0" | ||||
|   win32_registry: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
							
								
								
									
										17
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								pubspec.yaml
									
									
									
									
									
								
							| @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||
| # In Windows, build-name is used as the major, minor, and patch parts | ||||
| # of the product and file versions while build-number is used as the build suffix. | ||||
| version: 1.1.3+2260 | ||||
| version: 1.0.5+2255 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=3.0.0 <4.0.0' | ||||
| @@ -38,7 +38,7 @@ dependencies: | ||||
|   cupertino_icons: ^1.0.5 | ||||
|   path_provider: ^2.0.11 | ||||
|   flutter_fgbg: ^0.3.0 # Try removing reliance on this | ||||
|   flutter_local_notifications: ^17.0.0 | ||||
|   flutter_local_notifications: ^16.1.0 | ||||
|   provider: ^6.0.3 | ||||
|   http: ^1.0.0 | ||||
|   webview_flutter: ^4.0.0 | ||||
| @@ -48,26 +48,27 @@ dependencies: | ||||
|   url_launcher: ^6.1.5 | ||||
|   permission_handler: ^11.0.0 | ||||
|   fluttertoast: ^8.0.9 | ||||
|   device_info_plus: ^10.0.1 | ||||
|   file_picker: ^8.0.0+1 | ||||
|   device_info_plus: ^9.0.0 | ||||
|   file_picker: ^6.0.0 | ||||
|   animations: ^2.0.4 | ||||
|   android_package_installer: | ||||
|     git: | ||||
|       url: https://github.com/ImranR98/android_package_installer | ||||
|       ref: main | ||||
|   android_package_manager: ^0.7.0 | ||||
|   share_plus: ^8.0.2 | ||||
|   share_plus: ^7.0.0 | ||||
|   sqflite: ^2.2.0+3 | ||||
|   easy_localization: ^3.0.1 | ||||
|   android_intent_plus: ^5.0.1 | ||||
|   android_intent_plus: ^4.0.0 | ||||
|   flutter_markdown: ^0.6.14 | ||||
|   flutter_archive: ^6.0.0 | ||||
|   hsluv: ^1.1.3 | ||||
|   connectivity_plus: ^6.0.1 | ||||
|   connectivity_plus: ^5.0.0 | ||||
|   shared_storage: ^0.8.0 | ||||
|   crypto: ^3.0.3 | ||||
|   app_links: ^4.0.0 | ||||
|   app_links: ^3.5.0 | ||||
|   background_fetch: ^1.2.1 | ||||
|   dio: ^5.4.1 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user