mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-14 13:46:43 +02:00
Compare commits
74 Commits
v0.12.0-be
...
v0.13.4-be
Author | SHA1 | Date | |
---|---|---|---|
ca1371260c | |||
03c2ce9a01 | |||
eda5fec37c | |||
e21c6297ff | |||
c6297ea449 | |||
e33cc00266 | |||
96c92c8df9 | |||
e256ada2dc | |||
eb0be196da | |||
1606ad3442 | |||
d212f13345 | |||
f80c9ec33e | |||
7681e23de9 | |||
22a60df40e | |||
431a01f2a5 | |||
0cd4385de7 | |||
0774b3ddc3 | |||
b60b1ed058 | |||
b196715d60 | |||
0673e90dff | |||
59cfa242fb | |||
65ab72ba90 | |||
408bca8951 | |||
480467492a | |||
219b04aedb | |||
a0709856ef | |||
577642850f | |||
e1db024034 | |||
cc268aeeda | |||
d5f7eced8b | |||
cc3c4cc79f | |||
89b61884f1 | |||
33d3fc2d8e | |||
b07f5dd6b6 | |||
b43e13bb56 | |||
3be5543df4 | |||
91ad9efa43 | |||
ee292146d1 | |||
12867634b6 | |||
2e4fe89b85 | |||
b4642e16ad | |||
8ca5964d31 | |||
30c89fe385 | |||
fb9e66332d | |||
84b512f282 | |||
6f9aa85a72 | |||
639fc20fcb | |||
75631e5c5a | |||
9ec345761e | |||
1f9c2c1699 | |||
cbec486ad1 | |||
85ef60d4a8 | |||
44bde571bf | |||
eaaee5e7cd | |||
e1980f4de2 | |||
be9c671a56 | |||
0404449842 | |||
d6366a145e | |||
0a751cf545 | |||
5885ea57ad | |||
f8b326529f | |||
9f5f1174ba | |||
779de58f74 | |||
76e316422c | |||
36273fe02d | |||
03b592521c | |||
a5ef47a060 | |||
289c801fec | |||
73d04b1564 | |||
9469d56144 | |||
d063bca474 | |||
7c592756fe | |||
08586870fb | |||
8b123acdcd |
@ -15,8 +15,11 @@ Currently supported App sources:
|
|||||||
- [Mullvad](https://mullvad.net/en/)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
- [Signal](https://signal.org/)
|
- [Signal](https://signal.org/)
|
||||||
- [SourceForge](https://sourceforge.net/)
|
- [SourceForge](https://sourceforge.net/)
|
||||||
|
- [SourceHut](https://git.sr.ht/)
|
||||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||||
|
- [APKPure](https://apkpure.com/)
|
||||||
- Third Party F-Droid Repos
|
- Third Party F-Droid Repos
|
||||||
|
- Jenkins Jobs
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- [Steam](https://store.steampowered.com/mobile)
|
||||||
- [Telegram App](https://telegram.org)
|
- [Telegram App](https://telegram.org)
|
||||||
- [VLC](https://www.videolan.org/vlc/download-android.html)
|
- [VLC](https://www.videolan.org/vlc/download-android.html)
|
||||||
@ -34,7 +37,6 @@ Currently supported App sources:
|
|||||||
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
|
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
|
||||||
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
||||||
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
||||||
|
|
||||||
|
@ -25,6 +25,11 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action
|
||||||
|
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
|
||||||
|
android:exported="false"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
@ -46,9 +51,18 @@
|
|||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="dev.imranr.obtainium"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"/>
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
<paths>
|
<paths>
|
||||||
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
|
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
|
||||||
<external-path path="." name="external_storage_root" />
|
<external-path path="." name="external_storage_root" />
|
||||||
|
<external-path name="external_files" path="."/>
|
||||||
</paths>
|
</paths>
|
@ -26,6 +26,6 @@ subprojects {
|
|||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
|
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
|
||||||
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
|
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
|
||||||
"githubPATFormat": "Benutzername:Token",
|
"githubPATFormat": "Benutzername:Token",
|
||||||
"githubPATLinkText": "Über GitHub PATs",
|
|
||||||
"includePrereleases": "Vorabversionen einbeziehen",
|
"includePrereleases": "Vorabversionen einbeziehen",
|
||||||
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
|
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
|
||||||
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
|
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
|
||||||
@ -71,7 +70,7 @@
|
|||||||
"updateX": "Aktualisiere {}",
|
"updateX": "Aktualisiere {}",
|
||||||
"installX": "Installiere {}",
|
"installX": "Installiere {}",
|
||||||
"markXTrackOnlyAsUpdated": "Markiere {}\n(Nur Nachverfolgen)\nals aktualisiert",
|
"markXTrackOnlyAsUpdated": "Markiere {}\n(Nur Nachverfolgen)\nals aktualisiert",
|
||||||
"changeX": "Ändern {}",
|
"changeX": "Ändere {}",
|
||||||
"installUpdateApps": "Apps installieren/aktualisieren",
|
"installUpdateApps": "Apps installieren/aktualisieren",
|
||||||
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
||||||
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
||||||
@ -122,12 +121,12 @@
|
|||||||
"followSystem": "System folgen",
|
"followSystem": "System folgen",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"useBlackTheme": "Verwende Pure Black Dark Theme",
|
||||||
"appSortBy": "App sortieren nach",
|
"appSortBy": "App sortieren nach",
|
||||||
"authorName": "Autor/Name",
|
"authorName": "Autor/Name",
|
||||||
"nameAuthor": "Name/Autor",
|
"nameAuthor": "Name/Autor",
|
||||||
"asAdded": "Wie hinzugefügt",
|
"asAdded": "Wie hinzugefügt",
|
||||||
"appSortOrder": "App Sortierung nach",
|
"appSortOrder": "App sortieren nach",
|
||||||
"ascending": "Aufsteigend",
|
"ascending": "Aufsteigend",
|
||||||
"descending": "Absteigend",
|
"descending": "Absteigend",
|
||||||
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
|
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
|
||||||
@ -181,6 +180,7 @@
|
|||||||
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "App ID oder Name",
|
"appIdOrName": "App ID oder Name",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
|
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
|
||||||
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
|
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||||
@ -208,7 +208,7 @@
|
|||||||
"addCategory": "Kategorie hinzufügen",
|
"addCategory": "Kategorie hinzufügen",
|
||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"copiedToClipboard": "Copied to Clipboard",
|
"copiedToClipboard": "In die Zwischenablage kopiert",
|
||||||
"storagePermissionDenied": "Speicherberechtigung verweigert",
|
"storagePermissionDenied": "Speicherberechtigung verweigert",
|
||||||
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
|
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
|
||||||
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
|
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
|
||||||
@ -219,15 +219,20 @@
|
|||||||
"releaseDateAsVersionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert, aber ein Veröffentlichungsdatum verfügbar ist.",
|
"releaseDateAsVersionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert, aber ein Veröffentlichungsdatum verfügbar ist.",
|
||||||
"changes": "Änderungen",
|
"changes": "Änderungen",
|
||||||
"releaseDate": "Veröffentlichungsdatum",
|
"releaseDate": "Veröffentlichungsdatum",
|
||||||
"importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)",
|
"importFromURLsInFile": "Importieren von URLs aus Datei (z. B. OPML)",
|
||||||
"versionDetection": "Versionserkennung",
|
"versionDetection": "Versionserkennung",
|
||||||
"standardVersionDetection": "Standardversionserkennung",
|
"standardVersionDetection": "Standardversionserkennung",
|
||||||
"groupByCategory": "Nach Kategorie gruppieren",
|
"groupByCategory": "Nach Kategorie gruppieren",
|
||||||
"autoApkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
|
"autoApkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
|
||||||
"overrideSource": "Override Source",
|
"overrideSource": "Quelle überschreiben",
|
||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Nicht noch einmal zeigen",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
||||||
|
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Aktiviert Suche)",
|
||||||
|
"about": "Über",
|
||||||
|
"requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
||||||
|
"checkOnStart": "Überprüfe einmalig beim Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App entfernen?",
|
"one": "App entfernen?",
|
||||||
"other": "Apps entfernen?"
|
"other": "Apps entfernen?"
|
||||||
@ -254,7 +259,7 @@
|
|||||||
},
|
},
|
||||||
"minute": {
|
"minute": {
|
||||||
"one": "{} Minute",
|
"one": "{} Minute",
|
||||||
"other": "{} Minutes"
|
"other": "{} Minuten"
|
||||||
},
|
},
|
||||||
"hour": {
|
"hour": {
|
||||||
"one": "{} Stunde",
|
"one": "{} Stunde",
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
|
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
|
||||||
"githubPATHint": "PAT must be in this format: username:token",
|
"githubPATHint": "PAT must be in this format: username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "About GitHub PATs",
|
|
||||||
"includePrereleases": "Include prereleases",
|
"includePrereleases": "Include prereleases",
|
||||||
"fallbackToOlderReleases": "Fallback to older releases",
|
"fallbackToOlderReleases": "Fallback to older releases",
|
||||||
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
|
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
|
||||||
@ -122,7 +121,7 @@
|
|||||||
"followSystem": "Follow System",
|
"followSystem": "Follow System",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||||
"appSortBy": "App Sort By",
|
"appSortBy": "App Sort By",
|
||||||
"authorName": "Author/Name",
|
"authorName": "Author/Name",
|
||||||
"nameAuthor": "Name/Author",
|
"nameAuthor": "Name/Author",
|
||||||
@ -181,6 +180,7 @@
|
|||||||
"yesMarkUpdated": "Yes, Mark as Updated",
|
"yesMarkUpdated": "Yes, Mark as Updated",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "App ID or Name",
|
"appIdOrName": "App ID or Name",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
||||||
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||||
@ -228,6 +228,11 @@
|
|||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Don't show this again",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings",
|
"dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remove App?",
|
"one": "Remove App?",
|
||||||
"other": "Remove Apps?"
|
"other": "Remove Apps?"
|
||||||
|
284
assets/translations/es.json
Normal file
284
assets/translations/es.json
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "URL de la aplicación {} no válida",
|
||||||
|
"noReleaseFound": "No se ha podido encontrar una versión válida",
|
||||||
|
"noVersionFound": "No se ha podido determinar la versión de la publicación",
|
||||||
|
"urlMatchesNoSource": "La URL no coincide con ninguna fuente conocida",
|
||||||
|
"cantInstallOlderVersion": "No se puede instalar una versión previa de la aplicación",
|
||||||
|
"appIdMismatch": "La ID del paquete descargado no coincide con la ID de la aplicación instalada",
|
||||||
|
"functionNotImplemented": "Esta clase no ha implementado esta función",
|
||||||
|
"placeholder": "Espacio reservado",
|
||||||
|
"someErrors": "Han ocurrido algunos errores",
|
||||||
|
"unexpectedError": "Error Inesperado",
|
||||||
|
"ok": "Correcto",
|
||||||
|
"and": "y",
|
||||||
|
"startedBgUpdateTask": "Empezada la tarea de comprobación de actualizaciones en segundo plano",
|
||||||
|
"bgUpdateIgnoreAfterIs": "El parámetro ignoreAfter de la actualización en segundo plano es {}",
|
||||||
|
"startedActualBGUpdateCheck": "Ha comenzado la comprobación de actualizaciones en segundo plano",
|
||||||
|
"bgUpdateTaskFinished": "Ha finalizado la comprobación de actualizaciones en segundo plano",
|
||||||
|
"firstRun": "Esta es la primera ejecución de Obtainium",
|
||||||
|
"settingUpdateCheckIntervalTo": "Cambiando intervalo de actualización a {}",
|
||||||
|
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
|
||||||
|
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
|
||||||
|
"githubPATFormat": "nombre_de_usuario:token",
|
||||||
|
"includePrereleases": "Incluir versiones preliminares",
|
||||||
|
"fallbackToOlderReleases": "Retorceder a versiones previas",
|
||||||
|
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
|
||||||
|
"invalidRegEx": "Expresión regular inválida",
|
||||||
|
"noDescription": "Sin descripción",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"continue": "Continuar",
|
||||||
|
"requiredInBrackets": "(Requerido)",
|
||||||
|
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
|
||||||
|
"colour": "Color",
|
||||||
|
"githubStarredRepos": "Repositorios favoritos de GitHub",
|
||||||
|
"uname": "Nombre de usuario",
|
||||||
|
"wrongArgNum": "Número de argumentos provistos inválido",
|
||||||
|
"xIsTrackOnly": "{} es de 'Solo Seguimiento'",
|
||||||
|
"source": "Origen",
|
||||||
|
"app": "Aplicación",
|
||||||
|
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
|
||||||
|
"youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
|
||||||
|
"trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.",
|
||||||
|
"cancelled": "Cancelado",
|
||||||
|
"appAlreadyAdded": "Aplicación ya añadida",
|
||||||
|
"alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
|
||||||
|
"addApp": "Añadir Aplicación",
|
||||||
|
"appSourceURL": "URL de Origen de la Aplicación",
|
||||||
|
"error": "Error",
|
||||||
|
"add": "Añadir",
|
||||||
|
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
|
||||||
|
"search": "Buscar",
|
||||||
|
"additionalOptsFor": "Opciones Adicionales para {}",
|
||||||
|
"supportedSourcesBelow": "Fuentes Soportadas:",
|
||||||
|
"trackOnlyInBrackets": "(Solo Seguimiento)",
|
||||||
|
"searchableInBrackets": "(Soporta Búsquedas)",
|
||||||
|
"appsString": "Aplicaciones",
|
||||||
|
"noApps": "Sin Aplicaciones",
|
||||||
|
"noAppsForFilter": "Sin Aplicaciones para Filtrar",
|
||||||
|
"byX": "Por {}",
|
||||||
|
"percentProgress": "Progreso: {}%",
|
||||||
|
"pleaseWait": "Por favor, espere",
|
||||||
|
"updateAvailable": "Actualización Disponible",
|
||||||
|
"estimateInBracketsShort": "(Aprox.)",
|
||||||
|
"notInstalled": "No Instalado",
|
||||||
|
"estimateInBrackets": "(Aproximado)",
|
||||||
|
"selectAll": "Seleccionar Todo",
|
||||||
|
"deselectN": "Deseleccionar {}",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
|
||||||
|
"removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
|
||||||
|
"removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
|
||||||
|
"updateX": "Actualizar {}",
|
||||||
|
"installX": "Instalar {}",
|
||||||
|
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada",
|
||||||
|
"changeX": "Cambiar {}",
|
||||||
|
"installUpdateApps": "Instalar/Actualizar Aplicaciones",
|
||||||
|
"installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas",
|
||||||
|
"markXSelectedAppsAsUpdated": "¿Marcar {} Aplicaciones Seleccionadas como Actualizadas?",
|
||||||
|
"no": "No",
|
||||||
|
"yes": "Sí",
|
||||||
|
"markSelectedAppsUpdated": "Marcar Aplicaciones Seleccionadas como Actualizadas",
|
||||||
|
"pinToTop": "Fijar arriba",
|
||||||
|
"unpinFromTop": "Desfijar de arriba",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de utilidad cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
|
||||||
|
"shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas",
|
||||||
|
"resetInstallStatus": "Restaurar Estado de Instalación",
|
||||||
|
"more": "Más",
|
||||||
|
"removeOutdatedFilter": "Elimiar Filtro de Aplicaciones Desactualizado",
|
||||||
|
"showOutdatedOnly": "Mostrar solo Aplicaciones Desactualizadas",
|
||||||
|
"filter": "Filtrar",
|
||||||
|
"filterActive": "Filtrar *",
|
||||||
|
"filterApps": "Filtrar Actualizaciones",
|
||||||
|
"appName": "Nombre de la Aplicación",
|
||||||
|
"author": "Autor",
|
||||||
|
"upToDateApps": "Aplicaciones Actualizadas",
|
||||||
|
"nonInstalledApps": "Aplicaciones No Instaladas",
|
||||||
|
"importExport": "Importar/Exportar",
|
||||||
|
"settings": "Ajustes",
|
||||||
|
"exportedTo": "Exportado a {}",
|
||||||
|
"obtainiumExport": "Exportar Obtainium",
|
||||||
|
"invalidInput": "Input incorrecto",
|
||||||
|
"importedX": "Importado {}",
|
||||||
|
"obtainiumImport": "Importar Obtainium",
|
||||||
|
"importFromURLList": "Importar desde lista de URLs",
|
||||||
|
"searchQuery": "Consulta de Búsqueda",
|
||||||
|
"appURLList": "Lista de URLs de Aplicaciones",
|
||||||
|
"line": "Línea",
|
||||||
|
"searchX": "Buscar {}",
|
||||||
|
"noResults": "Resultados no encontrados",
|
||||||
|
"importX": "Importar {}",
|
||||||
|
"importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
|
||||||
|
"importErrors": "Import Errors",
|
||||||
|
"importedXOfYApps": "{} de {} Aplicaciones importadas.",
|
||||||
|
"followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
|
||||||
|
"okay": "Correcto",
|
||||||
|
"selectURL": "Seleccionar URL",
|
||||||
|
"selectURLs": "Seleccionar URLs",
|
||||||
|
"pick": "Escoger",
|
||||||
|
"theme": "Tema",
|
||||||
|
"dark": "Oscuro",
|
||||||
|
"light": "Claro",
|
||||||
|
"followSystem": "Seguir al Sistema",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Usar tema oscuro con negros puros",
|
||||||
|
"appSortBy": "Ordenar Aplicaciones Por",
|
||||||
|
"authorName": "Autor/Nombre",
|
||||||
|
"nameAuthor": "Nombre/Autor",
|
||||||
|
"asAdded": "Según se Añadieron",
|
||||||
|
"appSortOrder": "Orden de Clasificación de Aplicaciones",
|
||||||
|
"ascending": "Ascendente",
|
||||||
|
"descending": "Descendente",
|
||||||
|
"bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
|
||||||
|
"neverManualOnly": "Nunca - Solo Manual",
|
||||||
|
"appearance": "Apariencia",
|
||||||
|
"showWebInAppView": "Mostrar Vista de la Web de Origen",
|
||||||
|
"pinUpdates": "Fijar Actualizaciones en la Parte Superior de la Vista de Aplicaciones",
|
||||||
|
"updates": "Actualizaciones",
|
||||||
|
"sourceSpecific": "Fuente Específica",
|
||||||
|
"appSource": "Fuente de la Aplicación",
|
||||||
|
"noLogs": "Sin Logs",
|
||||||
|
"appLogs": "Logs de la Aplicación",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"share": "Compartir",
|
||||||
|
"appNotFound": "Aplicación no encontrada",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
|
"pickAnAPK": "Elige una APK",
|
||||||
|
"appHasMoreThanOnePackage": "{} tiene más de un paquete:",
|
||||||
|
"deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
|
||||||
|
"deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
|
||||||
|
"warning": "Aviso",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
|
||||||
|
"updatesAvailable": "Actualizaciones Disponibles",
|
||||||
|
"updatesAvailableNotifDescription": "Notifica al usuario de que hay actualizaciones para una o más aplicaciones monitorizadas por Obtainium",
|
||||||
|
"noNewUpdates": "No hay nuevas actualizaciones.",
|
||||||
|
"xHasAnUpdate": "{} tiene una actualización.",
|
||||||
|
"appsUpdated": "Aplicaciones Actualizadas",
|
||||||
|
"appsUpdatedNotifDescription": "Notifica al usuario de que una o más aplicaciones han sido actualizadas en segundo plano",
|
||||||
|
"xWasUpdatedToY": "{} ha sido actualizada a {}.",
|
||||||
|
"errorCheckingUpdates": "Error Buscando Actualizaciones",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "Una notificación que muestra cuándo la comprobación de actualizaciones en segundo plano falla",
|
||||||
|
"appsRemoved": "Aplicaciones Eliminadas",
|
||||||
|
"appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
|
||||||
|
"xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
|
||||||
|
"completeAppInstallation": "Instalación Completa de la Aplicación",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierta para instalar aplicaciones",
|
||||||
|
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación",
|
||||||
|
"checkingForUpdates": "Buscando Actualizaciones",
|
||||||
|
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
|
||||||
|
"pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
|
||||||
|
"trackOnly": "Solo Seguimiento",
|
||||||
|
"errorWithHttpStatusCode": "Error {}",
|
||||||
|
"versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
|
||||||
|
"unknown": "Desconocido",
|
||||||
|
"none": "Ninguno",
|
||||||
|
"never": "Nunca",
|
||||||
|
"latestVersionX": "Última Versión: {}",
|
||||||
|
"installedVersionX": "Versión Instalada: {}",
|
||||||
|
"lastUpdateCheckX": "Última Comprobación: {}",
|
||||||
|
"remove": "Eliminar",
|
||||||
|
"yesMarkUpdated": "Sí, Marcar como Actualizada",
|
||||||
|
"fdroid": "Repositorio oficial de F-Droid",
|
||||||
|
"appIdOrName": "ID o Nombre de la Aplicación",
|
||||||
|
"appId": "ID de la Aplicación",
|
||||||
|
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
|
||||||
|
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
|
||||||
|
"fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "Instalar",
|
||||||
|
"markInstalled": "Marcar como Instalda",
|
||||||
|
"update": "Actualizar",
|
||||||
|
"markUpdated": "Marcar como Actualizada",
|
||||||
|
"additionalOptions": "Opciones Adicionales",
|
||||||
|
"disableVersionDetection": "Descativar Detección de Versiones",
|
||||||
|
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
|
||||||
|
"downloadingX": "Descargando {}",
|
||||||
|
"downloadNotifDescription": "Notifica al usuario de progreso de descarga de una aplicación",
|
||||||
|
"noAPKFound": "APK no encontrada",
|
||||||
|
"noVersionDetection": "Sin detección de versiones",
|
||||||
|
"categorize": "Catogorizar",
|
||||||
|
"categories": "Categorías",
|
||||||
|
"category": "Categoría",
|
||||||
|
"noCategory": "Sin Categoría",
|
||||||
|
"noCategories": "Sin Categorías",
|
||||||
|
"deleteCategoriesQuestion": "¿Borrar Categorías?",
|
||||||
|
"categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán margadas como 'Sin Categoría'.",
|
||||||
|
"addCategory": "Añadir Categoría",
|
||||||
|
"label": "Nombre",
|
||||||
|
"language": "Idioma",
|
||||||
|
"copiedToClipboard": "Copiado al Portapapeles",
|
||||||
|
"storagePermissionDenied": "Permiso de Almacenamiento rechazado",
|
||||||
|
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.",
|
||||||
|
"filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares",
|
||||||
|
"removeFromObtainium": "Eliminar de Obtainium",
|
||||||
|
"uninstallFromDevice": "Desinstalar del Dispositivo",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
|
||||||
|
"releaseDateAsVersion": "Usar Fecha de Publicación como Versión",
|
||||||
|
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
|
||||||
|
"changes": "Cambios",
|
||||||
|
"releaseDate": "Fecha de Publicación",
|
||||||
|
"importFromURLsInFile": "Importar de URls en un Archivo (como OPML)",
|
||||||
|
"versionDetection": "Detección de Versiones",
|
||||||
|
"standardVersionDetection": "Detección de versiones estándar",
|
||||||
|
"groupByCategory": "Agrupar por Categoría",
|
||||||
|
"autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
|
||||||
|
"overrideSource": "Sobrescribir Fuente",
|
||||||
|
"dontShowAgain": "No mostrar de nuevo",
|
||||||
|
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
|
||||||
|
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "¿Eliminar Aplicación?",
|
||||||
|
"other": "¿Eliminar Aplicaciones?"
|
||||||
|
},
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "Muchas peticiones (limitado) - prueba de nuevo en {} minuto",
|
||||||
|
"other": "Muchas peticiones (limitado) - prueba de nuevo en {} minutos"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minuto",
|
||||||
|
"other": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minutos"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualización - se notificará al usuario si es necesario",
|
||||||
|
"other": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualizaciones - se notificará al usuario si es necesario"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{} Aplicación",
|
||||||
|
"other": "{} Aplicaciones"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} URL",
|
||||||
|
"other": "{} URLs"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} Minuto",
|
||||||
|
"other": "{} Minutos"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} Hora",
|
||||||
|
"other": "{} Horas"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} Día",
|
||||||
|
"other": "{} Días"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
|
||||||
|
"other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "{} y 1 aplicación más tiene actualizaciones.",
|
||||||
|
"other": "{} y {} aplicaciones más tiene actualizaciones."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "{} y 1 aplicación más han sido actualizadas.",
|
||||||
|
"other": "{} y {} aplicaciones más han sido actualizadas."
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
||||||
"githubPATHint": "PAT باید در این قالب باشد: username:token",
|
"githubPATHint": "PAT باید در این قالب باشد: username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "درباره گیتهاب PATs",
|
|
||||||
"includePrereleases": "شامل نسخه های اولیه",
|
"includePrereleases": "شامل نسخه های اولیه",
|
||||||
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
||||||
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
||||||
@ -122,7 +121,7 @@
|
|||||||
"followSystem": "هماهنگ با سیستم",
|
"followSystem": "هماهنگ با سیستم",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"useBlackTheme": "استفاده از تم تیره سیاه خالص",
|
||||||
"appSortBy": "مرتب سازی برنامه بر اساس",
|
"appSortBy": "مرتب سازی برنامه بر اساس",
|
||||||
"authorName": "سازنده/اسم",
|
"authorName": "سازنده/اسم",
|
||||||
"nameAuthor": "اسم/سازنده",
|
"nameAuthor": "اسم/سازنده",
|
||||||
@ -181,6 +180,7 @@
|
|||||||
"yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده",
|
"yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "شناسه یا نام برنامه",
|
"appIdOrName": "شناسه یا نام برنامه",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد",
|
"appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد",
|
||||||
"reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد",
|
"reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد",
|
||||||
"fdroidThirdPartyRepo": "مخازن شخص ثالث F-Droid",
|
"fdroidThirdPartyRepo": "مخازن شخص ثالث F-Droid",
|
||||||
@ -224,10 +224,15 @@
|
|||||||
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
||||||
"groupByCategory": "گروه بر اساس دسته",
|
"groupByCategory": "گروه بر اساس دسته",
|
||||||
"autoApkFilterByArch": "در صورت امکان سعی کنید APKها را بر اساس معماری CPU فیلتر کنید",
|
"autoApkFilterByArch": "در صورت امکان سعی کنید APKها را بر اساس معماری CPU فیلتر کنید",
|
||||||
"overrideSource": "Override Source",
|
"overrideSource": "نادیده گرفتن منبع",
|
||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "دوباره این را نشان نده",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "برنامه حذف شود؟",
|
"one": "برنامه حذف شود؟",
|
||||||
"other": "برنامه ها حذف شوند؟"
|
"other": "برنامه ها حذف شوند؟"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)",
|
"githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)",
|
||||||
"githubPATHint": "Le JAP doit être dans ce format : username:token",
|
"githubPATHint": "Le JAP doit être dans ce format : username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "À propos des JAP GitHub",
|
|
||||||
"includePrereleases": "Inclure les avant-premières",
|
"includePrereleases": "Inclure les avant-premières",
|
||||||
"fallbackToOlderReleases": "Retour aux anciennes versions",
|
"fallbackToOlderReleases": "Retour aux anciennes versions",
|
||||||
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
|
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
|
||||||
@ -122,7 +121,7 @@
|
|||||||
"followSystem": "Suivre le système",
|
"followSystem": "Suivre le système",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||||
"appSortBy": "Applications triées par",
|
"appSortBy": "Applications triées par",
|
||||||
"authorName": "Auteur/Nom",
|
"authorName": "Auteur/Nom",
|
||||||
"nameAuthor": "Nom/Auteur",
|
"nameAuthor": "Nom/Auteur",
|
||||||
@ -181,6 +180,7 @@
|
|||||||
"yesMarkUpdated": "Oui, marquer comme mis à jour",
|
"yesMarkUpdated": "Oui, marquer comme mis à jour",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "ID ou nom de l'application",
|
"appIdOrName": "ID ou nom de l'application",
|
||||||
|
"appId": "ID de l'application",
|
||||||
"appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
|
"appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
|
||||||
"reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications",
|
"reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications",
|
||||||
"fdroidThirdPartyRepo": "Dépôt tiers F-Droid",
|
"fdroidThirdPartyRepo": "Dépôt tiers F-Droid",
|
||||||
@ -228,6 +228,11 @@
|
|||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Don't show this again",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Supprimer l'application ?",
|
"one": "Supprimer l'application ?",
|
||||||
"other": "Supprimer les applications ?"
|
"other": "Supprimer les applications ?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
||||||
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
||||||
"githubPATFormat": "felhasználónév:token",
|
"githubPATFormat": "felhasználónév:token",
|
||||||
"githubPATLinkText": "A GitHub PAT-okról",
|
|
||||||
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
|
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
|
||||||
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
|
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
|
||||||
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
|
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
|
||||||
@ -181,6 +180,7 @@
|
|||||||
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "App ID vagy név",
|
"appIdOrName": "App ID vagy név",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
||||||
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo",
|
"fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo",
|
||||||
@ -223,10 +223,15 @@
|
|||||||
"standardVersionDetection": "Alapért. verzió érzékelés",
|
"standardVersionDetection": "Alapért. verzió érzékelés",
|
||||||
"groupByCategory": "Csoportosítás Kategória alapján",
|
"groupByCategory": "Csoportosítás Kategória alapján",
|
||||||
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
|
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
|
||||||
"overrideSource": "Override Source",
|
"overrideSource": "Forrás felülbírálása",
|
||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Ne mutassa ezt újra",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Eltávolítja az alkalmazást?",
|
"one": "Eltávolítja az alkalmazást?",
|
||||||
"other": "Eltávolítja az alkalmazást?"
|
"other": "Eltávolítja az alkalmazást?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
||||||
"githubPATHint": "PAT deve seguire questo formato: username:token",
|
"githubPATHint": "PAT deve seguire questo formato: username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "Informazioni su GitHub PAT",
|
|
||||||
"includePrereleases": "Includi prerelease",
|
"includePrereleases": "Includi prerelease",
|
||||||
"fallbackToOlderReleases": "Ripiega su release precedenti",
|
"fallbackToOlderReleases": "Ripiega su release precedenti",
|
||||||
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
|
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
|
||||||
@ -122,7 +121,7 @@
|
|||||||
"followSystem": "Segui sistema",
|
"followSystem": "Segui sistema",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||||
"appSortBy": "App ordinate per",
|
"appSortBy": "App ordinate per",
|
||||||
"authorName": "Autore/Nome",
|
"authorName": "Autore/Nome",
|
||||||
"nameAuthor": "Nome/Autore",
|
"nameAuthor": "Nome/Autore",
|
||||||
@ -181,6 +180,7 @@
|
|||||||
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "ID o nome dell'App",
|
"appIdOrName": "ID o nome dell'App",
|
||||||
|
"appId": "ID dell'App",
|
||||||
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
|
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
|
||||||
"reposHaveMultipleApps": "I repository possono contenere più App",
|
"reposHaveMultipleApps": "I repository possono contenere più App",
|
||||||
"fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
|
"fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
|
||||||
@ -228,6 +228,11 @@
|
|||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Don't show this again",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Rimuovere l'App?",
|
"one": "Rimuovere l'App?",
|
||||||
"other": "Rimuovere le App?"
|
"other": "Rimuovere le App?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
||||||
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
||||||
"githubPATFormat": "ユーザー名:トークン",
|
"githubPATFormat": "ユーザー名:トークン",
|
||||||
"githubPATLinkText": "GitHub PATsについて",
|
|
||||||
"includePrereleases": "プレリリースを含む",
|
"includePrereleases": "プレリリースを含む",
|
||||||
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
||||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
||||||
@ -122,7 +121,7 @@
|
|||||||
"followSystem": "システムに従う",
|
"followSystem": "システムに従う",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"useBlackTheme": "ピュアブラックダークテーマを使用する",
|
||||||
"appSortBy": "アプリの並び方",
|
"appSortBy": "アプリの並び方",
|
||||||
"authorName": "作者名/アプリ名",
|
"authorName": "作者名/アプリ名",
|
||||||
"nameAuthor": "アプリ名/作者名",
|
"nameAuthor": "アプリ名/作者名",
|
||||||
@ -181,6 +180,7 @@
|
|||||||
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "アプリのIDまたは名前",
|
"appIdOrName": "アプリのIDまたは名前",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
||||||
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
||||||
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
|
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
|
||||||
@ -224,10 +224,15 @@
|
|||||||
"standardVersionDetection": "標準のバージョン検出",
|
"standardVersionDetection": "標準のバージョン検出",
|
||||||
"groupByCategory": "カテゴリ別にグループ化する",
|
"groupByCategory": "カテゴリ別にグループ化する",
|
||||||
"autoApkFilterByArch": "可能であれば,CPUアーキテクチャによるAPKのフィルタリングを試みる",
|
"autoApkFilterByArch": "可能であれば,CPUアーキテクチャによるAPKのフィルタリングを試みる",
|
||||||
"overrideSource": "Override Source",
|
"overrideSource": "ソースの上書き",
|
||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "二度と表示しない",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "APK Originの警告を表示しない",
|
||||||
|
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
|
||||||
|
"gitlabPATLabel": "GitLab パーソナルアクセストークン (検索を有効化する)",
|
||||||
|
"about": "概要",
|
||||||
|
"requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "アプリを削除しますか?",
|
"one": "アプリを削除しますか?",
|
||||||
"other": "アプリを削除しますか?"
|
"other": "アプリを削除しますか?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
||||||
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
|
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "关于 GitHub 个人访问令牌",
|
|
||||||
"includePrereleases": "包含预发行版",
|
"includePrereleases": "包含预发行版",
|
||||||
"fallbackToOlderReleases": "将旧发行版作为备选",
|
"fallbackToOlderReleases": "将旧发行版作为备选",
|
||||||
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
|
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
|
||||||
@ -34,7 +33,7 @@
|
|||||||
"githubStarredRepos": "GitHub 已星标仓库",
|
"githubStarredRepos": "GitHub 已星标仓库",
|
||||||
"uname": "用户名",
|
"uname": "用户名",
|
||||||
"wrongArgNum": "参数数量错误",
|
"wrongArgNum": "参数数量错误",
|
||||||
"xIsTrackOnly": "{} 为“仅追踪”模式",
|
"xIsTrackOnly": "{}为“仅追踪”模式",
|
||||||
"source": "源代码",
|
"source": "源代码",
|
||||||
"app": "应用",
|
"app": "应用",
|
||||||
"appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
|
"appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
|
||||||
@ -51,8 +50,8 @@
|
|||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"additionalOptsFor": "{} 的更多选项",
|
"additionalOptsFor": "{} 的更多选项",
|
||||||
"supportedSourcesBelow": "支持的来源:",
|
"supportedSourcesBelow": "支持的来源:",
|
||||||
"trackOnlyInBrackets": "(仅追踪)",
|
"trackOnlyInBrackets": "(仅追踪)",
|
||||||
"searchableInBrackets": "(可搜索)",
|
"searchableInBrackets": "(可搜索)",
|
||||||
"appsString": "应用列表",
|
"appsString": "应用列表",
|
||||||
"noApps": "无应用",
|
"noApps": "无应用",
|
||||||
"noAppsForFilter": "没有符合条件的应用",
|
"noAppsForFilter": "没有符合条件的应用",
|
||||||
@ -60,9 +59,9 @@
|
|||||||
"percentProgress": "进度:{}%",
|
"percentProgress": "进度:{}%",
|
||||||
"pleaseWait": "请稍候",
|
"pleaseWait": "请稍候",
|
||||||
"updateAvailable": "更新可用",
|
"updateAvailable": "更新可用",
|
||||||
"estimateInBracketsShort": "(预计)",
|
"estimateInBracketsShort": "(推测)",
|
||||||
"notInstalled": "未安装",
|
"notInstalled": "未安装",
|
||||||
"estimateInBrackets": "(预计)",
|
"estimateInBrackets": "(推测)",
|
||||||
"selectAll": "全选",
|
"selectAll": "全选",
|
||||||
"deselectN": "取消选择 {}",
|
"deselectN": "取消选择 {}",
|
||||||
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
|
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
|
||||||
@ -75,8 +74,8 @@
|
|||||||
"installUpdateApps": "安装/更新应用",
|
"installUpdateApps": "安装/更新应用",
|
||||||
"installUpdateSelectedApps": "安装/更新选中的应用",
|
"installUpdateSelectedApps": "安装/更新选中的应用",
|
||||||
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
|
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
|
||||||
"no": "不要",
|
"no": "否",
|
||||||
"yes": "好的",
|
"yes": "是",
|
||||||
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
|
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
|
||||||
"pinToTop": "置顶",
|
"pinToTop": "置顶",
|
||||||
"unpinFromTop": "取消置顶",
|
"unpinFromTop": "取消置顶",
|
||||||
@ -143,7 +142,7 @@
|
|||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"share": "分享",
|
"share": "分享",
|
||||||
"appNotFound": "未找到应用",
|
"appNotFound": "未找到应用",
|
||||||
"obtainiumExportHyphenatedLowercase": "obtainium-导出",
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
"pickAnAPK": "选择一个 APK 文件",
|
"pickAnAPK": "选择一个 APK 文件",
|
||||||
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
||||||
"deviceSupportsXArch": "您的设备支持 {} 架构。",
|
"deviceSupportsXArch": "您的设备支持 {} 架构。",
|
||||||
@ -173,14 +172,15 @@
|
|||||||
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
|
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"none": "无",
|
"none": "无",
|
||||||
"never": "从不",
|
"never": "从未",
|
||||||
"latestVersionX": "最新版本:{}",
|
"latestVersionX": "最新版本:{}",
|
||||||
"installedVersionX": "当前版本:{}",
|
"installedVersionX": "当前版本:{}",
|
||||||
"lastUpdateCheckX": "上次更新检查:{}",
|
"lastUpdateCheckX": "上次更新检查:{}",
|
||||||
"remove": "删除",
|
"remove": "删除",
|
||||||
"yesMarkUpdated": "是的,标记为已更新",
|
"yesMarkUpdated": "是,标记为已更新",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid 官方存储库",
|
||||||
"appIdOrName": "应用 ID 或名称",
|
"appIdOrName": "应用 ID 或名称",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
||||||
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
||||||
"fdroidThirdPartyRepo": "F-Droid 第三方存储库",
|
"fdroidThirdPartyRepo": "F-Droid 第三方存储库",
|
||||||
@ -194,7 +194,7 @@
|
|||||||
"additionalOptions": "附加选项",
|
"additionalOptions": "附加选项",
|
||||||
"disableVersionDetection": "禁用版本检测",
|
"disableVersionDetection": "禁用版本检测",
|
||||||
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
||||||
"downloadingX": "正在下载 {}",
|
"downloadingX": "正在下载{}",
|
||||||
"downloadNotifDescription": "提示应用的下载进度",
|
"downloadNotifDescription": "提示应用的下载进度",
|
||||||
"noAPKFound": "未找到 APK 文件",
|
"noAPKFound": "未找到 APK 文件",
|
||||||
"noVersionDetection": "禁用版本检测",
|
"noVersionDetection": "禁用版本检测",
|
||||||
@ -223,11 +223,16 @@
|
|||||||
"versionDetection": "版本检测",
|
"versionDetection": "版本检测",
|
||||||
"standardVersionDetection": "常规版本检测",
|
"standardVersionDetection": "常规版本检测",
|
||||||
"groupByCategory": "按类别分组显示",
|
"groupByCategory": "按类别分组显示",
|
||||||
"autoApkFilterByArch": "如果可能,尝试按 CPU 架构筛选 APK 文件",
|
"autoApkFilterByArch": "如果可能,尝试按设备支持的 CPU 架构筛选 APK 文件",
|
||||||
"overrideSource": "Override Source",
|
"overrideSource": "覆盖来源",
|
||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "不再显示",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "不显示“仅追踪”模式警告",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "不显示 APK 文件来源警告",
|
||||||
|
"moveNonInstalledAppsToBottom": "将未安装应用置底",
|
||||||
|
"gitlabPATLabel": "GitLab 个人访问令牌(用于搜索)",
|
||||||
|
"about": "相关文档",
|
||||||
|
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
|
||||||
|
"checkOnStart": "启动时进行一次检查",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "是否删除应用?",
|
"one": "是否删除应用?",
|
||||||
"other": "是否删除应用?"
|
"other": "是否删除应用?"
|
||||||
|
114
lib/app_sources/apkcombo.dart
Normal file
114
lib/app_sources/apkcombo.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class APKCombo extends AppSource {
|
||||||
|
APKCombo() {
|
||||||
|
host = 'apkcombo.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/+[^/]+/+[^/]+');
|
||||||
|
var match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||||
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String> get requestHeaders => {
|
||||||
|
"User-Agent": "curl/8.0.1",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Host": "$host"
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
|
||||||
|
var res = await sourceRequest('$standardUrl/download/apk');
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
var html = parse(res.body);
|
||||||
|
return html
|
||||||
|
.querySelectorAll('#variants-tab > div > ul > li')
|
||||||
|
.map((e) {
|
||||||
|
String? arch = e
|
||||||
|
.querySelector('code')
|
||||||
|
?.text
|
||||||
|
.trim()
|
||||||
|
.replaceAll(',', '')
|
||||||
|
.replaceAll(':', '-')
|
||||||
|
.replaceAll(' ', '-');
|
||||||
|
return e.querySelectorAll('a').map((e) {
|
||||||
|
String? url = e.attributes['href'];
|
||||||
|
if (url != null &&
|
||||||
|
!Uri.parse(url).path.toLowerCase().endsWith('.apk')) {
|
||||||
|
url = null;
|
||||||
|
}
|
||||||
|
String verCode =
|
||||||
|
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
|
||||||
|
return MapEntry<String, String>(
|
||||||
|
arch != null ? '$arch-$verCode.apk' : '', url ?? '');
|
||||||
|
}).toList();
|
||||||
|
})
|
||||||
|
.reduce((value, element) => [...value, ...element])
|
||||||
|
.where((element) => element.value.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> apkUrlPrefetchModifier(
|
||||||
|
String apkUrl, String standardUrl) async {
|
||||||
|
var freshURLs = await getApkUrls(standardUrl);
|
||||||
|
var path2Match = Uri.parse(apkUrl).path;
|
||||||
|
for (var url in freshURLs) {
|
||||||
|
if (Uri.parse(url.value).path == path2Match) {
|
||||||
|
return url.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NoAPKError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
String appId = tryInferringAppId(standardUrl)!;
|
||||||
|
var preres = await sourceRequest(standardUrl);
|
||||||
|
if (preres.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(preres);
|
||||||
|
}
|
||||||
|
var res = parse(preres.body);
|
||||||
|
String? version = res.querySelector('div.version')?.text.trim();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String appName = res.querySelector('div.app_name')?.text.trim() ?? appId;
|
||||||
|
String author = res.querySelector('div.author')?.text.trim() ?? appName;
|
||||||
|
List<String> infoArray = res
|
||||||
|
.querySelectorAll('div.information-table > .item > div.value')
|
||||||
|
.map((e) => e.text.trim())
|
||||||
|
.toList();
|
||||||
|
DateTime? releaseDate;
|
||||||
|
if (infoArray.length >= 2) {
|
||||||
|
try {
|
||||||
|
releaseDate = DateFormat('MMMM d, yyyy').parse(infoArray[1]);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APKDetails(
|
||||||
|
version, await getApkUrls(standardUrl), AppNames(author, appName),
|
||||||
|
releaseDate: releaseDate);
|
||||||
|
}
|
||||||
|
}
|
@ -57,7 +57,7 @@ class APKMirror extends AppSource {
|
|||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse('$standardUrl/feed'));
|
Response res = await sourceRequest('$standardUrl/feed');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var items = parse(res.body).querySelectorAll('item');
|
var items = parse(res.body).querySelectorAll('item');
|
||||||
dynamic targetRelease;
|
dynamic targetRelease;
|
||||||
|
77
lib/app_sources/apkpure.dart
Normal file
77
lib/app_sources/apkpure.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class APKPure extends AppSource {
|
||||||
|
APKPure() {
|
||||||
|
host = 'apkpure.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegExB = RegExp('^https?://m.$host/+[^/]+/+[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
||||||
|
if (match != null) {
|
||||||
|
url = 'https://$host/${Uri.parse(url).path}';
|
||||||
|
}
|
||||||
|
RegExp standardUrlRegExA = RegExp('^https?://$host/+[^/]+/+[^/]+');
|
||||||
|
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||||
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
String appId = tryInferringAppId(standardUrl)!;
|
||||||
|
String host = Uri.parse(standardUrl).host;
|
||||||
|
var res = await sourceRequest('$standardUrl/download');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var html = parse(res.body);
|
||||||
|
String? version = html.querySelector('span.info-sdk span')?.text.trim();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? dateString =
|
||||||
|
html.querySelector('span.info-other span.date')?.text.trim();
|
||||||
|
DateTime? releaseDate;
|
||||||
|
try {
|
||||||
|
releaseDate = dateString != null
|
||||||
|
? DateFormat('MMM dd, yyyy').parse(dateString)
|
||||||
|
: null;
|
||||||
|
releaseDate = dateString != null && releaseDate == null
|
||||||
|
? DateFormat('MMMM dd, yyyy').parse(dateString)
|
||||||
|
: null;
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||||
|
List<MapEntry<String, String>> apkUrls = [
|
||||||
|
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
||||||
|
];
|
||||||
|
String author = html
|
||||||
|
.querySelector('span.info-sdk')
|
||||||
|
?.text
|
||||||
|
.trim()
|
||||||
|
.substring(version.length + 4) ??
|
||||||
|
Uri.parse(standardUrl).pathSegments.reversed.last;
|
||||||
|
String appName =
|
||||||
|
html.querySelector('h1.info-title')?.text.trim() ?? appId;
|
||||||
|
return APKDetails(version, apkUrls, AppNames(author, appName),
|
||||||
|
releaseDate: releaseDate);
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,4 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
@ -57,10 +55,10 @@ class Codeberg extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
return gh.getLatestAPKDetailsCommon(
|
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||||
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100',
|
(bool useTagUrl) async {
|
||||||
standardUrl,
|
return 'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
additionalSettings);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
@ -70,7 +68,7 @@ class Codeberg extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>> search(String query) async {
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
return gh.searchCommon(
|
return gh.searchCommon(
|
||||||
query,
|
query,
|
||||||
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -9,6 +10,7 @@ class FDroid extends AppSource {
|
|||||||
FDroid() {
|
FDroid() {
|
||||||
host = 'f-droid.org';
|
host = 'f-droid.org';
|
||||||
name = tr('fdroid');
|
name = tr('fdroid');
|
||||||
|
canSearch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -64,8 +66,37 @@ class FDroid extends AppSource {
|
|||||||
String? appId = tryInferringAppId(standardUrl);
|
String? appId = tryInferringAppId(standardUrl);
|
||||||
String host = Uri.parse(standardUrl).host;
|
String host = Uri.parse(standardUrl).host;
|
||||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await get(Uri.parse('https://$host/api/v1/packages/$appId')),
|
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
||||||
'https://$host/repo/$appId',
|
'https://$host/repo/$appId',
|
||||||
standardUrl);
|
standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
|
Response res = await sourceRequest(
|
||||||
|
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
|
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||||
|
String? url = e.attributes['href'];
|
||||||
|
if (url != null) {
|
||||||
|
try {
|
||||||
|
standardizeUrl(url);
|
||||||
|
} catch (e) {
|
||||||
|
url = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url != null) {
|
||||||
|
urlsWithDescriptions[url] = [
|
||||||
|
e.querySelector('.package-name')?.text.trim() ?? '',
|
||||||
|
e.querySelector('.package-summary')?.text.trim() ??
|
||||||
|
tr('noDescription')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return urlsWithDescriptions;
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -28,7 +27,7 @@ class FDroidRepo extends AppSource {
|
|||||||
if (appIdOrName == null) {
|
if (appIdOrName == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var res = await get(Uri.parse('$standardUrl/index.xml'));
|
var res = await sourceRequest('$standardUrl/index.xml');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var body = parse(res.body);
|
var body = parse(res.body);
|
||||||
var foundApps = body.querySelectorAll('application').where((element) {
|
var foundApps = body.querySelectorAll('application').where((element) {
|
||||||
|
@ -2,8 +2,10 @@ import 'dart:convert';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -34,7 +36,7 @@ class GitHub extends AppSource {
|
|||||||
hint: tr('githubPATFormat'),
|
hint: tr('githubPATFormat'),
|
||||||
belowWidgets: [
|
belowWidgets: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 4,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -43,10 +45,13 @@ class GitHub extends AppSource {
|
|||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('githubPATLinkText'),
|
tr('about'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline, fontSize: 12),
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
))
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -108,7 +113,7 @@ class GitHub extends AppSource {
|
|||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(requestUrl));
|
Response res = await sourceRequest(requestUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
@ -129,7 +134,7 @@ class GitHub extends AppSource {
|
|||||||
? DateTime.parse(rel['published_at'])
|
? DateTime.parse(rel['published_at'])
|
||||||
: null;
|
: null;
|
||||||
releases.sort((a, b) {
|
releases.sort((a, b) {
|
||||||
// See #478
|
// See #478 and #534
|
||||||
if (a == b) {
|
if (a == b) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (a == null) {
|
} else if (a == null) {
|
||||||
@ -137,8 +142,21 @@ class GitHub extends AppSource {
|
|||||||
} else if (b == null) {
|
} else if (b == null) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return getReleaseDateFromRelease(a)!
|
var nameA = a['tag_name'] ?? a['name'];
|
||||||
.compareTo(getReleaseDateFromRelease(b)!);
|
var nameB = b['tag_name'] ?? b['name'];
|
||||||
|
var stdFormats = findStandardFormatsForVersion(nameA, true)
|
||||||
|
.intersection(findStandardFormatsForVersion(nameB, true));
|
||||||
|
if (stdFormats.isNotEmpty) {
|
||||||
|
var reg = RegExp(stdFormats.first);
|
||||||
|
var matchA = reg.firstMatch(nameA);
|
||||||
|
var matchB = reg.firstMatch(nameB);
|
||||||
|
return compareAlphaNumeric(
|
||||||
|
(nameA as String).substring(matchA!.start, matchA.end),
|
||||||
|
(nameB as String).substring(matchB!.start, matchB.end));
|
||||||
|
} else {
|
||||||
|
return getReleaseDateFromRelease(a)!
|
||||||
|
.compareTo(getReleaseDateFromRelease(b)!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
releases = releases.reversed.toList();
|
releases = releases.reversed.toList();
|
||||||
@ -174,7 +192,7 @@ class GitHub extends AppSource {
|
|||||||
if (targetRelease == null) {
|
if (targetRelease == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
String? version = targetRelease['tag_name'];
|
String? version = targetRelease['tag_name'] ?? targetRelease['name'];
|
||||||
DateTime? releaseDate = getReleaseDateFromRelease(targetRelease);
|
DateTime? releaseDate = getReleaseDateFromRelease(targetRelease);
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
@ -194,15 +212,35 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLatestAPKDetailsCommon2(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
Future<String> Function(bool) reqUrlGenerator,
|
||||||
|
dynamic Function(Response)? onHttpErrorCode) async {
|
||||||
|
try {
|
||||||
|
return await getLatestAPKDetailsCommon(
|
||||||
|
await reqUrlGenerator(false), standardUrl, additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode);
|
||||||
|
} catch (err) {
|
||||||
|
if (err is NoReleasesError && additionalSettings['trackOnly'] == true) {
|
||||||
|
return await getLatestAPKDetailsCommon(
|
||||||
|
await reqUrlGenerator(true), standardUrl, additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
return getLatestAPKDetailsCommon(
|
return await getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100',
|
(bool useTagUrl) async {
|
||||||
standardUrl,
|
return 'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
additionalSettings, onHttpErrorCode: (Response res) {
|
}, (Response res) {
|
||||||
rateLimitErrorCheck(res);
|
rateLimitErrorCheck(res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -213,19 +251,21 @@ class GitHub extends AppSource {
|
|||||||
return AppNames(names[0], names[1]);
|
return AppNames(names[0], names[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String>> searchCommon(
|
Future<Map<String, List<String>>> searchCommon(
|
||||||
String query, String requestUrl, String rootProp,
|
String query, String requestUrl, String rootProp,
|
||||||
{Function(Response)? onHttpErrorCode}) async {
|
{Function(Response)? onHttpErrorCode}) async {
|
||||||
Response res = await get(Uri.parse(requestUrl));
|
Response res = await sourceRequest(requestUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, String> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
|
||||||
urlsWithDescriptions.addAll({
|
urlsWithDescriptions.addAll({
|
||||||
e['html_url'] as String:
|
e['html_url'] as String: [
|
||||||
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
e['full_name'] as String,
|
||||||
(e['description'] != null
|
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
||||||
? e['description'] as String
|
(e['description'] != null
|
||||||
: tr('noDescription')))
|
? e['description'] as String
|
||||||
|
: tr('noDescription')))
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return urlsWithDescriptions;
|
return urlsWithDescriptions;
|
||||||
@ -238,7 +278,7 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>> search(String query) async {
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
return searchCommon(
|
return searchCommon(
|
||||||
query,
|
query,
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||||
|
@ -1,14 +1,46 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class GitLab extends AppSource {
|
class GitLab extends AppSource {
|
||||||
GitLab() {
|
GitLab() {
|
||||||
host = 'gitlab.com';
|
host = 'gitlab.com';
|
||||||
|
canSearch = true;
|
||||||
|
|
||||||
|
additionalSourceSpecificSettingFormItems = [
|
||||||
|
GeneratedFormTextField('gitlab-creds',
|
||||||
|
label: tr('gitlabPATLabel'),
|
||||||
|
password: true,
|
||||||
|
required: false,
|
||||||
|
belowWidgets: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(
|
||||||
|
'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token',
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
tr('about'),
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
)
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
@ -28,6 +60,37 @@ class GitLab extends AppSource {
|
|||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> getPATIfAny() async {
|
||||||
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
|
await settingsProvider.initializeSettings();
|
||||||
|
String? creds = settingsProvider
|
||||||
|
.getSettingString(additionalSourceSpecificSettingFormItems[0].key);
|
||||||
|
return creds != null && creds.isNotEmpty ? creds : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
|
String? PAT = await getPATIfAny();
|
||||||
|
if (PAT == null) {
|
||||||
|
throw CredsNeededError(name);
|
||||||
|
}
|
||||||
|
var url =
|
||||||
|
'https://$host/api/v4/search?private_token=$PAT&scope=projects&search=${Uri.encodeQueryComponent(query)}';
|
||||||
|
var res = await sourceRequest(url);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
var json = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
Map<String, List<String>> results = {};
|
||||||
|
for (var element in json) {
|
||||||
|
results['https://$host/${element['path_with_namespace']}'] = [
|
||||||
|
element['name_with_namespace'],
|
||||||
|
element['description'] ?? tr('noDescription')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'$standardUrl/-/releases';
|
'$standardUrl/-/releases';
|
||||||
@ -39,7 +102,7 @@ class GitLab extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
|
Response res = await sourceRequest('$standardUrl/-/tags?format=atom');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var standardUri = Uri.parse(standardUrl);
|
var standardUri = Uri.parse(standardUrl);
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
|
@ -4,7 +4,94 @@ import 'package:http/http.dart';
|
|||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
||||||
|
try {
|
||||||
|
Uri.parse(ambiguousUrl).origin;
|
||||||
|
return ambiguousUrl;
|
||||||
|
} catch (err) {
|
||||||
|
// is relative
|
||||||
|
}
|
||||||
|
var currPathSegments = referenceAbsoluteUrl.path
|
||||||
|
.split('/')
|
||||||
|
.where((element) => element.trim().isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
||||||
|
return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
|
||||||
|
} else if (ambiguousUrl.split('/').length == 1) {
|
||||||
|
return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
|
||||||
|
} else {
|
||||||
|
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareAlphaNumeric(String a, String b) {
|
||||||
|
List<String> aParts = _splitAlphaNumeric(a);
|
||||||
|
List<String> bParts = _splitAlphaNumeric(b);
|
||||||
|
|
||||||
|
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
|
||||||
|
String aPart = aParts[i];
|
||||||
|
String bPart = bParts[i];
|
||||||
|
|
||||||
|
bool aIsNumber = _isNumeric(aPart);
|
||||||
|
bool bIsNumber = _isNumeric(bPart);
|
||||||
|
|
||||||
|
if (aIsNumber && bIsNumber) {
|
||||||
|
int aNumber = int.parse(aPart);
|
||||||
|
int bNumber = int.parse(bPart);
|
||||||
|
int cmp = aNumber.compareTo(bNumber);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
} else if (!aIsNumber && !bIsNumber) {
|
||||||
|
int cmp = aPart.compareTo(bPart);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Alphanumeric strings come before numeric strings
|
||||||
|
return aIsNumber ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aParts.length.compareTo(bParts.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _splitAlphaNumeric(String s) {
|
||||||
|
List<String> parts = [];
|
||||||
|
StringBuffer sb = StringBuffer();
|
||||||
|
|
||||||
|
bool isNumeric = _isNumeric(s[0]);
|
||||||
|
sb.write(s[0]);
|
||||||
|
|
||||||
|
for (int i = 1; i < s.length; i++) {
|
||||||
|
bool currentIsNumeric = _isNumeric(s[i]);
|
||||||
|
if (currentIsNumeric == isNumeric) {
|
||||||
|
sb.write(s[i]);
|
||||||
|
} else {
|
||||||
|
parts.add(sb.toString());
|
||||||
|
sb.clear();
|
||||||
|
sb.write(s[i]);
|
||||||
|
isNumeric = currentIsNumeric;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.add(sb.toString());
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isNumeric(String s) {
|
||||||
|
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
|
||||||
|
}
|
||||||
|
|
||||||
class HTML extends AppSource {
|
class HTML extends AppSource {
|
||||||
|
@override
|
||||||
|
// TODO: implement requestHeaders choice, hardcoded for now
|
||||||
|
Map<String, String>? get requestHeaders => {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return url;
|
return url;
|
||||||
@ -16,14 +103,16 @@ class HTML extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var uri = Uri.parse(standardUrl);
|
var uri = Uri.parse(standardUrl);
|
||||||
Response res = await get(uri);
|
Response res = await sourceRequest(standardUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
List<String> links = parse(res.body)
|
List<String> links = parse(res.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((element) => element.attributes['href'] ?? '')
|
.map((element) => element.attributes['href'] ?? '')
|
||||||
.where((element) => element.toLowerCase().endsWith('.apk'))
|
.where((element) =>
|
||||||
|
Uri.parse(element).path.toLowerCase().endsWith('.apk'))
|
||||||
.toList();
|
.toList();
|
||||||
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
|
links.sort(
|
||||||
|
(a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last));
|
||||||
if (additionalSettings['apkFilterRegEx'] != null) {
|
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||||
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||||
links = links.where((element) => reg.hasMatch(element)).toList();
|
links = links.where((element) => reg.hasMatch(element)).toList();
|
||||||
@ -34,25 +123,8 @@ class HTML extends AppSource {
|
|||||||
var rel = links.last;
|
var rel = links.last;
|
||||||
var apkName = rel.split('/').last;
|
var apkName = rel.split('/').last;
|
||||||
var version = apkName.substring(0, apkName.length - 4);
|
var version = apkName.substring(0, apkName.length - 4);
|
||||||
List<String> apkUrls = [rel].map((e) {
|
List<String> apkUrls =
|
||||||
try {
|
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
|
||||||
Uri.parse(e).origin;
|
|
||||||
return e;
|
|
||||||
} catch (err) {
|
|
||||||
// is relative
|
|
||||||
}
|
|
||||||
var currPathSegments = uri.path
|
|
||||||
.split('/')
|
|
||||||
.where((element) => element.trim().isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
if (e.startsWith('/') || currPathSegments.isEmpty) {
|
|
||||||
return '${uri.origin}/$e';
|
|
||||||
} else if (e.split('/').length == 1) {
|
|
||||||
return '${uri.origin}/${currPathSegments.join('/')}/$e';
|
|
||||||
} else {
|
|
||||||
return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e';
|
|
||||||
}
|
|
||||||
}).toList();
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
|
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -31,8 +30,8 @@ class IzzyOnDroid extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
String? appId = tryInferringAppId(standardUrl);
|
String? appId = tryInferringAppId(standardUrl);
|
||||||
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await get(
|
await sourceRequest(
|
||||||
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
|
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
|
||||||
'https://android.izzysoft.de/frepo/$appId',
|
'https://android.izzysoft.de/frepo/$appId',
|
||||||
standardUrl);
|
standardUrl);
|
||||||
}
|
}
|
||||||
|
66
lib/app_sources/jenkins.dart
Normal file
66
lib/app_sources/jenkins.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class Jenkins extends AppSource {
|
||||||
|
Jenkins() {
|
||||||
|
overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimJobUrl(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('.*/job/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
|
'$standardUrl/-/releases';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
standardUrl = trimJobUrl(standardUrl);
|
||||||
|
Response res =
|
||||||
|
await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var json = jsonDecode(res.body);
|
||||||
|
var releaseDate = json['timestamp'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
||||||
|
var version =
|
||||||
|
json['number'] == null ? null : (json['number'] as int).toString();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
var apkUrls = (json['artifacts'] as List<dynamic>)
|
||||||
|
.map((e) {
|
||||||
|
var path = (e['relativePath'] as String?);
|
||||||
|
if (path != null && path.isNotEmpty) {
|
||||||
|
path = '$standardUrl/lastSuccessfulBuild/artifact/$path';
|
||||||
|
}
|
||||||
|
return path == null
|
||||||
|
? const MapEntry<String, String>('', '')
|
||||||
|
: MapEntry<String, String>(
|
||||||
|
(e['fileName'] ?? e['relativePath']) as String, path);
|
||||||
|
})
|
||||||
|
.where((url) =>
|
||||||
|
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
return APKDetails(
|
||||||
|
version,
|
||||||
|
apkUrls,
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ class Mullvad extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('$standardUrl/en/download/android'));
|
Response res = await sourceRequest('$standardUrl/en/download/android');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var versions = parse(res.body)
|
var versions = parse(res.body)
|
||||||
.querySelectorAll('p')
|
.querySelectorAll('p')
|
||||||
|
@ -78,7 +78,7 @@ class NeutronCode extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse(standardUrl));
|
Response res = await sourceRequest(standardUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var name = http.querySelector('.pd-title')?.innerHtml;
|
var name = http.querySelector('.pd-title')?.innerHtml;
|
||||||
|
@ -19,7 +19,7 @@ class Signal extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res =
|
Response res =
|
||||||
await get(Uri.parse('https://updates.$host/android/latest.json'));
|
await sourceRequest('https://updates.$host/android/latest.json');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
String? apkUrl = json['url'];
|
String? apkUrl = json['url'];
|
||||||
|
@ -10,8 +10,14 @@ class SourceForge extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
|
RegExp standardUrlRegExB = RegExp('^https?://$host/p/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
||||||
|
if (match != null) {
|
||||||
|
url =
|
||||||
|
'https://${Uri.parse(url.substring(0, match.end)).host}/projects/${url.substring(Uri.parse(url.substring(0, match.end)).host.length + '/projects/'.length + 1)}';
|
||||||
|
}
|
||||||
|
RegExp standardUrlRegExA = RegExp('^https?://$host/projects/[^/]+');
|
||||||
|
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
@ -23,7 +29,7 @@ class SourceForge extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('$standardUrl/rss?path=/'));
|
Response res = await sourceRequest('$standardUrl/rss?path=/');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
var allDownloadLinks =
|
var allDownloadLinks =
|
||||||
|
107
lib/app_sources/sourcehut.dart
Normal file
107
lib/app_sources/sourcehut.dart
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
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';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
|
class SourceHut extends AppSource {
|
||||||
|
SourceHut() {
|
||||||
|
host = 'git.sr.ht';
|
||||||
|
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
Uri standardUri = Uri.parse(standardUrl);
|
||||||
|
String appName = standardUri.pathSegments.last;
|
||||||
|
bool fallbackToOlderReleases =
|
||||||
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
|
Response res = await sourceRequest('$standardUrl/refs/rss.xml');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var parsedHtml = parse(res.body);
|
||||||
|
List<APKDetails> apkDetailsList = [];
|
||||||
|
int ind = 0;
|
||||||
|
|
||||||
|
for (var entry in parsedHtml.querySelectorAll('item').sublist(0, 6)) {
|
||||||
|
// Limit 5 for speed
|
||||||
|
if (!fallbackToOlderReleases && ind > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
String? version = entry.querySelector('title')?.text.trim();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? releaseDateString = entry.querySelector('pubDate')?.innerHtml;
|
||||||
|
String releasePage = '$standardUrl/refs/$version';
|
||||||
|
DateTime? releaseDate;
|
||||||
|
try {
|
||||||
|
releaseDate = releaseDateString != null
|
||||||
|
? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
||||||
|
: null;
|
||||||
|
releaseDate = releaseDateString != null
|
||||||
|
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z')
|
||||||
|
.parse(releaseDateString)
|
||||||
|
: null;
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
var res2 = await sourceRequest(releasePage);
|
||||||
|
List<MapEntry<String, String>> apkUrls = [];
|
||||||
|
if (res2.statusCode == 200) {
|
||||||
|
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
|
.where((e) => e.toLowerCase().endsWith('.apk'))
|
||||||
|
.map((e) => ensureAbsoluteUrl(e, standardUri))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
apkDetailsList.add(APKDetails(
|
||||||
|
version,
|
||||||
|
apkUrls,
|
||||||
|
AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName,
|
||||||
|
appName),
|
||||||
|
releaseDate: releaseDate));
|
||||||
|
ind++;
|
||||||
|
}
|
||||||
|
if (apkDetailsList.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
if (fallbackToOlderReleases) {
|
||||||
|
if (additionalSettings['trackOnly'] != true) {
|
||||||
|
apkDetailsList =
|
||||||
|
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
||||||
|
}
|
||||||
|
if (apkDetailsList.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apkDetailsList.first;
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ class SteamMobile extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('https://$host/mobile'));
|
Response res = await sourceRequest('https://$host/mobile');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var apkNamePrefix = additionalSettings['app'] as String?;
|
var apkNamePrefix = additionalSettings['app'] as String?;
|
||||||
if (apkNamePrefix == null) {
|
if (apkNamePrefix == null) {
|
||||||
|
@ -20,7 +20,7 @@ class TelegramApp extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK'));
|
Response res = await sourceRequest('https://t.me/s/TAndroidAPK');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var messages =
|
var messages =
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@ -19,8 +18,8 @@ class VLC extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(
|
Response res = await sourceRequest(
|
||||||
Uri.parse('https://www.videolan.org/vlc/download-android.html'));
|
'https://www.videolan.org/vlc/download-android.html');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var dwUrlBase = 'get.videolan.org/vlc-android';
|
var dwUrlBase = 'get.videolan.org/vlc-android';
|
||||||
var dwLinks = parse(res.body)
|
var dwLinks = parse(res.body)
|
||||||
@ -38,7 +37,7 @@ class VLC extends AppSource {
|
|||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? targetUrl = 'https://$dwUrlBase/$version/';
|
String? targetUrl = 'https://$dwUrlBase/$version/';
|
||||||
Response res2 = await get(Uri.parse(targetUrl));
|
Response res2 = await sourceRequest(targetUrl);
|
||||||
String mirrorDwBase =
|
String mirrorDwBase =
|
||||||
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
|
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
|
||||||
List<String> apkUrls = [];
|
List<String> apkUrls = [];
|
||||||
|
@ -14,21 +14,21 @@ class WhatsApp extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
|
String apkUrl, String standardUrl) async {
|
||||||
|
Response res = await sourceRequest('https://www.whatsapp.com/android');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var targetLinks = parse(res.body)
|
var targetLinks = parse(res.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((e) => e.attributes['href'])
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
.where((e) => e != null)
|
.where((e) => e.isNotEmpty)
|
||||||
.where((e) =>
|
.where((e) =>
|
||||||
e!.contains('scontent.whatsapp.net') &&
|
e.contains('content.whatsapp.net') && e.contains('WhatsApp.apk'))
|
||||||
e.contains('WhatsApp.apk'))
|
|
||||||
.toList();
|
.toList();
|
||||||
if (targetLinks.isEmpty) {
|
if (targetLinks.isEmpty) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
return targetLinks[0]!;
|
return targetLinks[0];
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ class WhatsApp extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
|
Response res = await sourceRequest('https://www.whatsapp.com/android');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var targetElements = parse(res.body)
|
var targetElements = parse(res.body)
|
||||||
.querySelectorAll('p')
|
.querySelectorAll('p')
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:android_package_installer/android_package_installer.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
@ -24,6 +25,11 @@ class InvalidURLError extends ObtainiumError {
|
|||||||
: super(tr('invalidURLForSource', args: [sourceName]));
|
: super(tr('invalidURLForSource', args: [sourceName]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CredsNeededError extends ObtainiumError {
|
||||||
|
CredsNeededError(String sourceName)
|
||||||
|
: super(tr('requiresCredentialsInSettings', args: [sourceName]));
|
||||||
|
}
|
||||||
|
|
||||||
class NoReleasesError extends ObtainiumError {
|
class NoReleasesError extends ObtainiumError {
|
||||||
NoReleasesError() : super(tr('noReleaseFound'));
|
NoReleasesError() : super(tr('noReleaseFound'));
|
||||||
}
|
}
|
||||||
@ -44,6 +50,11 @@ class DowngradeError extends ObtainiumError {
|
|||||||
DowngradeError() : super(tr('cantInstallOlderVersion'));
|
DowngradeError() : super(tr('cantInstallOlderVersion'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InstallError extends ObtainiumError {
|
||||||
|
InstallError(int code)
|
||||||
|
: super(PackageInstallerStatus.byCode(code).name.substring(7));
|
||||||
|
}
|
||||||
|
|
||||||
class IDChangedError extends ObtainiumError {
|
class IDChangedError extends ObtainiumError {
|
||||||
IDChangedError() : super(tr('appIdMismatch'));
|
IDChangedError() : super(tr('appIdMismatch'));
|
||||||
}
|
}
|
||||||
|
@ -21,21 +21,22 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
|
|||||||
// ignore: implementation_imports
|
// ignore: implementation_imports
|
||||||
import 'package:easy_localization/src/localization.dart';
|
import 'package:easy_localization/src/localization.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.12.0';
|
const String currentVersion = '0.13.4';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
const int bgUpdateCheckAlarmId = 666;
|
const int bgUpdateCheckAlarmId = 666;
|
||||||
|
|
||||||
const supportedLocales = [
|
List<MapEntry<Locale, String>> supportedLocales = const [
|
||||||
Locale('en'),
|
MapEntry(Locale('en'), 'English'),
|
||||||
Locale('zh'),
|
MapEntry(Locale('zh'), '汉语'),
|
||||||
Locale('it'),
|
MapEntry(Locale('it'), 'Italiano'),
|
||||||
Locale('ja'),
|
MapEntry(Locale('ja'), '日本語'),
|
||||||
Locale('hu'),
|
MapEntry(Locale('hu'), 'Magyar'),
|
||||||
Locale('de'),
|
MapEntry(Locale('de'), 'Deutsch'),
|
||||||
Locale('fa'),
|
MapEntry(Locale('fa'), 'فارسی'),
|
||||||
Locale('fr')
|
MapEntry(Locale('fr'), 'Français'),
|
||||||
|
MapEntry(Locale('es'), 'Español'),
|
||||||
];
|
];
|
||||||
const fallbackLocale = Locale('en');
|
const fallbackLocale = Locale('en');
|
||||||
const localeDir = 'assets/translations';
|
const localeDir = 'assets/translations';
|
||||||
@ -52,7 +53,7 @@ Future<void> loadTranslations() async {
|
|||||||
saveLocale: true,
|
saveLocale: true,
|
||||||
forceLocale: forceLocale != null ? Locale(forceLocale) : null,
|
forceLocale: forceLocale != null ? Locale(forceLocale) : null,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
supportedLocales: supportedLocales,
|
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||||
assetLoader: const RootBundleAssetLoader(),
|
assetLoader: const RootBundleAssetLoader(),
|
||||||
useOnlyLangCode: true,
|
useOnlyLangCode: true,
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
@ -171,7 +172,7 @@ void main() async {
|
|||||||
Provider(create: (context) => LogsProvider())
|
Provider(create: (context) => LogsProvider())
|
||||||
],
|
],
|
||||||
child: EasyLocalization(
|
child: EasyLocalization(
|
||||||
supportedLocales: supportedLocales,
|
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||||
path: localeDir,
|
path: localeDir,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
useOnlyLangCode: true,
|
useOnlyLangCode: true,
|
||||||
@ -221,7 +222,7 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
], onlyIfExists: false);
|
], onlyIfExists: false);
|
||||||
}
|
}
|
||||||
if (!supportedLocales
|
if (!supportedLocales
|
||||||
.map((e) => e.languageCode)
|
.map((e) => e.key.languageCode)
|
||||||
.contains(context.locale.languageCode) ||
|
.contains(context.locale.languageCode) ||
|
||||||
settingsProvider.forcedLocale == null &&
|
settingsProvider.forcedLocale == null &&
|
||||||
context.deviceLocale.languageCode !=
|
context.deviceLocale.languageCode !=
|
||||||
|
@ -13,17 +13,20 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
@override
|
@override
|
||||||
late List<String> requiredArgs = [tr('uname')];
|
late List<String> requiredArgs = [tr('uname')];
|
||||||
|
|
||||||
Future<Map<String, String>> getOnePageOfUserStarredUrlsWithDescriptions(
|
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
String username, int page) async {
|
String username, int page) async {
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, String> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||||
urlsWithDescriptions.addAll({
|
urlsWithDescriptions.addAll({
|
||||||
e['html_url'] as String: e['description'] != null
|
e['html_url'] as String: [
|
||||||
? e['description'] as String
|
e['full_name'] as String,
|
||||||
: tr('noDescription')
|
e['description'] != null
|
||||||
|
? e['description'] as String
|
||||||
|
: tr('noDescription')
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return urlsWithDescriptions;
|
return urlsWithDescriptions;
|
||||||
@ -35,11 +38,12 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args) async {
|
Future<Map<String, List<String>>> getUrlsWithDescriptions(
|
||||||
|
List<String> args) async {
|
||||||
if (args.length != requiredArgs.length) {
|
if (args.length != requiredArgs.length) {
|
||||||
throw ObtainiumError(tr('wrongArgNum'));
|
throw ObtainiumError(tr('wrongArgNum'));
|
||||||
}
|
}
|
||||||
Map<String, String> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
var page = 1;
|
var page = 1;
|
||||||
while (true) {
|
while (true) {
|
||||||
var pageUrls =
|
var pageUrls =
|
||||||
|
@ -159,14 +159,23 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
app.preferredApkIndex =
|
app.preferredApkIndex =
|
||||||
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var downloadedApk = await appsProvider.downloadApp(
|
var downloadedArtifact = await appsProvider.downloadApp(
|
||||||
app, globalNavigatorKey.currentContext);
|
app, globalNavigatorKey.currentContext);
|
||||||
app.id = downloadedApk.appId;
|
DownloadedApk? downloadedFile;
|
||||||
|
DownloadedXApkDir? downloadedDir;
|
||||||
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
|
downloadedFile = downloadedArtifact;
|
||||||
|
} else {
|
||||||
|
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||||
|
}
|
||||||
|
app.id = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||||
}
|
}
|
||||||
if (appsProvider.apps.containsKey(app.id)) {
|
if (appsProvider.apps.containsKey(app.id)) {
|
||||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||||
}
|
}
|
||||||
if (app.additionalSettings['trackOnly'] == true) {
|
if (app.additionalSettings['trackOnly'] == true ||
|
||||||
|
app.additionalSettings['versionDetection'] !=
|
||||||
|
'standardVersionDetection') {
|
||||||
app.installedVersion = app.latestVersion;
|
app.installedVersion = app.latestVersion;
|
||||||
}
|
}
|
||||||
app.categories = pickedCategories;
|
app.categories = pickedCategories;
|
||||||
@ -246,13 +255,22 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
searching = true;
|
searching = true;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
var results = await Future.wait(sourceProvider.sources
|
var results = await Future.wait(
|
||||||
.where((e) => e.canSearch)
|
sourceProvider.sources.where((e) => e.canSearch).map((e) async {
|
||||||
.map((e) => e.search(searchQuery)));
|
try {
|
||||||
|
return await e.search(searchQuery);
|
||||||
|
} catch (err) {
|
||||||
|
if (err is! CredsNeededError) {
|
||||||
|
rethrow;
|
||||||
|
} else {
|
||||||
|
return <String, List<String>>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// .then((results) async {
|
// .then((results) async {
|
||||||
// Interleave results instead of simple reduce
|
// Interleave results instead of simple reduce
|
||||||
Map<String, String> res = {};
|
Map<String, List<String>> res = {};
|
||||||
var si = 0;
|
var si = 0;
|
||||||
var done = false;
|
var done = false;
|
||||||
while (!done) {
|
while (!done) {
|
||||||
@ -265,6 +283,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
si++;
|
si++;
|
||||||
}
|
}
|
||||||
|
if (res.isEmpty) {
|
||||||
|
throw ObtainiumError(tr('noResults'));
|
||||||
|
}
|
||||||
List<String>? selectedUrls = res.isEmpty
|
List<String>? selectedUrls = res.isEmpty
|
||||||
? []
|
? []
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
@ -327,8 +348,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 25,
|
height: 16,
|
||||||
),
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
bool shouldShowSearchBar() =>
|
bool shouldShowSearchBar() =>
|
||||||
@ -357,33 +378,33 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
searching
|
||||||
onPressed: searchQuery.isEmpty || doingSomething
|
? const CircularProgressIndicator()
|
||||||
? null
|
: ElevatedButton(
|
||||||
: () {
|
onPressed: searchQuery.isEmpty || doingSomething
|
||||||
runSearch();
|
? null
|
||||||
},
|
: () {
|
||||||
child: Text(tr('search')))
|
runSearch();
|
||||||
|
},
|
||||||
|
child: Text(tr('search')))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget getAdditionalOptsCol() => Column(
|
Widget getAdditionalOptsCol() => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Divider(
|
const SizedBox(
|
||||||
height: 64,
|
height: 16,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
tr('additionalOptsFor',
|
tr('additionalOptsFor',
|
||||||
args: [pickedSource?.name ?? tr('source')]),
|
args: [pickedSource?.name ?? tr('source')]),
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
if (pickedSourceOverride != null ||
|
|
||||||
pickedSource.runtimeType.toString() ==
|
|
||||||
HTML().runtimeType.toString())
|
|
||||||
getHTMLSourceOverrideDropdown(),
|
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
key: Key(pickedSource.runtimeType.toString()),
|
key: Key(pickedSource.runtimeType.toString()),
|
||||||
items: pickedSource!.combinedAppSpecificSettingFormItems,
|
items: pickedSource!.combinedAppSpecificSettingFormItems,
|
||||||
@ -457,11 +478,15 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
if (shouldShowSearchBar())
|
if (pickedSourceOverride != null ||
|
||||||
const SizedBox(
|
(pickedSource != null &&
|
||||||
height: 16,
|
pickedSource.runtimeType.toString() ==
|
||||||
),
|
HTML().runtimeType.toString()))
|
||||||
|
getHTMLSourceOverrideDropdown(),
|
||||||
if (shouldShowSearchBar()) getSearchBarRow(),
|
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
if (pickedSource != null)
|
if (pickedSource != null)
|
||||||
getAdditionalOptsCol()
|
getAdditionalOptsCol()
|
||||||
else
|
else
|
||||||
|
@ -32,6 +32,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
getUpdate(String id) {
|
getUpdate(String id) {
|
||||||
appsProvider.checkUpdate(id).catchError((e) {
|
appsProvider.checkUpdate(id).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,7 +445,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: app!.downloadProgress! / 100))
|
value: app!.downloadProgress! >= 0
|
||||||
|
? app.downloadProgress! / 100
|
||||||
|
: null))
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -52,14 +52,37 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
|
||||||
|
GlobalKey<RefreshIndicatorState>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var appsProvider = context.watch<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
var settingsProvider = context.watch<SettingsProvider>();
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
var sourceProvider = SourceProvider();
|
var sourceProvider = SourceProvider();
|
||||||
var listedApps = appsProvider.getAppValues().toList();
|
var listedApps = appsProvider.getAppValues().toList();
|
||||||
var currentFilterIsUpdatesOnly =
|
|
||||||
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
refresh() {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = DateTime.now();
|
||||||
|
});
|
||||||
|
return appsProvider.checkUpdates().catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
|
return <App>[];
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appsProvider.loadingApps &&
|
||||||
|
appsProvider.apps.isNotEmpty &&
|
||||||
|
settingsProvider.checkJustStarted() &&
|
||||||
|
settingsProvider.checkOnStart) {
|
||||||
|
_refreshIndicatorKey.currentState?.show();
|
||||||
|
}
|
||||||
|
|
||||||
selectedAppIds = selectedAppIds
|
selectedAppIds = selectedAppIds
|
||||||
.where((element) => listedApps.map((e) => e.app.id).contains(element))
|
.where((element) => listedApps.map((e) => e.app.id).contains(element))
|
||||||
@ -104,6 +127,11 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (filter.idFilter.isNotEmpty) {
|
||||||
|
if (!app.app.id.contains(filter.idFilter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (filter.categoryFilter.isNotEmpty &&
|
if (filter.categoryFilter.isNotEmpty &&
|
||||||
filter.categoryFilter
|
filter.categoryFilter
|
||||||
.intersection(app.app.categories.toSet())
|
.intersection(app.app.categories.toSet())
|
||||||
@ -185,6 +213,18 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
listedApps = [...temp, ...listedApps];
|
listedApps = [...temp, ...listedApps];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsProvider.buryNonInstalled) {
|
||||||
|
var temp = [];
|
||||||
|
listedApps = listedApps.where((sa) {
|
||||||
|
if (sa.app.installedVersion == null) {
|
||||||
|
temp.add(sa);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
listedApps = [...listedApps, ...temp];
|
||||||
|
}
|
||||||
|
|
||||||
var tempPinned = [];
|
var tempPinned = [];
|
||||||
var tempNotPinned = [];
|
var tempNotPinned = [];
|
||||||
for (var a in listedApps) {
|
for (var a in listedApps) {
|
||||||
@ -303,7 +343,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
?.isBefore(refreshingSince!) ??
|
?.isBefore(refreshingSince!) ??
|
||||||
true))
|
true))
|
||||||
.length /
|
.length /
|
||||||
appsProvider.apps.length,
|
(appsProvider.apps.isNotEmpty ? appsProvider.apps.length : 1),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
@ -343,6 +383,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
[listedApps[appIndex].app.id],
|
[listedApps[appIndex].app.id],
|
||||||
globalNavigatorKey.currentContext).catchError((e) {
|
globalNavigatorKey.currentContext).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
return <String>[];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@ -405,37 +446,35 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
width: 10,
|
width: 10,
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
Column(
|
GestureDetector(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
onTap: showChangesFn,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: MediaQuery.of(context).size.width / 4),
|
|
||||||
child: Text(
|
|
||||||
getVersionText(index),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
)),
|
|
||||||
]),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
onTap: showChangesFn,
|
Container(
|
||||||
child: Text(
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width / 4),
|
||||||
|
child: Text(getVersionText(index),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.end)),
|
||||||
|
]),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
getChangesButtonString(index, showChangesFn != null),
|
getChangesButtonString(index, showChangesFn != null),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
decoration: showChangesFn != null
|
decoration: showChangesFn != null
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: TextDecoration.none),
|
: TextDecoration.none),
|
||||||
))
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
))
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -503,10 +542,21 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal)),
|
: FontWeight.normal)),
|
||||||
trailing: listedApps[index].downloadProgress != null
|
trailing: listedApps[index].downloadProgress != null
|
||||||
? Text(tr('percentProgress', args: [
|
? SizedBox(
|
||||||
listedApps[index].downloadProgress?.toInt().toString() ??
|
width: 90,
|
||||||
'100'
|
child: Text(
|
||||||
]))
|
listedApps[index].downloadProgress! >= 0
|
||||||
|
? tr('percentProgress', args: [
|
||||||
|
listedApps[index]
|
||||||
|
.downloadProgress!
|
||||||
|
.toInt()
|
||||||
|
.toString()
|
||||||
|
])
|
||||||
|
: tr('pleaseWait'),
|
||||||
|
textAlign: (listedApps[index].downloadProgress! >= 0)
|
||||||
|
? TextAlign.start
|
||||||
|
: TextAlign.end,
|
||||||
|
))
|
||||||
: trailingRow,
|
: trailingRow,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (selectedAppIds.isNotEmpty) {
|
if (selectedAppIds.isNotEmpty) {
|
||||||
@ -639,6 +689,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
settingsProvider: settingsProvider)
|
settingsProvider: settingsProvider)
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
return <String>[];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -834,44 +885,41 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMainBottomButtonsRow() {
|
getMainBottomButtons() {
|
||||||
return Row(
|
return [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
IconButton(
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: selectedAppIds.isEmpty
|
onPressed: getMassObtainFunction(),
|
||||||
? null
|
tooltip: selectedAppIds.isEmpty
|
||||||
: () {
|
? tr('installUpdateApps')
|
||||||
appsProvider.removeAppsWithModal(
|
: tr('installUpdateSelectedApps'),
|
||||||
context, selectedApps.toList());
|
icon: const Icon(
|
||||||
},
|
Icons.file_download_outlined,
|
||||||
tooltip: tr('removeSelectedApps'),
|
)),
|
||||||
icon: const Icon(Icons.delete_outline_outlined),
|
IconButton(
|
||||||
),
|
visualDensity: VisualDensity.compact,
|
||||||
IconButton(
|
onPressed: selectedAppIds.isEmpty
|
||||||
visualDensity: VisualDensity.compact,
|
? null
|
||||||
onPressed: getMassObtainFunction(),
|
: () {
|
||||||
tooltip: selectedAppIds.isEmpty
|
appsProvider.removeAppsWithModal(
|
||||||
? tr('installUpdateApps')
|
context, selectedApps.toList());
|
||||||
: tr('installUpdateSelectedApps'),
|
},
|
||||||
icon: const Icon(
|
tooltip: tr('removeSelectedApps'),
|
||||||
Icons.file_download_outlined,
|
icon: const Icon(Icons.delete_outline_outlined),
|
||||||
)),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: selectedAppIds.isEmpty ? null : launchCategorizeDialog(),
|
onPressed: selectedAppIds.isEmpty ? null : launchCategorizeDialog(),
|
||||||
tooltip: tr('categorize'),
|
tooltip: tr('categorize'),
|
||||||
icon: const Icon(Icons.category_outlined),
|
icon: const Icon(Icons.category_outlined),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: selectedAppIds.isEmpty ? null : showMoreOptionsDialog,
|
onPressed: selectedAppIds.isEmpty ? null : showMoreOptionsDialog,
|
||||||
tooltip: tr('more'),
|
tooltip: tr('more'),
|
||||||
icon: const Icon(Icons.more_horiz),
|
icon: const Icon(Icons.more_horiz),
|
||||||
),
|
),
|
||||||
],
|
];
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showFilterDialog() async {
|
showFilterDialog() async {
|
||||||
@ -893,6 +941,12 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
required: false,
|
required: false,
|
||||||
defaultValue: vals['author'])
|
defaultValue: vals['author'])
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('appId',
|
||||||
|
label: tr('appId'),
|
||||||
|
required: false,
|
||||||
|
defaultValue: vals['appId'])
|
||||||
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('upToDateApps',
|
GeneratedFormSwitch('upToDateApps',
|
||||||
label: tr('upToDateApps'),
|
label: tr('upToDateApps'),
|
||||||
@ -938,50 +992,33 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFilterButtonsRow() {
|
getFilterButtonsRow() {
|
||||||
|
var isFilterOff = filter.isIdenticalTo(neutralFilter, settingsProvider);
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
getSelectAllButton(),
|
getSelectAllButton(),
|
||||||
|
IconButton(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||||
|
tooltip: isFilterOff ? tr('filter') : tr('filterActive'),
|
||||||
|
onPressed: isFilterOff
|
||||||
|
? showFilterDialog
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
filter = AppsFilter();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(isFilterOff
|
||||||
|
? Icons.filter_list_rounded
|
||||||
|
: Icons.filter_list_off_rounded)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: Row(
|
||||||
scrollDirection: Axis.horizontal,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
child: getMainBottomButtonsRow())),
|
children: getMainBottomButtons(),
|
||||||
const VerticalDivider(),
|
)),
|
||||||
IconButton(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
if (currentFilterIsUpdatesOnly) {
|
|
||||||
filter = AppsFilter();
|
|
||||||
} else {
|
|
||||||
filter = updatesOnlyFilter;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
tooltip: currentFilterIsUpdatesOnly
|
|
||||||
? tr('removeOutdatedFilter')
|
|
||||||
: tr('showOutdatedOnly'),
|
|
||||||
icon: Icon(
|
|
||||||
currentFilterIsUpdatesOnly
|
|
||||||
? Icons.update_disabled_rounded
|
|
||||||
: Icons.update_rounded,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton.icon(
|
|
||||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
|
||||||
label: Text(
|
|
||||||
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
|
||||||
? tr('filter')
|
|
||||||
: tr('filterActive'),
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight:
|
|
||||||
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
|
||||||
? FontWeight.normal
|
|
||||||
: FontWeight.bold),
|
|
||||||
),
|
|
||||||
onPressed: showFilterDialog,
|
|
||||||
icon: const Icon(Icons.filter_list_rounded))
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1005,19 +1042,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () {
|
key: _refreshIndicatorKey,
|
||||||
HapticFeedback.lightImpact();
|
onRefresh: refresh,
|
||||||
setState(() {
|
|
||||||
refreshingSince = DateTime.now();
|
|
||||||
});
|
|
||||||
return appsProvider.checkUpdates().catchError((e) {
|
|
||||||
showError(e, context);
|
|
||||||
}).whenComplete(() {
|
|
||||||
setState(() {
|
|
||||||
refreshingSince = null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: CustomScrollView(slivers: <Widget>[
|
child: CustomScrollView(slivers: <Widget>[
|
||||||
CustomAppBar(title: tr('appsString')),
|
CustomAppBar(title: tr('appsString')),
|
||||||
...getLoadingWidgets(),
|
...getLoadingWidgets(),
|
||||||
@ -1035,6 +1061,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
class AppsFilter {
|
class AppsFilter {
|
||||||
late String nameFilter;
|
late String nameFilter;
|
||||||
late String authorFilter;
|
late String authorFilter;
|
||||||
|
late String idFilter;
|
||||||
late bool includeUptodate;
|
late bool includeUptodate;
|
||||||
late bool includeNonInstalled;
|
late bool includeNonInstalled;
|
||||||
late Set<String> categoryFilter;
|
late Set<String> categoryFilter;
|
||||||
@ -1043,6 +1070,7 @@ class AppsFilter {
|
|||||||
AppsFilter(
|
AppsFilter(
|
||||||
{this.nameFilter = '',
|
{this.nameFilter = '',
|
||||||
this.authorFilter = '',
|
this.authorFilter = '',
|
||||||
|
this.idFilter = '',
|
||||||
this.includeUptodate = true,
|
this.includeUptodate = true,
|
||||||
this.includeNonInstalled = true,
|
this.includeNonInstalled = true,
|
||||||
this.categoryFilter = const {},
|
this.categoryFilter = const {},
|
||||||
@ -1052,6 +1080,7 @@ class AppsFilter {
|
|||||||
return {
|
return {
|
||||||
'appName': nameFilter,
|
'appName': nameFilter,
|
||||||
'author': authorFilter,
|
'author': authorFilter,
|
||||||
|
'appId': idFilter,
|
||||||
'upToDateApps': includeUptodate,
|
'upToDateApps': includeUptodate,
|
||||||
'nonInstalledApps': includeNonInstalled,
|
'nonInstalledApps': includeNonInstalled,
|
||||||
'sourceFilter': sourceFilter
|
'sourceFilter': sourceFilter
|
||||||
@ -1061,6 +1090,7 @@ class AppsFilter {
|
|||||||
setFormValuesFromMap(Map<String, dynamic> values) {
|
setFormValuesFromMap(Map<String, dynamic> values) {
|
||||||
nameFilter = values['appName']!;
|
nameFilter = values['appName']!;
|
||||||
authorFilter = values['author']!;
|
authorFilter = values['author']!;
|
||||||
|
idFilter = values['appId']!;
|
||||||
includeUptodate = values['upToDateApps'];
|
includeUptodate = values['upToDateApps'];
|
||||||
includeNonInstalled = values['nonInstalledApps'];
|
includeNonInstalled = values['nonInstalledApps'];
|
||||||
sourceFilter = values['sourceFilter'];
|
sourceFilter = values['sourceFilter'];
|
||||||
@ -1069,6 +1099,7 @@ class AppsFilter {
|
|||||||
bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
|
bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
|
||||||
authorFilter.trim() == other.authorFilter.trim() &&
|
authorFilter.trim() == other.authorFilter.trim() &&
|
||||||
nameFilter.trim() == other.nameFilter.trim() &&
|
nameFilter.trim() == other.nameFilter.trim() &&
|
||||||
|
idFilter.trim() == other.idFilter.trim() &&
|
||||||
includeUptodate == other.includeUptodate &&
|
includeUptodate == other.includeUptodate &&
|
||||||
includeNonInstalled == other.includeNonInstalled &&
|
includeNonInstalled == other.includeNonInstalled &&
|
||||||
settingsProvider.setEqual(categoryFilter, other.categoryFilter) &&
|
settingsProvider.setEqual(categoryFilter, other.categoryFilter) &&
|
||||||
|
@ -6,6 +6,8 @@ import 'package:obtainium/pages/add_app.dart';
|
|||||||
import 'package:obtainium/pages/apps.dart';
|
import 'package:obtainium/pages/apps.dart';
|
||||||
import 'package:obtainium/pages/import_export.dart';
|
import 'package:obtainium/pages/import_export.dart';
|
||||||
import 'package:obtainium/pages/settings.dart';
|
import 'package:obtainium/pages/settings.dart';
|
||||||
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
@ -24,6 +26,7 @@ class NavigationPageItem {
|
|||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
List<int> selectedIndexHistory = [];
|
List<int> selectedIndexHistory = [];
|
||||||
|
int prevAppCount = -1;
|
||||||
|
|
||||||
List<NavigationPageItem> pages = [
|
List<NavigationPageItem> pages = [
|
||||||
NavigationPageItem(tr('appsString'), Icons.apps,
|
NavigationPageItem(tr('appsString'), Icons.apps,
|
||||||
@ -36,6 +39,39 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
AppsProvider appsProvider = context.watch<AppsProvider>();
|
||||||
|
|
||||||
|
switchToPage(int index) async {
|
||||||
|
if (index == 0) {
|
||||||
|
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
|
||||||
|
null) {
|
||||||
|
// Avoid duplicate GlobalKey error
|
||||||
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
selectedIndexHistory.clear();
|
||||||
|
});
|
||||||
|
} else if (selectedIndexHistory.isEmpty ||
|
||||||
|
(selectedIndexHistory.isNotEmpty &&
|
||||||
|
selectedIndexHistory.last != index)) {
|
||||||
|
setState(() {
|
||||||
|
int existingInd = selectedIndexHistory.indexOf(index);
|
||||||
|
if (existingInd >= 0) {
|
||||||
|
selectedIndexHistory.removeAt(existingInd);
|
||||||
|
}
|
||||||
|
selectedIndexHistory.add(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevAppCount >= 0 &&
|
||||||
|
appsProvider.apps.length > prevAppCount &&
|
||||||
|
selectedIndexHistory.isNotEmpty &&
|
||||||
|
selectedIndexHistory.last == 1) {
|
||||||
|
switchToPage(0);
|
||||||
|
}
|
||||||
|
prevAppCount = appsProvider.apps.length;
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@ -65,27 +101,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: (int index) async {
|
onDestinationSelected: (int index) async {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
if (index == 0) {
|
await switchToPage(index);
|
||||||
while ((pages[0].widget.key as GlobalKey<AppsPageState>)
|
|
||||||
.currentState !=
|
|
||||||
null) {
|
|
||||||
// Avoid duplicate GlobalKey error
|
|
||||||
await Future.delayed(const Duration(microseconds: 1));
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
selectedIndexHistory.clear();
|
|
||||||
});
|
|
||||||
} else if (selectedIndexHistory.isEmpty ||
|
|
||||||
(selectedIndexHistory.isNotEmpty &&
|
|
||||||
selectedIndexHistory.last != index)) {
|
|
||||||
setState(() {
|
|
||||||
int existingInd = selectedIndexHistory.indexOf(index);
|
|
||||||
if (existingInd >= 0) {
|
|
||||||
selectedIndexHistory.removeAt(existingInd);
|
|
||||||
}
|
|
||||||
selectedIndexHistory.add(index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
selectedIndex:
|
selectedIndex:
|
||||||
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||||
|
@ -323,8 +323,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (importInProgress)
|
if (importInProgress)
|
||||||
Column(
|
const Column(
|
||||||
children: const [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 14,
|
height: 14,
|
||||||
),
|
),
|
||||||
@ -470,7 +470,7 @@ class UrlSelectionModal extends StatefulWidget {
|
|||||||
this.selectedByDefault = true,
|
this.selectedByDefault = true,
|
||||||
this.onlyOneSelectionAllowed = false});
|
this.onlyOneSelectionAllowed = false});
|
||||||
|
|
||||||
Map<String, String> urlsWithDescriptions;
|
Map<String, List<String>> urlsWithDescriptions;
|
||||||
bool selectedByDefault;
|
bool selectedByDefault;
|
||||||
bool onlyOneSelectionAllowed;
|
bool onlyOneSelectionAllowed;
|
||||||
|
|
||||||
@ -479,7 +479,7 @@ class UrlSelectionModal extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||||
Map<MapEntry<String, String>, bool> urlWithDescriptionSelections = {};
|
Map<MapEntry<String, List<String>>, bool> urlWithDescriptionSelections = {};
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -522,16 +522,28 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
launchUrlString(urlWithD.key,
|
launchUrlString(urlWithD.key,
|
||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Column(
|
||||||
Uri.parse(urlWithD.key).path.substring(1),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: const TextStyle(decoration: TextDecoration.underline),
|
children: [
|
||||||
textAlign: TextAlign.start,
|
Text(
|
||||||
|
urlWithD.value[0],
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
Uri.parse(urlWithD.key).host,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
|
)
|
||||||
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
var descriptionText = Text(
|
var descriptionText = Text(
|
||||||
urlWithD.value.length > 128
|
urlWithD.value[1].length > 128
|
||||||
? '${urlWithD.value.substring(0, 128)}...'
|
? '${urlWithD.value[1].substring(0, 128)}...'
|
||||||
: urlWithD.value,
|
: urlWithD.value[1],
|
||||||
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -144,8 +144,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
child: Text(tr('followSystem')),
|
child: Text(tr('followSystem')),
|
||||||
),
|
),
|
||||||
...supportedLocales.map((e) => DropdownMenuItem(
|
...supportedLocales.map((e) => DropdownMenuItem(
|
||||||
value: e.toLanguageTag(),
|
value: e.key.toLanguageTag(),
|
||||||
child: Text(e.toLanguageTag().toUpperCase()),
|
child: Text(e.value),
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -205,6 +205,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const height32 = SizedBox(
|
||||||
|
height: 32,
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@ -217,9 +221,38 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
: Column(
|
: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text(
|
||||||
|
tr('updates'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
|
intervalDropdown,
|
||||||
|
height16,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(child: Text(tr('checkOnStart'))),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.checkOnStart,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.checkOnStart = value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height32,
|
||||||
|
Text(
|
||||||
|
tr('sourceSpecific'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
|
...sourceSpecificFields,
|
||||||
|
height32,
|
||||||
Text(
|
Text(
|
||||||
tr('appearance'),
|
tr('appearance'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
themeDropdown,
|
themeDropdown,
|
||||||
@ -227,7 +260,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('useBlackTheme')),
|
Flexible(child: Text(tr('useBlackTheme'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.useBlackTheme,
|
value: settingsProvider.useBlackTheme,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -254,7 +287,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('showWebInAppView')),
|
Flexible(child: Text(tr('showWebInAppView'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.showAppWebpage,
|
value: settingsProvider.showAppWebpage,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -266,7 +299,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('pinUpdates')),
|
Flexible(child: Text(tr('pinUpdates'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.pinUpdates,
|
value: settingsProvider.pinUpdates,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -278,7 +311,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('groupByCategory')),
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
tr('moveNonInstalledAppsToBottom'))),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.buryNonInstalled,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.buryNonInstalled = value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height16,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(child: Text(tr('groupByCategory'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.groupByCategory,
|
value: settingsProvider.groupByCategory,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -290,7 +337,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('dontShowTrackOnlyWarnings')),
|
Flexible(
|
||||||
|
child:
|
||||||
|
Text(tr('dontShowTrackOnlyWarnings'))),
|
||||||
Switch(
|
Switch(
|
||||||
value:
|
value:
|
||||||
settingsProvider.hideTrackOnlyWarning,
|
settingsProvider.hideTrackOnlyWarning,
|
||||||
@ -304,7 +353,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('dontShowAPKOriginWarnings')),
|
Flexible(
|
||||||
|
child:
|
||||||
|
Text(tr('dontShowAPKOriginWarnings'))),
|
||||||
Switch(
|
Switch(
|
||||||
value:
|
value:
|
||||||
settingsProvider.hideAPKOriginWarning,
|
settingsProvider.hideAPKOriginWarning,
|
||||||
@ -314,31 +365,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(
|
height32,
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
height16,
|
|
||||||
Text(
|
|
||||||
tr('updates'),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary),
|
|
||||||
),
|
|
||||||
intervalDropdown,
|
|
||||||
const Divider(
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
tr('sourceSpecific'),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary),
|
|
||||||
),
|
|
||||||
...sourceSpecificFields,
|
|
||||||
const Divider(
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
tr('categories'),
|
tr('categories'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
|
@ -6,11 +6,11 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:android_intent_plus/flag.dart';
|
import 'package:android_intent_plus/flag.dart';
|
||||||
|
import 'package:android_package_installer/android_package_installer.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
|
||||||
import 'package:installed_apps/app_info.dart';
|
import 'package:installed_apps/app_info.dart';
|
||||||
import 'package:installed_apps/installed_apps.dart';
|
import 'package:installed_apps/installed_apps.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
@ -27,6 +27,7 @@ import 'package:flutter_fgbg/flutter_fgbg.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:android_intent_plus/android_intent.dart';
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
|
import 'package:archive/archive.dart';
|
||||||
|
|
||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
@ -46,6 +47,13 @@ class DownloadedApk {
|
|||||||
DownloadedApk(this.appId, this.file);
|
DownloadedApk(this.appId, this.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DownloadedXApkDir {
|
||||||
|
String appId;
|
||||||
|
File file;
|
||||||
|
Directory extracted;
|
||||||
|
DownloadedXApkDir(this.appId, this.file, this.extracted);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> generateStandardVersionRegExStrings() {
|
List<String> generateStandardVersionRegExStrings() {
|
||||||
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
|
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
|
||||||
var basics = [
|
var basics = [
|
||||||
@ -113,24 +121,36 @@ class AppsProvider with ChangeNotifier {
|
|||||||
() async {
|
() async {
|
||||||
// Load Apps into memory (in background, this is done later instead of in the constructor)
|
// Load Apps into memory (in background, this is done later instead of in the constructor)
|
||||||
await loadApps();
|
await loadApps();
|
||||||
// Delete existing APKs
|
// Delete any partial APKs
|
||||||
(await getExternalStorageDirectory())
|
var cutoff = DateTime.now().subtract(const Duration(days: 7));
|
||||||
?.listSync()
|
(await getExternalCacheDirectories())
|
||||||
|
?.first
|
||||||
|
.listSync()
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element.path.endsWith('.apk') ||
|
element.path.endsWith('.part') ||
|
||||||
element.path.endsWith('.apk.part'))
|
element.statSync().modified.isBefore(cutoff))
|
||||||
.forEach((apk) {
|
.forEach((partialApk) {
|
||||||
apk.delete();
|
partialApk.delete();
|
||||||
});
|
});
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(String url, String fileName, Function? onProgress,
|
Future<File> downloadFile(
|
||||||
{bool useExisting = true}) async {
|
String url, String fileNameNoExt, Function? onProgress,
|
||||||
var destDir = (await getExternalStorageDirectory())!.path;
|
{bool useExisting = true, Map<String, String>? headers}) async {
|
||||||
StreamedResponse response =
|
var destDir = (await getExternalCacheDirectories())!.first.path;
|
||||||
await Client().send(Request('GET', Uri.parse(url)));
|
var req = Request('GET', Uri.parse(url));
|
||||||
File downloadedFile = File('$destDir/$fileName');
|
if (headers != null) {
|
||||||
|
req.headers.addAll(headers);
|
||||||
|
}
|
||||||
|
var client = Client();
|
||||||
|
StreamedResponse response = await client.send(req);
|
||||||
|
String ext =
|
||||||
|
response.headers['content-disposition']?.split('.').last ?? 'apk';
|
||||||
|
if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||||
|
ext = ext.substring(0, ext.length - 1);
|
||||||
|
}
|
||||||
|
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
|
||||||
if (!(downloadedFile.existsSync() && useExisting)) {
|
if (!(downloadedFile.existsSync() && useExisting)) {
|
||||||
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
||||||
if (tempDownloadedFile.existsSync()) {
|
if (tempDownloadedFile.existsSync()) {
|
||||||
@ -158,11 +178,34 @@ class AppsProvider with ChangeNotifier {
|
|||||||
throw response.reasonPhrase ?? tr('unexpectedError');
|
throw response.reasonPhrase ?? tr('unexpectedError');
|
||||||
}
|
}
|
||||||
tempDownloadedFile.renameSync(downloadedFile.path);
|
tempDownloadedFile.renameSync(downloadedFile.path);
|
||||||
|
} else {
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
return downloadedFile;
|
return downloadedFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
|
Future<File> handleAPKIDChange(App app, PackageArchiveInfo newInfo,
|
||||||
|
File downloadedFile, String downloadUrl) async {
|
||||||
|
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
||||||
|
// The former case should be handled (give the App its real ID), the latter is a security issue
|
||||||
|
if (app.id != newInfo.packageName) {
|
||||||
|
var isTempId = SourceProvider().isTempId(app);
|
||||||
|
if (apps[app.id] != null && !isTempId) {
|
||||||
|
throw IDChangedError();
|
||||||
|
}
|
||||||
|
var originalAppId = app.id;
|
||||||
|
app.id = newInfo.packageName;
|
||||||
|
downloadedFile = downloadedFile.renameSync(
|
||||||
|
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}');
|
||||||
|
if (apps[originalAppId] != null) {
|
||||||
|
await removeApps([originalAppId]);
|
||||||
|
await saveApps([app], onlyIfExists: !isTempId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return downloadedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object> downloadApp(App app, BuildContext? context) async {
|
||||||
NotificationsProvider? notificationsProvider =
|
NotificationsProvider? notificationsProvider =
|
||||||
context?.read<NotificationsProvider>();
|
context?.read<NotificationsProvider>();
|
||||||
var notifId = DownloadNotification(app.finalName, 0).id;
|
var notifId = DownloadNotification(app.finalName, 0).id;
|
||||||
@ -171,15 +214,16 @@ class AppsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String downloadUrl = await SourceProvider()
|
AppSource source = SourceProvider()
|
||||||
.getSource(app.url, overrideSource: app.overrideSource)
|
.getSource(app.url, overrideSource: app.overrideSource);
|
||||||
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value);
|
String downloadUrl = await source.apkUrlPrefetchModifier(
|
||||||
var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
|
app.apkUrls[app.preferredApkIndex].value, app.url);
|
||||||
var notif = DownloadNotification(app.finalName, 100);
|
var notif = DownloadNotification(app.finalName, 100);
|
||||||
notificationsProvider?.cancel(notif.id);
|
notificationsProvider?.cancel(notif.id);
|
||||||
int? prevProg;
|
int? prevProg;
|
||||||
File downloadedFile =
|
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
|
||||||
await downloadFile(downloadUrl, fileName, (double? progress) {
|
var downloadedFile = await downloadFile(downloadUrl, fileNameNoExt,
|
||||||
|
headers: source.requestHeaders, (double? progress) {
|
||||||
int? prog = progress?.ceil();
|
int? prog = progress?.ceil();
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null) {
|
||||||
apps[app.id]!.downloadProgress = progress;
|
apps[app.id]!.downloadProgress = progress;
|
||||||
@ -191,33 +235,45 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
prevProg = prog;
|
prevProg = prog;
|
||||||
});
|
});
|
||||||
// Delete older versions of the APK if any
|
// Set to 90 for remaining steps, will make null in 'finally'
|
||||||
|
if (apps[app.id] != null) {
|
||||||
|
apps[app.id]!.downloadProgress = -1;
|
||||||
|
notifyListeners();
|
||||||
|
notif = DownloadNotification(app.finalName, -1);
|
||||||
|
notificationsProvider?.notify(notif);
|
||||||
|
}
|
||||||
|
PackageArchiveInfo? newInfo;
|
||||||
|
var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk');
|
||||||
|
Directory? xapkDir;
|
||||||
|
if (isAPK) {
|
||||||
|
newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||||
|
} else {
|
||||||
|
// Assume XAPK
|
||||||
|
String xapkDirPath = '${downloadedFile.path}-dir';
|
||||||
|
unzipFile(downloadedFile.path, '${downloadedFile.path}-dir');
|
||||||
|
xapkDir = Directory(xapkDirPath);
|
||||||
|
var apks = xapkDir
|
||||||
|
.listSync()
|
||||||
|
.where((e) => e.path.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
newInfo = await PackageArchiveInfo.fromPath(apks.first.path);
|
||||||
|
}
|
||||||
|
downloadedFile =
|
||||||
|
await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl);
|
||||||
|
// Delete older versions of the file if any
|
||||||
for (var file in downloadedFile.parent.listSync()) {
|
for (var file in downloadedFile.parent.listSync()) {
|
||||||
var fn = file.path.split('/').last;
|
var fn = file.path.split('/').last;
|
||||||
if (fn.startsWith('${app.id}-') &&
|
if (fn.startsWith('${app.id}-') &&
|
||||||
fn.endsWith('.apk') &&
|
FileSystemEntity.isFileSync(file.path) &&
|
||||||
fn != fileName) {
|
file.path != downloadedFile.path) {
|
||||||
file.delete();
|
file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
if (isAPK) {
|
||||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
return DownloadedApk(app.id, downloadedFile);
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
} else {
|
||||||
if (app.id != newInfo.packageName) {
|
return DownloadedXApkDir(app.id, downloadedFile, xapkDir!);
|
||||||
var isTempId = SourceProvider().isTempId(app);
|
|
||||||
if (apps[app.id] != null && !isTempId) {
|
|
||||||
throw IDChangedError();
|
|
||||||
}
|
|
||||||
var originalAppId = app.id;
|
|
||||||
app.id = newInfo.packageName;
|
|
||||||
downloadedFile = downloadedFile.renameSync(
|
|
||||||
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
|
|
||||||
if (apps[originalAppId] != null) {
|
|
||||||
await removeApps([originalAppId]);
|
|
||||||
await saveApps([app], onlyIfExists: !isTempId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return DownloadedApk(app.id, downloadedFile);
|
|
||||||
} finally {
|
} finally {
|
||||||
notificationsProvider?.cancel(notifId);
|
notificationsProvider?.cancel(notifId);
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null) {
|
||||||
@ -264,11 +320,44 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
void unzipFile(String filePath, String destinationPath) {
|
||||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
final bytes = File(filePath).readAsBytesSync();
|
||||||
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
|
final archive = ZipDecoder().decodeBytes(bytes);
|
||||||
// But even then, we don't know if it actually succeeded
|
|
||||||
Future<void> installApk(DownloadedApk file) async {
|
for (final file in archive) {
|
||||||
|
final filename = '$destinationPath/${file.name}';
|
||||||
|
if (file.isFile) {
|
||||||
|
final data = file.content as List<int>;
|
||||||
|
File(filename)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(data);
|
||||||
|
} else {
|
||||||
|
Directory(filename).create(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> installXApkDir(DownloadedXApkDir dir,
|
||||||
|
{bool silent = false}) async {
|
||||||
|
try {
|
||||||
|
var somethingInstalled = false;
|
||||||
|
for (var apk in dir.extracted
|
||||||
|
.listSync()
|
||||||
|
.where((f) => f is File && f.path.toLowerCase().endsWith('.apk'))) {
|
||||||
|
somethingInstalled = somethingInstalled ||
|
||||||
|
await installApk(DownloadedApk(dir.appId, apk as File),
|
||||||
|
silent: silent);
|
||||||
|
}
|
||||||
|
if (somethingInstalled) {
|
||||||
|
dir.file.delete();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
dir.extracted.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> installApk(DownloadedApk file, {bool silent = false}) async {
|
||||||
|
// TODO: Use 'silent' when/if ever possible
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
||||||
AppInfo? appInfo;
|
AppInfo? appInfo;
|
||||||
try {
|
try {
|
||||||
@ -281,16 +370,19 @@ class AppsProvider with ChangeNotifier {
|
|||||||
!(await canDowngradeApps())) {
|
!(await canDowngradeApps())) {
|
||||||
throw DowngradeError();
|
throw DowngradeError();
|
||||||
}
|
}
|
||||||
await InstallPlugin.installApk(file.file.path, obtainiumId);
|
int? code =
|
||||||
if (file.appId == obtainiumId) {
|
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
||||||
// Obtainium prompt should be lowest
|
bool installed = false;
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
if (code != null && code != 0 && code != 3) {
|
||||||
|
throw InstallError(code);
|
||||||
|
} else if (code == 0) {
|
||||||
|
installed = true;
|
||||||
|
apps[file.appId]!.app.installedVersion =
|
||||||
|
apps[file.appId]!.app.latestVersion;
|
||||||
|
file.file.delete();
|
||||||
}
|
}
|
||||||
apps[file.appId]!.app.installedVersion =
|
await saveApps([apps[file.appId]!.app]);
|
||||||
apps[file.appId]!.app.latestVersion;
|
return installed;
|
||||||
// Don't correct install status as installation may not be done yet
|
|
||||||
await saveApps([apps[file.appId]!.app],
|
|
||||||
attemptToCorrectInstallStatus: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void uninstallApp(String appId) async {
|
void uninstallApp(String appId) async {
|
||||||
@ -395,76 +487,62 @@ class AppsProvider with ChangeNotifier {
|
|||||||
a.installedVersion = a.latestVersion;
|
a.installedVersion = a.latestVersion;
|
||||||
return a;
|
return a;
|
||||||
}).toList());
|
}).toList());
|
||||||
// Download APKs for all Apps to be installed
|
|
||||||
|
// Prepare to download+install Apps
|
||||||
MultiAppMultiError errors = MultiAppMultiError();
|
MultiAppMultiError errors = MultiAppMultiError();
|
||||||
List<DownloadedApk?> downloadedFiles =
|
List<String> installedIds = [];
|
||||||
await Future.wait(appsToInstall.map((id) async {
|
|
||||||
|
// Move Obtainium to the end of the line (let all other apps update first)
|
||||||
|
String? temp;
|
||||||
|
appsToInstall.removeWhere((element) {
|
||||||
|
bool res = element == obtainiumId || element == obtainiumTempId;
|
||||||
|
if (res) {
|
||||||
|
temp = element;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
if (temp != null) {
|
||||||
|
appsToInstall = [...appsToInstall, temp!];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var id in appsToInstall) {
|
||||||
try {
|
try {
|
||||||
return await downloadApp(apps[id]!.app, context);
|
// ignore: use_build_context_synchronously
|
||||||
|
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
|
||||||
|
DownloadedApk? downloadedFile;
|
||||||
|
DownloadedXApkDir? downloadedDir;
|
||||||
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
|
downloadedFile = downloadedArtifact;
|
||||||
|
} else {
|
||||||
|
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||||
|
}
|
||||||
|
bool willBeSilent = await canInstallSilently(
|
||||||
|
apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app);
|
||||||
|
willBeSilent = false; // TODO: Remove this when silent updates work
|
||||||
|
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
||||||
|
true)) {
|
||||||
|
throw ObtainiumError(tr('cancelled'));
|
||||||
|
}
|
||||||
|
if (!willBeSilent && context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
await waitForUserToReturnToForeground(context);
|
||||||
|
}
|
||||||
|
apps[id]?.downloadProgress = -1;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
if (downloadedFile != null) {
|
||||||
|
await installApk(downloadedFile, silent: willBeSilent);
|
||||||
|
} else {
|
||||||
|
await installXApkDir(downloadedDir!, silent: willBeSilent);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
apps[id]?.downloadProgress = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
installedIds.add(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.add(id, e.toString());
|
errors.add(id, e.toString());
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}));
|
|
||||||
downloadedFiles =
|
|
||||||
downloadedFiles.where((element) => element != null).toList();
|
|
||||||
// Separate the Apps to install into silent and regular lists
|
|
||||||
List<DownloadedApk> silentUpdates = [];
|
|
||||||
List<DownloadedApk> regularInstalls = [];
|
|
||||||
for (var f in downloadedFiles) {
|
|
||||||
bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app);
|
|
||||||
if (willBeSilent) {
|
|
||||||
silentUpdates.add(f);
|
|
||||||
} else {
|
|
||||||
regularInstalls.add(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move everything to the regular install list (since silent updates don't currently work)
|
|
||||||
// TODO: Remove this when silent updates work
|
|
||||||
regularInstalls.addAll(silentUpdates);
|
|
||||||
|
|
||||||
// If Obtainium is being installed, it should be the last one
|
|
||||||
List<DownloadedApk> moveObtainiumToStart(List<DownloadedApk> items) {
|
|
||||||
DownloadedApk? temp;
|
|
||||||
items.removeWhere((element) {
|
|
||||||
bool res =
|
|
||||||
element.appId == obtainiumId || element.appId == obtainiumTempId;
|
|
||||||
if (res) {
|
|
||||||
temp = element;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
if (temp != null) {
|
|
||||||
items = [temp!, ...items];
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
silentUpdates = moveObtainiumToStart(silentUpdates);
|
|
||||||
regularInstalls = moveObtainiumToStart(regularInstalls);
|
|
||||||
|
|
||||||
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
|
||||||
true)) {
|
|
||||||
throw ObtainiumError(tr('cancelled'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Install silent updates (uncomment when it works - TODO)
|
|
||||||
// for (var u in silentUpdates) {
|
|
||||||
// await installApk(u, silent: true); // Would need to add silent option
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Do regular installs
|
|
||||||
if (regularInstalls.isNotEmpty && context != null) {
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
await waitForUserToReturnToForeground(context);
|
|
||||||
for (var i in regularInstalls) {
|
|
||||||
try {
|
|
||||||
await installApk(i);
|
|
||||||
} catch (e) {
|
|
||||||
errors.add(i.appId, e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.content.isNotEmpty) {
|
if (errors.content.isNotEmpty) {
|
||||||
@ -473,7 +551,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
NotificationsProvider().cancel(UpdateNotification([]).id);
|
NotificationsProvider().cancel(UpdateNotification([]).id);
|
||||||
|
|
||||||
return downloadedFiles.map((e) => e!.appId).toList();
|
return installedIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> getAppsDir() async {
|
Future<Directory> getAppsDir() async {
|
||||||
@ -712,11 +790,18 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeApps(List<String> appIds) async {
|
Future<void> removeApps(List<String> appIds) async {
|
||||||
|
var apkFiles = (await getExternalCacheDirectories())?.first.listSync();
|
||||||
for (var appId in appIds) {
|
for (var appId in appIds) {
|
||||||
File file = File('${(await getAppsDir()).path}/$appId.json');
|
File file = File('${(await getAppsDir()).path}/$appId.json');
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
file.deleteSync();
|
file.deleteSync();
|
||||||
}
|
}
|
||||||
|
apkFiles
|
||||||
|
?.where(
|
||||||
|
(element) => element.path.split('/').last.startsWith('$appId-'))
|
||||||
|
.forEach((element) {
|
||||||
|
element.delete();
|
||||||
|
});
|
||||||
if (apps.containsKey(appId)) {
|
if (apps.containsKey(appId)) {
|
||||||
apps.remove(appId);
|
apps.remove(appId);
|
||||||
}
|
}
|
||||||
|
@ -167,7 +167,8 @@ class NotificationsProvider {
|
|||||||
progress: progPercent ?? 0,
|
progress: progPercent ?? 0,
|
||||||
maxProgress: 100,
|
maxProgress: 100,
|
||||||
showProgress: progPercent != null,
|
showProgress: progPercent != null,
|
||||||
onlyAlertOnce: onlyAlertOnce)));
|
onlyAlertOnce: onlyAlertOnce,
|
||||||
|
indeterminate: progPercent != null && progPercent < 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notify(ObtainiumNotification notif,
|
Future<void> notify(ObtainiumNotification notif,
|
||||||
|
@ -35,6 +35,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
|
|||||||
|
|
||||||
class SettingsProvider with ChangeNotifier {
|
class SettingsProvider with ChangeNotifier {
|
||||||
SharedPreferences? prefs;
|
SharedPreferences? prefs;
|
||||||
|
bool justStarted = true;
|
||||||
|
|
||||||
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
||||||
|
|
||||||
@ -92,6 +93,15 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get checkOnStart {
|
||||||
|
return prefs?.getBool('checkOnStart') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set checkOnStart(bool checkOnStart) {
|
||||||
|
prefs?.setBool('checkOnStart', checkOnStart);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
SortColumnSettings get sortColumn {
|
SortColumnSettings get sortColumn {
|
||||||
return SortColumnSettings.values[
|
return SortColumnSettings.values[
|
||||||
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
|
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
|
||||||
@ -120,6 +130,14 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool checkJustStarted() {
|
||||||
|
if (justStarted) {
|
||||||
|
justStarted = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> getInstallPermission({bool enforce = false}) async {
|
Future<bool> getInstallPermission({bool enforce = false}) async {
|
||||||
while (!(await Permission.requestInstallPackages.isGranted)) {
|
while (!(await Permission.requestInstallPackages.isGranted)) {
|
||||||
// Explicit request as InstallPlugin request sometimes bugged
|
// Explicit request as InstallPlugin request sometimes bugged
|
||||||
@ -154,6 +172,15 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get buryNonInstalled {
|
||||||
|
return prefs?.getBool('buryNonInstalled') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set buryNonInstalled(bool show) {
|
||||||
|
prefs?.setBool('buryNonInstalled', show);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
bool get groupByCategory {
|
bool get groupByCategory {
|
||||||
return prefs?.getBool('groupByCategory') ?? false;
|
return prefs?.getBool('groupByCategory') ?? false;
|
||||||
}
|
}
|
||||||
@ -206,8 +233,7 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
.map((e) => e as App)
|
.map((e) => e as App)
|
||||||
.toList();
|
.toList();
|
||||||
if (changedApps.isNotEmpty) {
|
if (changedApps.isNotEmpty) {
|
||||||
appsProvider.saveApps(changedApps,
|
appsProvider.saveApps(changedApps);
|
||||||
attemptToCorrectInstallStatus: false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs?.setString('categories', jsonEncode(cats));
|
prefs?.setString('categories', jsonEncode(cats));
|
||||||
@ -217,7 +243,7 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
String? get forcedLocale {
|
String? get forcedLocale {
|
||||||
var fl = prefs?.getString('forcedLocale');
|
var fl = prefs?.getString('forcedLocale');
|
||||||
return supportedLocales
|
return supportedLocales
|
||||||
.where((element) => element.toLanguageTag() == fl)
|
.where((element) => element.key.toLanguageTag() == fl)
|
||||||
.isNotEmpty
|
.isNotEmpty
|
||||||
? fl
|
? fl
|
||||||
: null;
|
: null;
|
||||||
@ -227,7 +253,7 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
if (fl == null) {
|
if (fl == null) {
|
||||||
prefs?.remove('forcedLocale');
|
prefs?.remove('forcedLocale');
|
||||||
} else if (supportedLocales
|
} else if (supportedLocales
|
||||||
.where((element) => element.toLanguageTag() == fl)
|
.where((element) => element.key.toLanguageTag() == fl)
|
||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
prefs?.setString('forcedLocale', fl);
|
prefs?.setString('forcedLocale', fl);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||||
|
import 'package:obtainium/app_sources/apkpure.dart';
|
||||||
import 'package:obtainium/app_sources/codeberg.dart';
|
import 'package:obtainium/app_sources/codeberg.dart';
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||||
@ -15,10 +16,12 @@ import 'package:obtainium/app_sources/github.dart';
|
|||||||
import 'package:obtainium/app_sources/gitlab.dart';
|
import 'package:obtainium/app_sources/gitlab.dart';
|
||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
|
import 'package:obtainium/app_sources/jenkins.dart';
|
||||||
import 'package:obtainium/app_sources/mullvad.dart';
|
import 'package:obtainium/app_sources/mullvad.dart';
|
||||||
import 'package:obtainium/app_sources/neutroncode.dart';
|
import 'package:obtainium/app_sources/neutroncode.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
|
import 'package:obtainium/app_sources/sourcehut.dart';
|
||||||
import 'package:obtainium/app_sources/steammobile.dart';
|
import 'package:obtainium/app_sources/steammobile.dart';
|
||||||
import 'package:obtainium/app_sources/telegramapp.dart';
|
import 'package:obtainium/app_sources/telegramapp.dart';
|
||||||
import 'package:obtainium/app_sources/vlc.dart';
|
import 'package:obtainium/app_sources/vlc.dart';
|
||||||
@ -319,6 +322,22 @@ abstract class AppSource {
|
|||||||
name = runtimeType.toString();
|
name = runtimeType.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overrideVersionDetectionFormDefault(String vd, bool disableStandard) {
|
||||||
|
additionalAppSpecificSourceAgnosticSettingFormItems =
|
||||||
|
additionalAppSpecificSourceAgnosticSettingFormItems.map((e) {
|
||||||
|
return e.map((e2) {
|
||||||
|
if (e2.key == 'versionDetection') {
|
||||||
|
var item = e2 as GeneratedFormDropdown;
|
||||||
|
item.defaultValue = vd;
|
||||||
|
if (disableStandard) {
|
||||||
|
item.disabledOptKeys = ['standardVersionDetection'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e2;
|
||||||
|
}).toList();
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
String standardizeUrl(String url) {
|
String standardizeUrl(String url) {
|
||||||
url = preStandardizeUrl(url);
|
url = preStandardizeUrl(url);
|
||||||
if (!hostChanged) {
|
if (!hostChanged) {
|
||||||
@ -327,6 +346,18 @@ abstract class AppSource {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String>? get requestHeaders => null;
|
||||||
|
|
||||||
|
Future<Response> sourceRequest(String url) async {
|
||||||
|
if (requestHeaders != null) {
|
||||||
|
var req = Request('GET', Uri.parse(url));
|
||||||
|
req.headers.addAll(requestHeaders!);
|
||||||
|
return Response.fromStream(await Client().send(req));
|
||||||
|
} else {
|
||||||
|
return get(Uri.parse(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
@ -341,7 +372,7 @@ abstract class AppSource {
|
|||||||
[];
|
[];
|
||||||
|
|
||||||
// Some additional data may be needed for Apps regardless of Source
|
// Some additional data may be needed for Apps regardless of Source
|
||||||
final List<List<GeneratedFormItem>>
|
List<List<GeneratedFormItem>>
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItems = [
|
additionalAppSpecificSourceAgnosticSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch(
|
GeneratedFormSwitch(
|
||||||
@ -393,12 +424,13 @@ abstract class AppSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
Future<String> apkUrlPrefetchModifier(
|
||||||
|
String apkUrl, String standardUrl) async {
|
||||||
return apkUrl;
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canSearch = false;
|
bool canSearch = false;
|
||||||
Future<Map<String, String>> search(String query) {
|
Future<Map<String, List<String>>> search(String query) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,7 +448,7 @@ ObtainiumError getObtainiumHttpError(Response res) {
|
|||||||
abstract class MassAppUrlSource {
|
abstract class MassAppUrlSource {
|
||||||
late String name;
|
late String name;
|
||||||
late List<String> requiredArgs;
|
late List<String> requiredArgs;
|
||||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
Future<Map<String, List<String>>> getUrlsWithDescriptions(List<String> args);
|
||||||
}
|
}
|
||||||
|
|
||||||
regExValidator(String? value) {
|
regExValidator(String? value) {
|
||||||
@ -440,8 +472,12 @@ class SourceProvider {
|
|||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
IzzyOnDroid(),
|
||||||
FDroidRepo(),
|
FDroidRepo(),
|
||||||
|
Jenkins(),
|
||||||
SourceForge(),
|
SourceForge(),
|
||||||
|
SourceHut(),
|
||||||
APKMirror(),
|
APKMirror(),
|
||||||
|
APKPure(),
|
||||||
|
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
Signal(),
|
Signal(),
|
||||||
VLC(),
|
VLC(),
|
||||||
|
207
pubspec.lock
207
pubspec.lock
@ -5,18 +5,27 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: android_alarm_manager_plus
|
name: android_alarm_manager_plus
|
||||||
sha256: "88a8001851fdc9bd54fa4e30d0277bb900a50f3d86ff244da7f027400bf23ac0"
|
sha256: ed5fb34f8befc382fb4800b02aa86a34d279b911e1c05752f702d12fcfe26e0e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "3.0.0"
|
||||||
android_intent_plus:
|
android_intent_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: android_intent_plus
|
name: android_intent_plus
|
||||||
sha256: "04cbc7c332a6f0bba88fed354de78813e9d24049c1800aaf10f449c7adc22603"
|
sha256: f79fbb8ccb64b5584d19caa9c3d15613bf21cfbd829a6ca7f089fb5dfd43f8aa
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.9"
|
version: "4.0.0"
|
||||||
|
android_package_installer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: main
|
||||||
|
resolved-ref: f09c79eee5be3c60b04760143eb954a13fdd07f1
|
||||||
|
url: "https://github.com/ImranR98/android_package_installer"
|
||||||
|
source: git
|
||||||
|
version: "0.0.1"
|
||||||
animations:
|
animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -25,22 +34,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
archive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
|
sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.1"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.0"
|
version: "2.11.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -53,10 +70,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
|
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.3.0"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -69,10 +86,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
|
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -85,10 +110,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.3"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -117,10 +142,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903
|
sha256: "9b1a0c32b2a503f8fe9f8764fac7b5fcd4f6bd35d8f49de5350bccf9e2a33b8a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.2"
|
version: "9.0.0"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -141,10 +166,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: easy_localization
|
name: easy_localization
|
||||||
sha256: "6a2e99fa0bfe5765bf4c6ca9b137d5de2c75593007178c5e4cd2ae985f870080"
|
sha256: f30e9b20ed4d1b890171c30241d9b9c43efe21fee55dee7bd68f94daf269ea75
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2-dev.2"
|
||||||
easy_logger:
|
easy_logger:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -165,10 +190,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.2"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -181,10 +206,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff
|
sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.10"
|
version: "5.3.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -210,26 +235,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
sha256: "2876372952b65ca7f684e698eba22bda1cf581fa071dd30ba2f01900f507d0d1"
|
sha256: ee6ee56855aa920899b68586b538474d086c149932220b47b92502cbfb5ba5e5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.0.0+1"
|
version: "14.0.0+2"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_linux
|
name: flutter_local_notifications_linux
|
||||||
sha256: "909bb95de05a2e793503a2437146285a2f600cd0b3f826e26b870a334d8586d7"
|
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0+1"
|
||||||
flutter_local_notifications_platform_interface:
|
flutter_local_notifications_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_platform_interface
|
name: flutter_local_notifications_platform_interface
|
||||||
sha256: "63235c42de5b6c99846969a27ad0209c401e6b77b0498939813725b5791c107c"
|
sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.0.0+1"
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -247,10 +272,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb"
|
sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.13"
|
version: "2.0.14"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -273,18 +298,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: html
|
name: html
|
||||||
sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb"
|
sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.2"
|
version: "0.15.3"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
|
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.5"
|
version: "0.13.6"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -293,14 +318,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
install_plugin_v2:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: install_plugin_v2
|
|
||||||
sha256: d6b014637e7a53839e9c5a254f9fd9bb8866392c6db1f16184ce17818cc2d979
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
installed_apps:
|
installed_apps:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -313,42 +330,42 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
|
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.0"
|
version: "0.18.0"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: js
|
name: js
|
||||||
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.5"
|
version: "0.6.7"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
|
sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.1.0"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: markdown
|
name: markdown
|
||||||
sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5
|
sha256: "8e332924094383133cee218b676871f42db2514f1f6ac617b6cf6152a7faab8e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.2"
|
version: "7.1.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
|
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.13"
|
version: "0.12.15"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -361,10 +378,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
|
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.9.1"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -401,18 +418,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
version: "1.8.3"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
|
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.14"
|
version: "2.0.15"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -425,10 +442,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.3"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -497,10 +514,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
|
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.4.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -517,6 +534,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
pointycastle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointycastle
|
||||||
|
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.7.3"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -537,10 +562,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1
|
sha256: "322a1ec9d9fe07e2e2252c098ce93d12dbd06133cc4c00ffe6a4ef505c295c17"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.4"
|
version: "7.0.0"
|
||||||
share_plus_platform_interface:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -553,10 +578,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
|
sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -569,10 +594,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
|
sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -622,10 +647,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
|
sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.8"
|
version: "2.2.8+2"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -678,10 +703,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
|
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.16"
|
version: "0.5.1"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -694,26 +719,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: typed_data
|
name: typed_data
|
||||||
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.2"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
|
sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.10"
|
version: "6.1.11"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd"
|
sha256: "7aac14be5f4731b923cc697ae2d42043945076cd0dbb8806baecc92c1dc88891"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.31"
|
version: "6.0.33"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -790,10 +815,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
sha256: d6cf18cd6c809c5a9294cd99707a21986aac4e08c87e1916ce2590315fb55d3a
|
sha256: "1acea8def62592123e2fbbca164ed8681a98a890bdcbb88f916d5b4a22687759"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.6.2"
|
version: "3.7.0"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -806,18 +831,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: c94d242d8cbe1012c06ba7ac790c46d6e6b68723b7d34f8c74ed19f68d166f49
|
sha256: "4646bb68297803bdbb96d46853e8fcb560d6cb5e04153fa64581535767875dfe"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "3.4.3"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
|
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "4.1.4"
|
||||||
|
win32_registry:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32_registry
|
||||||
|
sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -830,10 +863,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
|
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.2"
|
version: "6.3.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.0 <3.0.0"
|
dart: ">=3.0.0-417 <4.0.0"
|
||||||
flutter: ">=3.4.0-17.0.pre"
|
flutter: ">=3.4.0-17.0.pre"
|
||||||
|
16
pubspec.yaml
16
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
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.12.0+160 # When changing this, update the tag in main() accordingly
|
version: 0.13.4+168 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
@ -48,18 +48,22 @@ dependencies:
|
|||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
permission_handler: ^10.0.0
|
permission_handler: ^10.0.0
|
||||||
fluttertoast: ^8.0.9
|
fluttertoast: ^8.0.9
|
||||||
device_info_plus: ^8.0.0
|
device_info_plus: ^9.0.0
|
||||||
file_picker: ^5.2.10
|
file_picker: ^5.2.10
|
||||||
animations: ^2.0.4
|
animations: ^2.0.4
|
||||||
install_plugin_v2: ^1.0.0
|
android_package_installer:
|
||||||
share_plus: ^6.0.1
|
git:
|
||||||
|
url: https://github.com/ImranR98/android_package_installer
|
||||||
|
ref: main
|
||||||
|
share_plus: ^7.0.0
|
||||||
installed_apps: ^1.3.1
|
installed_apps: ^1.3.1
|
||||||
package_archive_info: ^0.1.0
|
package_archive_info: ^0.1.0
|
||||||
android_alarm_manager_plus: ^2.1.0
|
android_alarm_manager_plus: ^3.0.0
|
||||||
sqflite: ^2.2.0+3
|
sqflite: ^2.2.0+3
|
||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
android_intent_plus: ^3.1.5
|
android_intent_plus: ^4.0.0
|
||||||
flutter_markdown: ^0.6.14
|
flutter_markdown: ^0.6.14
|
||||||
|
archive: ^3.3.7
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user