mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
19a4489d4f | |||
98e4ae1921 | |||
cdff0613b3 | |||
0f803b963f | |||
b9fffed625 | |||
918091ba54 | |||
09d729261c | |||
e50e9e40fc | |||
3f1ca3b957 | |||
8d95da46b1 | |||
a11de53c97 | |||
cf088f15ae | |||
6055ae6a69 | |||
2223616f2a | |||
dab5e4246b | |||
39b76a41cf | |||
377ebeeca2 | |||
c88f55af5a | |||
2dca74edb2 | |||
e35fd1e01e | |||
86be6a77d7 | |||
7d2f215b80 | |||
7d9a641e24 | |||
e0c69b9cf4 | |||
5f971dcddb | |||
b539f0a926 | |||
db82fe7b8f | |||
81c9f4ad47 | |||
092f81cb00 |
2
.flutter
2
.flutter
Submodule .flutter updated: b25305a883...fcf2c11572
@ -1,6 +1,6 @@
|
|||||||
#  Obtainium
|
<div align="center"><a href="https://github.com/Safouene1/support-palestine-banner/blob/master/Markdown-pages/Support.md"><img src="https://raw.githubusercontent.com/Safouene1/support-palestine-banner/master/banner-support.svg" alt="Support Palestine" style="width: 100%;"></a></div>
|
||||||
|
|
||||||
[](https://techforpalestine.org/learn-more)
|
#  Obtainium
|
||||||
|
|
||||||
Get Android app updates straight from the source.
|
Get Android app updates straight from the source.
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ Or, contribute some configurations to the website by creating a PR at [this repo
|
|||||||
|
|
||||||
Verification info:
|
Verification info:
|
||||||
- Package ID: `dev.imranr.obtainium`
|
- Package ID: `dev.imranr.obtainium`
|
||||||
- SHA-256 Hash of Signing Certificate: `B3:53:60:1F:6A:1D:5F:D6:60:3A:E2:F5:0B:E8:0C:F3:01:36:7B:86:B6:AB:8B:1F:66:24:3D:A9:6C:D5:73:62`
|
- SHA-256 hash of signing certificate: `B3:53:60:1F:6A:1D:5F:D6:60:3A:E2:F5:0B:E8:0C:F3:01:36:7B:86:B6:AB:8B:1F:66:24:3D:A9:6C:D5:73:62`
|
||||||
- Note: The above signature is also valid for the F-Droid flavour of Obtainium, thanks to [reproducible builds](https://f-droid.org/docs/Reproducible_Builds/).
|
- Note: The above signature is also valid for the F-Droid flavour of Obtainium, thanks to [reproducible builds](https://f-droid.org/docs/Reproducible_Builds/).
|
||||||
- [PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index) (to verify APK hashes)
|
- [PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index) (to verify APK hashes)
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ androidComponents {
|
|||||||
onVariants { variant ->
|
onVariants { variant ->
|
||||||
variant.outputs.forEach { output ->
|
variant.outputs.forEach { output ->
|
||||||
val name = output.filters.find { it.filterType == ABI }?.identifier
|
val name = output.filters.find { it.filterType == ABI }?.identifier
|
||||||
val baseAbiCode = abiCodes[name]
|
val baseAbiCode = abiCodes[name] ?: 0
|
||||||
if (baseAbiCode != null) {
|
if (baseAbiCode != null) {
|
||||||
output.versionCode.set(baseAbiCode + ((output.versionCode.get() ?: 0) * 10))
|
output.versionCode.set(baseAbiCode + ((output.versionCode.get() ?: 0) * 10))
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,13 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
<service
|
||||||
|
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:exported="false" />
|
||||||
|
<meta-data
|
||||||
|
android:name="dev.imranr.obtainium.service.NOTIFICATION_ICON"
|
||||||
|
android:resource="@drawable/ic_notification" />
|
||||||
</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" />
|
||||||
@ -74,6 +81,8 @@
|
|||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29" />
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "تحتوي صفحة Obtainium على GitHub المرتبطة أدناه على روابط لمقاطع فيديو، مقالات، مناقشات وموارد أخرى ستساعدك على فهم كيفية استخدام التطبيق.",
|
"documentationLinksNote": "تحتوي صفحة Obtainium على GitHub المرتبطة أدناه على روابط لمقاطع فيديو، مقالات، مناقشات وموارد أخرى ستساعدك على فهم كيفية استخدام التطبيق.",
|
||||||
"batteryOptimizationNote": "لاحظ أن التنزيلات في الخلفية قد تعمل بشكل أكثر موثوقية إذا قمت بتعطيل تحسينات بطارية النظام لـ Obtainium.",
|
"batteryOptimizationNote": "لاحظ أن التنزيلات في الخلفية قد تعمل بشكل أكثر موثوقية إذا قمت بتعطيل تحسينات بطارية النظام لـ Obtainium.",
|
||||||
"fileDeletionError": "فشل حذف الملف (حاول حذفه يدويًا ثم حاول مرة أخرى): \"{}\"",
|
"fileDeletionError": "فشل حذف الملف (حاول حذفه يدويًا ثم حاول مرة أخرى): \"{}\"",
|
||||||
|
"foregroundService": "الحصول على خدمة الحصول على خدمة المقدمة",
|
||||||
|
"foregroundServiceExplanation": "استخدام خدمة مقدمة للتحقق من التحديثات (أكثر موثوقية وتستهلك طاقة أكبر)",
|
||||||
|
"fgServiceNotice": "هذا الإشعار مطلوب للتحقق من التحديث في الخلفية (يمكن إخفاؤه في إعدادات نظام التشغيل)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "إزالة التطبيق؟",
|
"one": "إزالة التطبيق؟",
|
||||||
"other": "إزالة التطبيقات؟"
|
"other": "إزالة التطبيقات؟"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
||||||
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
||||||
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Use a foreground service for update checking (more reliable, consumes more power)",
|
||||||
|
"fgServiceNotice": "This notification is required for background update checking (it can be hidden in the OS settings)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Želite li ukloniti aplikaciju?",
|
"one": "Želite li ukloniti aplikaciju?",
|
||||||
"other": "Želite li ukloniti aplikacije?"
|
"other": "Želite li ukloniti aplikacije?"
|
||||||
|
@ -329,7 +329,10 @@
|
|||||||
"welcome": "Benvinguda",
|
"welcome": "Benvinguda",
|
||||||
"documentationLinksNote": "La pàgina GitHub d'Obtainium enllaçada a sota conté enllaços a vídeos, articles, debats i altres recursos que t'ajudaran a entendre com usar l'aplicació.",
|
"documentationLinksNote": "La pàgina GitHub d'Obtainium enllaçada a sota conté enllaços a vídeos, articles, debats i altres recursos que t'ajudaran a entendre com usar l'aplicació.",
|
||||||
"batteryOptimizationNote": "Tingues present que les descàrregues en segon pla funcionaran millor si inhabilites l'optimització de bateria per a Obtainium.",
|
"batteryOptimizationNote": "Tingues present que les descàrregues en segon pla funcionaran millor si inhabilites l'optimització de bateria per a Obtainium.",
|
||||||
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
"fileDeletionError": "No s'ha pogut suprimir el fitxer (intenta suprimir-lo manualment i torna-ho a provar): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Use a foreground service for update checking (more reliable, consumes more power)",
|
||||||
|
"fgServiceNotice": "This notification is required for background update checking (it can be hidden in the OS settings)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "¿Suprimeixo l'aplicació?",
|
"one": "¿Suprimeixo l'aplicació?",
|
||||||
"other": "¿Suprimeixo les aplicacions?"
|
"other": "¿Suprimeixo les aplicacions?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Níže odkazovaná stránka Obtainium GitHub obsahuje odkazy na videa, články, diskuse a další zdroje, které vám pomohou pochopit, jak aplikaci používat.",
|
"documentationLinksNote": "Níže odkazovaná stránka Obtainium GitHub obsahuje odkazy na videa, články, diskuse a další zdroje, které vám pomohou pochopit, jak aplikaci používat.",
|
||||||
"batteryOptimizationNote": "Všimněte si, že stahování na pozadí může fungovat spolehlivěji, pokud vypnete optimalizaci baterie operačního systému pro Obtainium.",
|
"batteryOptimizationNote": "Všimněte si, že stahování na pozadí může fungovat spolehlivěji, pokud vypnete optimalizaci baterie operačního systému pro Obtainium.",
|
||||||
"fileDeletionError": "Soubor se nepodařilo odstranit (zkuste jej odstranit ručně a pak to zkuste znovu): \"{}\"",
|
"fileDeletionError": "Soubor se nepodařilo odstranit (zkuste jej odstranit ručně a pak to zkuste znovu): \"{}\"",
|
||||||
|
"foregroundService": "Služba Obtainium na popředí",
|
||||||
|
"foregroundServiceExplanation": "Použití služby v popředí pro kontrolu aktualizací (spolehlivější, spotřebovává více energie).",
|
||||||
|
"fgServiceNotice": "Toto oznámení je nutné pro kontrolu aktualizací na pozadí (lze jej skrýt v nastavení operačního systému).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Odstranit Apku?",
|
"one": "Odstranit Apku?",
|
||||||
"other": "Odstranit Apky?"
|
"other": "Odstranit Apky?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Obtainiums GitHub-side, som der linkes til nedenfor, indeholder links til videoer, artikler, diskussioner og andre ressourcer, som kan hjælpe dig med at forstå, hvordan du bruger appen.",
|
"documentationLinksNote": "Obtainiums GitHub-side, som der linkes til nedenfor, indeholder links til videoer, artikler, diskussioner og andre ressourcer, som kan hjælpe dig med at forstå, hvordan du bruger appen.",
|
||||||
"batteryOptimizationNote": "Bemærk, at baggrundsdownloads kan fungere mere pålideligt, hvis du deaktiverer OS-batterioptimering for Obtainium.",
|
"batteryOptimizationNote": "Bemærk, at baggrundsdownloads kan fungere mere pålideligt, hvis du deaktiverer OS-batterioptimering for Obtainium.",
|
||||||
"fileDeletionError": "Kunne ikke slette filen (prøv at slette den manuelt og prøv igen): \"{}\"",
|
"fileDeletionError": "Kunne ikke slette filen (prøv at slette den manuelt og prøv igen): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium forgrundstjeneste",
|
||||||
|
"foregroundServiceExplanation": "Brug en forgrundstjeneste til opdateringskontrol (mere pålidelig, bruger mere strøm)",
|
||||||
|
"fgServiceNotice": "Denne meddelelse er nødvendig for baggrundsopdateringskontrol (den kan skjules i OS-indstillingerne).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Fjern app?",
|
"one": "Fjern app?",
|
||||||
"other": "Fjern apps?"
|
"other": "Fjern apps?"
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"noDescription": "Keine Beschreibung",
|
"noDescription": "Keine Beschreibung",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"continue": "Weiter",
|
"continue": "Weiter",
|
||||||
"requiredInBrackets": "(wird benötigt)",
|
"requiredInBrackets": "(notwendig)",
|
||||||
"dropdownNoOptsError": "FEHLER: DROPDOWN MUSS MINDESTENS EINE OPTION HABEN",
|
"dropdownNoOptsError": "FEHLER: DROPDOWN MUSS MINDESTENS EINE OPTION HABEN",
|
||||||
"colour": "Farbe",
|
"colour": "Farbe",
|
||||||
"standard": "Standard",
|
"standard": "Standard",
|
||||||
@ -32,7 +32,7 @@
|
|||||||
"source": "Quelle",
|
"source": "Quelle",
|
||||||
"app": "App",
|
"app": "App",
|
||||||
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zur Nachverfolgung.",
|
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zur Nachverfolgung.",
|
||||||
"youPickedTrackOnly": "Du hast die Option „Nur nachverfolgen“ gewählt.",
|
"youPickedTrackOnly": "Sie haben die Option „Nur nachverfolgen“ gewählt.",
|
||||||
"trackOnlyAppDescription": "Die App wird auf neue verfügbare Versionen überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
|
"trackOnlyAppDescription": "Die App wird auf neue verfügbare Versionen überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
|
||||||
"cancelled": "Abgebrochen",
|
"cancelled": "Abgebrochen",
|
||||||
"appAlreadyAdded": "App bereits hinzugefügt",
|
"appAlreadyAdded": "App bereits hinzugefügt",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"additionalOptsFor": "Zusatzoptionen für {}",
|
"additionalOptsFor": "Zusatzoptionen für {}",
|
||||||
"supportedSources": "Unterstützte Quellen",
|
"supportedSources": "Unterstützte Quellen",
|
||||||
"trackOnlyInBrackets": "(Nur nachverfolgen)",
|
"trackOnlyInBrackets": "(Nur nachverfolgen)",
|
||||||
"searchableInBrackets": "(Durchsuchbar)",
|
"searchableInBrackets": "(durchsuchbar)",
|
||||||
"appsString": "Apps",
|
"appsString": "Apps",
|
||||||
"noApps": "Keine Apps",
|
"noApps": "Keine Apps",
|
||||||
"noAppsForFilter": "Keine Apps für ausgewählten Filter",
|
"noAppsForFilter": "Keine Apps für ausgewählten Filter",
|
||||||
@ -98,26 +98,26 @@
|
|||||||
"importFromURLList": "Aus URL-Liste importieren",
|
"importFromURLList": "Aus URL-Liste importieren",
|
||||||
"searchQuery": "Suchanfrage",
|
"searchQuery": "Suchanfrage",
|
||||||
"appURLList": "App-URL-Liste",
|
"appURLList": "App-URL-Liste",
|
||||||
"line": "Linie",
|
"line": "Zeile",
|
||||||
"searchX": "{} suchen",
|
"searchX": "{} suchen",
|
||||||
"noResults": "Keine Ergebnisse gefunden",
|
"noResults": "Keine Ergebnisse gefunden",
|
||||||
"importX": "{} importieren",
|
"importX": "{} importieren",
|
||||||
"importedAppsIdDisclaimer": "Importierte Apps werden manchmal fälschlicherweise als „Nicht installiert“ angezeigt. Um dies zu beheben, installiere sie erneut über Obtainium. Dies hat keine Auswirkungen auf App-Daten. Es betrifft nur URL- und Drittanbieter-Importmethoden.",
|
"importedAppsIdDisclaimer": "Importierte Apps werden manchmal fälschlicherweise als „Nicht installiert“ angezeigt. Um dies zu beheben, installieren Sie sie erneut über Obtainium. Dies hat keine Auswirkungen auf App-Daten. Es betrifft nur URL- und Drittanbieter-Importmethoden.",
|
||||||
"importErrors": "Importfehler",
|
"importErrors": "Importfehler",
|
||||||
"importedXOfYApps": "{} von {} Apps importiert.",
|
"importedXOfYApps": "{} von {} Apps importiert.",
|
||||||
"followingURLsHadErrors": "Bei folgenden URLs traten Fehler auf:",
|
"followingURLsHadErrors": "Bei folgenden URLs traten Fehler auf:",
|
||||||
"selectURL": "URL auswählen",
|
"selectURL": "URL auswählen",
|
||||||
"selectURLs": "URLs auswählen",
|
"selectURLs": "URLs auswählen",
|
||||||
"pick": "Auswählen",
|
"pick": "Auswählen",
|
||||||
"theme": "Design",
|
"theme": "Erscheinungsbild",
|
||||||
"dark": "Dunkel",
|
"dark": "Dunkel",
|
||||||
"light": "Hell",
|
"light": "Hell",
|
||||||
"followSystem": "Systemstandard",
|
"followSystem": "Systemstandard",
|
||||||
"followSystemThemeExplanation": "Das Abrufen des Systemdesigns ist unter Android < 10 nur mit Hilfe von Drittanbieterapps möglich",
|
"followSystemThemeExplanation": "Das Abrufen des Systemdesigns ist unter Android < 10 nur mit Hilfe von Drittanbieterapps möglich",
|
||||||
"useBlackTheme": "Rein schwarzen Hintergrund verwenden",
|
"useBlackTheme": "Rein schwarzen Hintergrund verwenden",
|
||||||
"appSortBy": "App sortieren nach",
|
"appSortBy": "App sortieren nach",
|
||||||
"authorName": "Hrsg./Name",
|
"authorName": "Herausgebende/Name",
|
||||||
"nameAuthor": "Name/Hrsg.",
|
"nameAuthor": "Name/Herausgebende",
|
||||||
"asAdded": "Wie hinzugefügt",
|
"asAdded": "Wie hinzugefügt",
|
||||||
"appSortOrder": "App sortieren nach",
|
"appSortOrder": "App sortieren nach",
|
||||||
"ascending": "Aufsteigend",
|
"ascending": "Aufsteigend",
|
||||||
@ -138,12 +138,12 @@
|
|||||||
"obtainiumExportHyphenatedLowercase": "Obtainium-Export",
|
"obtainiumExportHyphenatedLowercase": "Obtainium-Export",
|
||||||
"pickAnAPK": "APK auswählen",
|
"pickAnAPK": "APK auswählen",
|
||||||
"appHasMoreThanOnePackage": "{} verfügt über mehr als ein Paket:",
|
"appHasMoreThanOnePackage": "{} verfügt über mehr als ein Paket:",
|
||||||
"deviceSupportsXArch": "Dein Gerät unterstützt die CPU-Architektur {}.",
|
"deviceSupportsXArch": "Ihr Gerät unterstützt die CPU-Architektur {}.",
|
||||||
"deviceSupportsFollowingArchs": "Dein Gerät unterstützt die folgenden CPU-Architekturen:",
|
"deviceSupportsFollowingArchs": "Ihr Gerät unterstützt die folgenden CPU-Architekturen:",
|
||||||
"warning": "Warnung",
|
"warning": "Warnung",
|
||||||
"sourceIsXButPackageFromYPrompt": "Die App-Quelle ist '{}', aber das Release-Paket stammt von '{}'. Fortfahren?",
|
"sourceIsXButPackageFromYPrompt": "Die App-Quelle ist '{}', aber das Release-Paket stammt von '{}'. Fortfahren?",
|
||||||
"updatesAvailable": "Aktualisierungen verfügbar",
|
"updatesAvailable": "Aktualisierungen verfügbar",
|
||||||
"updatesAvailableNotifDescription": "Benachrichtigt, wenn Aktualisierungen für eine oder mehrere von Obtainium verfolgte Apps verfügbar sind",
|
"updatesAvailableNotifDescription": "Benachrichtigt, wenn Aktualisierungen für eine oder mehrere von Obtainium überwachte Apps verfügbar sind",
|
||||||
"noNewUpdates": "Keine neuen Aktualisierungen.",
|
"noNewUpdates": "Keine neuen Aktualisierungen.",
|
||||||
"xHasAnUpdate": "{} hat eine Aktualisierung.",
|
"xHasAnUpdate": "{} hat eine Aktualisierung.",
|
||||||
"appsUpdated": "App wurde aktualisiert",
|
"appsUpdated": "App wurde aktualisiert",
|
||||||
@ -161,7 +161,7 @@
|
|||||||
"completeAppInstallationNotifDescription": "Aufforderung zu Obtainium zurückzukehren, um die Installation einer App abzuschließen",
|
"completeAppInstallationNotifDescription": "Aufforderung zu Obtainium zurückzukehren, um die Installation einer App abzuschließen",
|
||||||
"checkingForUpdates": "Nach Aktualisierungen suchen",
|
"checkingForUpdates": "Nach Aktualisierungen suchen",
|
||||||
"checkingForUpdatesNotifDescription": "Vorübergehende Benachrichtigung, die bei der Suche nach Aktualisierungen angezeigt wird",
|
"checkingForUpdatesNotifDescription": "Vorübergehende Benachrichtigung, die bei der Suche nach Aktualisierungen angezeigt wird",
|
||||||
"pleaseAllowInstallPerm": "Bitte erlaube Obtainium die Installation von Apps",
|
"pleaseAllowInstallPerm": "Bitte erlauben Sie Obtainium die Installation von Apps",
|
||||||
"trackOnly": "Nur nachverfolgen",
|
"trackOnly": "Nur nachverfolgen",
|
||||||
"errorWithHttpStatusCode": "Fehler {}",
|
"errorWithHttpStatusCode": "Fehler {}",
|
||||||
"versionCorrectionDisabled": "Versionskorrektur deaktiviert (Plugin scheint nicht zu funktionieren)",
|
"versionCorrectionDisabled": "Versionskorrektur deaktiviert (Plugin scheint nicht zu funktionieren)",
|
||||||
@ -189,8 +189,8 @@
|
|||||||
"downloadingX": "{} wird heruntergeladen",
|
"downloadingX": "{} wird heruntergeladen",
|
||||||
"downloadX": "{} herunterladen",
|
"downloadX": "{} herunterladen",
|
||||||
"downloadedX": "{} heruntergeladen",
|
"downloadedX": "{} heruntergeladen",
|
||||||
"releaseAsset": "Release-Asset",
|
"releaseAsset": "Release-Anhang",
|
||||||
"downloadNotifDescription": "Zeigt den Fortschritt beim Herunterladen einer App",
|
"downloadNotifDescription": "Fortschrittsanzeige beim Herunterladen einer App",
|
||||||
"noAPKFound": "Keine APK gefunden",
|
"noAPKFound": "Keine APK gefunden",
|
||||||
"noVersionDetection": "Keine Versionserkennung",
|
"noVersionDetection": "Keine Versionserkennung",
|
||||||
"categorize": "Kategorisieren",
|
"categorize": "Kategorisieren",
|
||||||
@ -221,16 +221,16 @@
|
|||||||
"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",
|
||||||
"autoLinkFilterByArch": "Versuchen Sie, Links nach CPU-Architektur zu filtern, wenn möglich",
|
"autoLinkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
|
||||||
"overrideSource": "Quelle überschreiben",
|
"overrideSource": "Quelle überschreiben",
|
||||||
"dontShowAgain": "Nicht noch einmal anzeigen",
|
"dontShowAgain": "Nicht noch einmal anzeigen",
|
||||||
"dontShowTrackOnlyWarnings": "Warnung für „Nur nachverfolgen“ nicht anzeigen",
|
"dontShowTrackOnlyWarnings": "Warnung für ‚Nur nachverfolgen‘ nicht anzeigen",
|
||||||
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
||||||
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der App-Ansicht verschieben",
|
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der App-Ansicht verschieben",
|
||||||
"gitlabPATLabel": "Persönlicher Zugangstoken für GitLab",
|
"gitlabPATLabel": "Persönlicher Zugangstoken für GitLab",
|
||||||
"about": "Über",
|
"about": "Weitere Informationen",
|
||||||
"requiresCredentialsInSettings": "{}: Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
"requiresCredentialsInSettings": "{}: Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
||||||
"checkOnStart": "Einmalig beim Start überprüfen",
|
"checkOnStart": "Einmalig beim Start auf Aktualisierungen prüfen",
|
||||||
"tryInferAppIdFromCode": "Versuchen, die App-ID aus dem Quellcode zu ermitteln",
|
"tryInferAppIdFromCode": "Versuchen, die App-ID aus dem Quellcode zu ermitteln",
|
||||||
"removeOnExternalUninstall": "Extern deinstallierte Apps automatische entfernen",
|
"removeOnExternalUninstall": "Extern deinstallierte Apps automatische entfernen",
|
||||||
"pickHighestVersionCode": "Automatische Auswahl des APK mit höchstem Versionscode",
|
"pickHighestVersionCode": "Automatische Auswahl des APK mit höchstem Versionscode",
|
||||||
@ -246,7 +246,7 @@
|
|||||||
"customLinkFilterRegex": "Benutzerdefinierter APK-Linkfilter durch regulären Ausdruck (Standard '.apk$')",
|
"customLinkFilterRegex": "Benutzerdefinierter APK-Linkfilter durch regulären Ausdruck (Standard '.apk$')",
|
||||||
"appsPossiblyUpdated": "App-Aktualisierungen wurden versucht",
|
"appsPossiblyUpdated": "App-Aktualisierungen wurden versucht",
|
||||||
"appsPossiblyUpdatedNotifDescription": "Benachrichtigt, dass möglicherweise eine oder mehrere Apps im Hintergrund aktualisiert wurden",
|
"appsPossiblyUpdatedNotifDescription": "Benachrichtigt, dass möglicherweise eine oder mehrere Apps im Hintergrund aktualisiert wurden",
|
||||||
"xWasPossiblyUpdatedToY": "{} wurde eventuell auf Version {} aktualisiert.",
|
"xWasPossiblyUpdatedToY": "{} wurde möglicherweise auf Version {} aktualisiert.",
|
||||||
"enableBackgroundUpdates": "Hintergrundaktualisierungen aktivieren",
|
"enableBackgroundUpdates": "Hintergrundaktualisierungen aktivieren",
|
||||||
"backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist unter Umständen nicht für alle Apps möglich.",
|
"backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist unter Umständen nicht für alle Apps möglich.",
|
||||||
"backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
|
"backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
|
||||||
@ -287,19 +287,19 @@
|
|||||||
"downloadingXNotifChannel": "{} herunterladen",
|
"downloadingXNotifChannel": "{} herunterladen",
|
||||||
"completeAppInstallationNotifChannel": "App-Installation abschließen",
|
"completeAppInstallationNotifChannel": "App-Installation abschließen",
|
||||||
"checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
|
"checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
|
||||||
"onlyCheckInstalledOrTrackOnlyApps": "Nur installierte und mit „Nur nachverfolgen“ markierte Apps auf Aktualisierungen prüfen",
|
"onlyCheckInstalledOrTrackOnlyApps": "Nur mit „Nur nachverfolgen“ markierte und installierte Apps auf Aktualisierungen prüfen",
|
||||||
"supportFixedAPKURL": "Unterstützung von festen APK-URLs",
|
"supportFixedAPKURL": "Unterstützung von festen APK-URLs",
|
||||||
"selectX": "{} wählen",
|
"selectX": "{} wählen",
|
||||||
"parallelDownloads": "Parallele Downloads erlauben",
|
"parallelDownloads": "Parallele Downloads erlauben",
|
||||||
"useShizuku": "Shizuku oder Sui zur Installation verwenden",
|
"useShizuku": "Shizuku oder Sui zur Installation verwenden",
|
||||||
"shizukuBinderNotFound": "Kompatibler Shizuku-Dienst wurde nicht gefunden",
|
"shizukuBinderNotFound": "Kompatibler Shizuku-Dienst wurde nicht gefunden",
|
||||||
"shizukuOld": "Veraltete Shizuku-Version (< 11) - bitte aktualisiere sie",
|
"shizukuOld": "Veraltete Shizuku-Version (< 11) – bitte aktualisieren Sie sie",
|
||||||
"shizukuOldAndroidWithADB": "Shizuku läuft auf Android < 8.1 mit ADB - aktualisiere die Android-Version oder verwende stattdessen Sui",
|
"shizukuOldAndroidWithADB": "Shizuku läuft auf Android < 8.1 mit ADB – aktualisieren Sie die Android-Version oder verwenden Sie stattdessen Sui",
|
||||||
"shizukuPretendToBeGooglePlay": "(Mittels Shizuku) Google Play als Installationsquelle registrieren",
|
"shizukuPretendToBeGooglePlay": "(Mittels Shizuku) Google Play als Installationsquelle registrieren",
|
||||||
"useSystemFont": "Systemschriftart verwenden",
|
"useSystemFont": "Systemschriftart verwenden",
|
||||||
"useVersionCodeAsOSVersion": "Versionscode (versionCode) als erkannte Version vom Betriebssystem verwenden",
|
"useVersionCodeAsOSVersion": "Versionscode (versionCode) als vom Betriebssystem erkannte Version verwenden",
|
||||||
"requestHeader": "Kopfzeile („Header“) anfordern",
|
"requestHeader": "Kopfzeile („Header“) anfordern",
|
||||||
"useLatestAssetDateAsReleaseDate": "Letzten Asset-Upload als Veröffentlichungsdatum verwenden",
|
"useLatestAssetDateAsReleaseDate": "Letzten Upload eines Release-Anhangs als Veröffentlichungsdatum verwenden",
|
||||||
"defaultPseudoVersioningMethod": "Standardmäßiges Verfahren zur Pseudo-Versionierung",
|
"defaultPseudoVersioningMethod": "Standardmäßiges Verfahren zur Pseudo-Versionierung",
|
||||||
"partialAPKHash": "Partieller APK-Hash",
|
"partialAPKHash": "Partieller APK-Hash",
|
||||||
"APKLinkHash": "APK-Link-Hash",
|
"APKLinkHash": "APK-Link-Hash",
|
||||||
@ -322,7 +322,7 @@
|
|||||||
"refreshBeforeDownload": "App-Details vor dem Download aktualisieren",
|
"refreshBeforeDownload": "App-Details vor dem Download aktualisieren",
|
||||||
"tencentAppStore": "Tencent App Store",
|
"tencentAppStore": "Tencent App Store",
|
||||||
"coolApk": "CoolApk",
|
"coolApk": "CoolApk",
|
||||||
"vivoAppStore": "vivo App Store (CN)",
|
"vivoAppStore": "vivo App Store (China)",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"smartname": "Name (Smart)",
|
"smartname": "Name (Smart)",
|
||||||
"sortMethod": "Sortierverfahren",
|
"sortMethod": "Sortierverfahren",
|
||||||
@ -330,13 +330,16 @@
|
|||||||
"documentationLinksNote": "Die unten verlinkte GitHub-Seite von Obtainium enthält Links zu Videos, Artikeln, Diskussionen und anderen Ressourcen, die Ihnen helfen werden, die Verwendung der App zu verstehen.",
|
"documentationLinksNote": "Die unten verlinkte GitHub-Seite von Obtainium enthält Links zu Videos, Artikeln, Diskussionen und anderen Ressourcen, die Ihnen helfen werden, die Verwendung der App zu verstehen.",
|
||||||
"batteryOptimizationNote": "Beachten Sie, dass Downloads im Hintergrund möglicherweise zuverlässiger funktionieren, wenn Sie die Batterieoptimierung des Betriebssystems für Obtainium deaktivieren.",
|
"batteryOptimizationNote": "Beachten Sie, dass Downloads im Hintergrund möglicherweise zuverlässiger funktionieren, wenn Sie die Batterieoptimierung des Betriebssystems für Obtainium deaktivieren.",
|
||||||
"fileDeletionError": "Die Datei konnte nicht gelöscht werden (versuchen Sie, sie manuell zu löschen und versuchen Sie es dann erneut): \"{}\"",
|
"fileDeletionError": "Die Datei konnte nicht gelöscht werden (versuchen Sie, sie manuell zu löschen und versuchen Sie es dann erneut): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium-Vordergrunddienst",
|
||||||
|
"foregroundServiceExplanation": "Verwendung eines Dienstes im Vordergrund für die Aktualisierungsprüfung (zuverlässiger, verbraucht mehr Strom)",
|
||||||
|
"fgServiceNotice": "Diese Benachrichtigung ist für die Prüfung von Updates im Hintergrund erforderlich (sie kann in den Betriebssystemeinstellungen ausgeblendet werden)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App entfernen?",
|
"one": "App entfernen?",
|
||||||
"other": "Apps entfernen?"
|
"other": "Apps entfernen?"
|
||||||
},
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Zu viele Anfragen (Rate begrenzt) – versuche es in {} Minute erneut",
|
"one": "Zu viele Anfragen (Rate begrenzt) – versuchen Sie es in {} Minute erneut",
|
||||||
"other": "Zu viele Anfragen (Rate begrenzt) – versuche es in {} Minuten erneut"
|
"other": "Zu viele Anfragen (Rate begrenzt) – versuchen Sie es in {} Minuten erneut"
|
||||||
},
|
},
|
||||||
"bgUpdateGotErrorRetryInMinutes": {
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
"one": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minute geplant",
|
"one": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minute geplant",
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
||||||
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
||||||
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Use a foreground service for update checking (more reliable, consumes more power)",
|
||||||
|
"fgServiceNotice": "This notification is required for background update checking (it can be hidden in the OS settings)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Forigi la aplikaĵon?",
|
"one": "Forigi la aplikaĵon?",
|
||||||
"other": "Forigi la aplikaĵojn?"
|
"other": "Forigi la aplikaĵojn?"
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
"cancelled": "Cancelled",
|
"cancelled": "Cancelled",
|
||||||
"appAlreadyAdded": "App already added",
|
"appAlreadyAdded": "App already added",
|
||||||
"alreadyUpToDateQuestion": "App already up to date?",
|
"alreadyUpToDateQuestion": "App already up to date?",
|
||||||
"addApp": "Add App",
|
"addApp": "Add app",
|
||||||
"appSourceURL": "App source URL",
|
"appSourceURL": "App source URL",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"author": "Author",
|
"author": "Author",
|
||||||
"upToDateApps": "Up to date apps",
|
"upToDateApps": "Up to date apps",
|
||||||
"nonInstalledApps": "Non-installed apps",
|
"nonInstalledApps": "Non-installed apps",
|
||||||
"importExport": "Import/Export",
|
"importExport": "Import/export",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"exportedTo": "Exported to {}",
|
"exportedTo": "Exported to {}",
|
||||||
"obtainiumExport": "Obtainium export",
|
"obtainiumExport": "Obtainium export",
|
||||||
@ -328,8 +328,11 @@
|
|||||||
"sortMethod": "Sort method",
|
"sortMethod": "Sort method",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
||||||
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
"batteryOptimizationNote": "Note that background downloads may work more reliably if you switch to the \"foreground service\" in the Obtainium settings and/or disable battery optimization for Obtainium in your OS settings.",
|
||||||
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Use a foreground service for update checking (more reliable, consumes more power)",
|
||||||
|
"fgServiceNotice": "This notification is required for background update checking (it can be hidden in the OS settings)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remove app?",
|
"one": "Remove app?",
|
||||||
"other": "Remove apps?"
|
"other": "Remove apps?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "La página GitHub de Obtainium enlazada a continuación contiene enlaces a vídeos, artículos, debates y otros recursos que te ayudarán a entender cómo utilizar la aplicación.",
|
"documentationLinksNote": "La página GitHub de Obtainium enlazada a continuación contiene enlaces a vídeos, artículos, debates y otros recursos que te ayudarán a entender cómo utilizar la aplicación.",
|
||||||
"batteryOptimizationNote": "Ten en cuenta que las descargas en segundo plano pueden funcionar de forma más fiable si desactivas las optimizaciones de batería del sistema operativo para Obtainium.",
|
"batteryOptimizationNote": "Ten en cuenta que las descargas en segundo plano pueden funcionar de forma más fiable si desactivas las optimizaciones de batería del sistema operativo para Obtainium.",
|
||||||
"fileDeletionError": "No se ha podido eliminar el archivo (intente eliminarlo manualmente y vuelva a intentarlo): \"{}\"",
|
"fileDeletionError": "No se ha podido eliminar el archivo (intente eliminarlo manualmente y vuelva a intentarlo): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium servicio en primer plano",
|
||||||
|
"foregroundServiceExplanation": "Utilizar un servicio en primer plano para comprobar las actualizaciones (más fiable, consume más energía).",
|
||||||
|
"fgServiceNotice": "Esta notificación es necesaria para la comprobación de actualizaciones en segundo plano (puede ocultarse en la configuración del sistema operativo).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "¿Eliminar aplicación?",
|
"one": "¿Eliminar aplicación?",
|
||||||
"other": "¿Eliminar aplicaciones?"
|
"other": "¿Eliminar aplicaciones?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
||||||
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
||||||
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Use a foreground service for update checking (more reliable, consumes more power)",
|
||||||
|
"fgServiceNotice": "This notification is required for background update checking (it can be hidden in the OS settings)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "برنامه حذف شود؟",
|
"one": "برنامه حذف شود؟",
|
||||||
"other": "برنامه ها حذف شوند؟"
|
"other": "برنامه ها حذف شوند؟"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "La page GitHub d'Obtainium, dont le lien figure ci-dessous, contient des liens vers des vidéos, des articles, des discussions et d'autres ressources qui vous aideront à comprendre comment utiliser l'application.",
|
"documentationLinksNote": "La page GitHub d'Obtainium, dont le lien figure ci-dessous, contient des liens vers des vidéos, des articles, des discussions et d'autres ressources qui vous aideront à comprendre comment utiliser l'application.",
|
||||||
"batteryOptimizationNote": "Notez que les téléchargements en arrière-plan peuvent fonctionner de manière plus fiable si vous désactivez les optimisations de la batterie du système d'exploitation pour Obtainium.",
|
"batteryOptimizationNote": "Notez que les téléchargements en arrière-plan peuvent fonctionner de manière plus fiable si vous désactivez les optimisations de la batterie du système d'exploitation pour Obtainium.",
|
||||||
"fileDeletionError": "Échec de la suppression du fichier (essayez de le supprimer manuellement puis réessayez) : \"{}\"",
|
"fileDeletionError": "Échec de la suppression du fichier (essayez de le supprimer manuellement puis réessayez) : \"{}\"",
|
||||||
|
"foregroundService": "Service d'avant-plan Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Utiliser un service de premier plan pour la vérification des mises à jour (plus fiable, consomme plus d'énergie)",
|
||||||
|
"fgServiceNotice": "Cette notification est nécessaire pour la vérification des mises à jour en arrière-plan (elle peut être masquée dans les paramètres du système d'exploitation).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Supprimer l'application ?",
|
"one": "Supprimer l'application ?",
|
||||||
"other": "Supprimer les applications ?"
|
"other": "Supprimer les applications ?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Az alábbi hivatkozás az Obtainium GitHub oldalára vezet, amely további videók, cikkek, beszélgetések és egyéb források hivatkozásait tartalmazza, amelyek segítenek megérteni az alkalmazás használatát.",
|
"documentationLinksNote": "Az alábbi hivatkozás az Obtainium GitHub oldalára vezet, amely további videók, cikkek, beszélgetések és egyéb források hivatkozásait tartalmazza, amelyek segítenek megérteni az alkalmazás használatát.",
|
||||||
"batteryOptimizationNote": "Megjegyzés: A háttérfrissítések megbízhatóbban működhetnek, ha kikapcsolja a rendszer akkumulátor-optimalizálását az Obtainium számára.",
|
"batteryOptimizationNote": "Megjegyzés: A háttérfrissítések megbízhatóbban működhetnek, ha kikapcsolja a rendszer akkumulátor-optimalizálását az Obtainium számára.",
|
||||||
"fileDeletionError": "Nem sikerült törölni a fájlt (próbálja meg kézzel törölni, majd próbálja meg újra): \"{}\"",
|
"fileDeletionError": "Nem sikerült törölni a fájlt (próbálja meg kézzel törölni, majd próbálja meg újra): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium új szolgáltatás",
|
||||||
|
"foregroundServiceExplanation": "Frissítésellenőrzéshez előtérben lévő szolgáltatás használata (megbízhatóbb, de több energiát fogyaszt)",
|
||||||
|
"fgServiceNotice": "Ez az értesítés a háttérben történő frissítésellenőrzéshez szükséges (az operációs rendszer beállításaiban elrejthető).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Eltávolítja az alkalmazást?",
|
"one": "Eltávolítja az alkalmazást?",
|
||||||
"other": "Eltávolítja az alkalmazásokat?"
|
"other": "Eltávolítja az alkalmazásokat?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Halaman GitHub Obtainium yang ditautkan di bawah ini berisi tautan ke video, artikel, diskusi, dan sumber daya lain yang akan membantu Anda memahami cara menggunakan aplikasi.",
|
"documentationLinksNote": "Halaman GitHub Obtainium yang ditautkan di bawah ini berisi tautan ke video, artikel, diskusi, dan sumber daya lain yang akan membantu Anda memahami cara menggunakan aplikasi.",
|
||||||
"batteryOptimizationNote": "Perhatikan bahwa unduhan latar belakang dapat bekerja lebih andal jika Anda menonaktifkan optimasi baterai OS untuk Obtainium.",
|
"batteryOptimizationNote": "Perhatikan bahwa unduhan latar belakang dapat bekerja lebih andal jika Anda menonaktifkan optimasi baterai OS untuk Obtainium.",
|
||||||
"fileDeletionError": "Gagal menghapus file (coba hapus secara manual, lalu coba lagi): \"{}\"",
|
"fileDeletionError": "Gagal menghapus file (coba hapus secara manual, lalu coba lagi): \"{}\"",
|
||||||
|
"foregroundService": "Layanan latar depan Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Gunakan layanan latar depan untuk pemeriksaan pembaruan (lebih dapat diandalkan, menghabiskan lebih banyak daya)",
|
||||||
|
"fgServiceNotice": "Pemberitahuan ini diperlukan untuk pemeriksaan pembaruan latar belakang (dapat disembunyikan dalam pengaturan OS)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Hapus aplikasi?",
|
"one": "Hapus aplikasi?",
|
||||||
"other": "Hapus aplikasi?"
|
"other": "Hapus aplikasi?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "La pagina GitHub di Obtainium collegata qui sotto contiene collegamenti a video, articoli, discussioni e altre risorse che vi aiuteranno a capire come utilizzare l'applicazione.",
|
"documentationLinksNote": "La pagina GitHub di Obtainium collegata qui sotto contiene collegamenti a video, articoli, discussioni e altre risorse che vi aiuteranno a capire come utilizzare l'applicazione.",
|
||||||
"batteryOptimizationNote": "Si noti che i download in background potrebbero funzionare in modo più affidabile se si disabilita l'ottimizzazione della batteria del sistema operativo per Obtainium.",
|
"batteryOptimizationNote": "Si noti che i download in background potrebbero funzionare in modo più affidabile se si disabilita l'ottimizzazione della batteria del sistema operativo per Obtainium.",
|
||||||
"fileDeletionError": "Errore nell'eliminazione del file (provare a cancellarlo manualmente e poi riprovare): \"{}\"",
|
"fileDeletionError": "Errore nell'eliminazione del file (provare a cancellarlo manualmente e poi riprovare): \"{}\"",
|
||||||
|
"foregroundService": "Servizio Obtainium in primo piano",
|
||||||
|
"foregroundServiceExplanation": "Utilizzare un servizio in primo piano per il controllo degli aggiornamenti (più affidabile, consuma più energia)",
|
||||||
|
"fgServiceNotice": "Questa notifica è necessaria per il controllo degli aggiornamenti in background (può essere nascosta nelle impostazioni del sistema operativo).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Rimuovere l'app?",
|
"one": "Rimuovere l'app?",
|
||||||
"other": "Rimuovere le app?"
|
"other": "Rimuovere le app?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "以下のリンクにあるObtainium GitHubページには、ビデオ、記事、ディスカッション、その他のリソースへのリンクがあり、アプリの使い方を理解するのに役立ちます。",
|
"documentationLinksNote": "以下のリンクにあるObtainium GitHubページには、ビデオ、記事、ディスカッション、その他のリソースへのリンクがあり、アプリの使い方を理解するのに役立ちます。",
|
||||||
"batteryOptimizationNote": "ObtainiumのOSバッテリー最適化を無効にすると、バックグラウンドダウンロードがより確実に動作するようになります。",
|
"batteryOptimizationNote": "ObtainiumのOSバッテリー最適化を無効にすると、バックグラウンドダウンロードがより確実に動作するようになります。",
|
||||||
"fileDeletionError": "ファイルの削除に失敗しました(手動で削除してから再試行してください):\"{}\"",
|
"fileDeletionError": "ファイルの削除に失敗しました(手動で削除してから再試行してください):\"{}\"",
|
||||||
|
"foregroundService": "フォアグラウンド・サービス",
|
||||||
|
"foregroundServiceExplanation": "更新チェックにフォアグラウンド・サービスを使用する(より信頼性が高いが、より電力を消費する)",
|
||||||
|
"fgServiceNotice": "この通知は、バックグラウンドでアップデートをチェックするために必要です(OSの設定で非表示にできます)。",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "アプリを削除しますか?",
|
"one": "アプリを削除しますか?",
|
||||||
"other": "アプリを削除しますか?"
|
"other": "アプリを削除しますか?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "아래에 링크된 Obtainium 깃허브 페이지에는 앱 사용 방법을 이해하는 데 도움이 되는 동영상, 기사, 토론 및 기타 리소스에 대한 링크가 포함되어 있습니다.",
|
"documentationLinksNote": "아래에 링크된 Obtainium 깃허브 페이지에는 앱 사용 방법을 이해하는 데 도움이 되는 동영상, 기사, 토론 및 기타 리소스에 대한 링크가 포함되어 있습니다.",
|
||||||
"batteryOptimizationNote": "Obtainium의 OS 배터리 최적화를 비활성화하면 백그라운드 다운로드가 더 안정적으로 작동할 수 있습니다.",
|
"batteryOptimizationNote": "Obtainium의 OS 배터리 최적화를 비활성화하면 백그라운드 다운로드가 더 안정적으로 작동할 수 있습니다.",
|
||||||
"fileDeletionError": "파일을 삭제하지 못했습니다(수동으로 삭제한 후 다시 시도하세요): \"{}\"",
|
"fileDeletionError": "파일을 삭제하지 못했습니다(수동으로 삭제한 후 다시 시도하세요): \"{}\"",
|
||||||
|
"foregroundService": "오베티움 포그라운드 서비스",
|
||||||
|
"foregroundServiceExplanation": "업데이트 확인을 위해 포그라운드 서비스 사용(안정성 향상, 전력 소비량 증가)",
|
||||||
|
"fgServiceNotice": "이 알림은 백그라운드 업데이트 확인에 필요합니다(OS 설정에서 숨길 수 있음).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "앱을 제거하시겠습니까?",
|
"one": "앱을 제거하시겠습니까?",
|
||||||
"other": "앱을 제거하시겠습니까?"
|
"other": "앱을 제거하시겠습니까?"
|
||||||
|
396
assets/translations/ml.json
Normal file
396
assets/translations/ml.json
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "ഒരു സാധുവായ {} ആപ്പ് URL അല്ല",
|
||||||
|
"noReleaseFound": "അനുയോജ്യമായ റിലീസ് കണ്ടെത്താനായില്ല",
|
||||||
|
"noVersionFound": "റിലീസ് പതിപ്പ് നിർണ്ണയിക്കാനായില്ല",
|
||||||
|
"urlMatchesNoSource": "URL അറിയപ്പെടുന്ന ഒരു ഉറവിടവുമായും യോജിക്കുന്നില്ല",
|
||||||
|
"cantInstallOlderVersion": "ആപ്പിന്റെ പഴയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാൻ കഴിയില്ല.",
|
||||||
|
"appIdMismatch": "ഡൗൺലോഡ് ചെയ്ത പാക്കേജ് ഐഡി നിലവിലുള്ള ആപ്പ് ഐഡിയുമായി ചേരുന്നില്ല",
|
||||||
|
"functionNotImplemented": "ഈ ക്ലാസ് ഈ ഫംഗ്ഷൻ നടപ്പിലാക്കിയിട്ടില്ല",
|
||||||
|
"placeholder": "സ്ഥലസൂചകം",
|
||||||
|
"someErrors": "ചില പിശകുകൾ സംഭവിച്ചു",
|
||||||
|
"unexpectedError": "പ്രതീക്ഷിക്കാത്ത പിശക്",
|
||||||
|
"ok": "ശരി",
|
||||||
|
"and": "കൂടാതെ",
|
||||||
|
"githubPATLabel": "GitHub പേഴ്സണൽ ആക്സസ് ടോക്കൺ (നിരക്ക് പരിധി വർദ്ധിപ്പിക്കുന്നു)",
|
||||||
|
"includePrereleases": "പ്രീ-റിലീസുകൾ ഉൾപ്പെടുത്തുക",
|
||||||
|
"fallbackToOlderReleases": "പഴയ റിലീസുകളിലേക്ക് മടങ്ങുക",
|
||||||
|
"filterReleaseTitlesByRegEx": "റിലീസ് ശീർഷകങ്ങൾ റെഗുലർ എക്സ്പ്രഷൻ ഉപയോഗിച്ച് ഫിൽട്ടർ ചെയ്യുക",
|
||||||
|
"invalidRegEx": "സാധുവായ റെഗുലർ എക്സ്പ്രഷൻ അല്ല",
|
||||||
|
"noDescription": "വിവരണം ലഭ്യമല്ല",
|
||||||
|
"cancel": "റദ്ദാക്കുക",
|
||||||
|
"continue": "തുടരുക",
|
||||||
|
"requiredInBrackets": "(ആവശ്യമാണ്)",
|
||||||
|
"dropdownNoOptsError": "തെറ്റ്: ഡ്രോപ്പ്ഡൗണിന് കുറഞ്ഞത് ഒരു ഓപ്ഷനെങ്കിലും ഉണ്ടായിരിക്കണം",
|
||||||
|
"colour": "നിറം",
|
||||||
|
"standard": "സാധാരണ",
|
||||||
|
"custom": "ഇഷ്ടാനുസൃതം",
|
||||||
|
"useMaterialYou": "മെറ്റീരിയൽ യൂ ഉപയോഗിക്കുക",
|
||||||
|
"githubStarredRepos": "GitHub സ്റ്റാർ ചെയ്ത റെപ്പോസിറ്ററികൾ",
|
||||||
|
"uname": "ഉപയോക്തൃനാമം",
|
||||||
|
"wrongArgNum": "തെറ്റായ എണ്ണം ആർഗ്യുമെന്റുകൾ നൽകി",
|
||||||
|
"xIsTrackOnly": "{} ട്രാക്ക്-മാത്രം ആണ്",
|
||||||
|
"source": "ഉറവിടം",
|
||||||
|
"app": "ആപ്പ്",
|
||||||
|
"appsFromSourceAreTrackOnly": "ഈ ഉറവിടത്തിൽ നിന്നുള്ള ആപ്പുകൾ 'ട്രാക്ക്-മാത്രം' ആണ്.",
|
||||||
|
"youPickedTrackOnly": "നിങ്ങൾ 'ട്രാക്ക്-മാത്രം' ഓപ്ഷൻ തിരഞ്ഞെടുത്തു.",
|
||||||
|
"trackOnlyAppDescription": "ആപ്പ് അപ്ഡേറ്റുകൾക്കായി ട്രാക്ക് ചെയ്യപ്പെടും, പക്ഷേ ഒബ്റ്റേനിയത്തിന് അത് ഡൗൺലോഡ് ചെയ്യാനോ ഇൻസ്റ്റാൾ ചെയ്യാനോ കഴിയില്ല.",
|
||||||
|
"cancelled": "റദ്ദാക്കി",
|
||||||
|
"appAlreadyAdded": "ആപ്പ് ഇതിനകം കൂട്ടിച്ചേർത്തിട്ടുണ്ട്",
|
||||||
|
"alreadyUpToDateQuestion": "ആപ്പ് ഇതിനകം ഏറ്റവും പുതിയ പതിപ്പിലാണോ?",
|
||||||
|
"addApp": "ആപ്പ് ചേർക്കുക",
|
||||||
|
"appSourceURL": "ആപ്പ് ഉറവിട URL",
|
||||||
|
"error": "പിശക്",
|
||||||
|
"add": "ചേർക്കുക",
|
||||||
|
"searchSomeSourcesLabel": "തിരയുക (ചില ഉറവിടങ്ങളിൽ മാത്രം)",
|
||||||
|
"search": "തിരയുക",
|
||||||
|
"additionalOptsFor": "{} നായുള്ള അധിക ഓപ്ഷനുകൾ",
|
||||||
|
"supportedSources": "പിന്തുണയ്ക്കുന്ന ഉറവിടങ്ങൾ",
|
||||||
|
"trackOnlyInBrackets": "(ട്രാക്ക്-മാത്രം)",
|
||||||
|
"searchableInBrackets": "(തിരയാവുന്നത്)",
|
||||||
|
"appsString": "ആപ്പുകൾ",
|
||||||
|
"noApps": "ആപ്പുകളൊന്നുമില്ല",
|
||||||
|
"noAppsForFilter": "ഈ ഫിൽട്ടറിനായി ആപ്പുകളൊന്നുമില്ല",
|
||||||
|
"byX": "{} വഴി",
|
||||||
|
"percentProgress": "പുരോഗതി: {}%",
|
||||||
|
"pleaseWait": "ദയവായി കാത്തിരിക്കുക",
|
||||||
|
"updateAvailable": "അപ്ഡേറ്റ് ലഭ്യമാണ്",
|
||||||
|
"notInstalled": "ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല",
|
||||||
|
"pseudoVersion": "സ്യൂഡോ-പതിപ്പ്",
|
||||||
|
"selectAll": "എല്ലാം തിരഞ്ഞെടുക്കുക",
|
||||||
|
"deselectX": "{} തിരഞ്ഞെടുക്കൽ റദ്ദാക്കുക",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{} ഒബ്റ്റേനിയത്തിൽ നിന്ന് നീക്കം ചെയ്യപ്പെടും, പക്ഷേ ഉപകരണത്തിൽ ഇൻസ്റ്റാൾ ചെയ്ത നിലയിൽ തുടരും.",
|
||||||
|
"removeSelectedAppsQuestion": "തിരഞ്ഞെടുത്ത ആപ്പുകൾ നീക്കം ചെയ്യണോ?",
|
||||||
|
"removeSelectedApps": "തിരഞ്ഞെടുത്ത ആപ്പുകൾ നീക്കം ചെയ്യുക",
|
||||||
|
"updateX": "{} അപ്ഡേറ്റ് ചെയ്യുക",
|
||||||
|
"installX": "{} ഇൻസ്റ്റാൾ ചെയ്യുക",
|
||||||
|
"markXTrackOnlyAsUpdated": "{}\n(ട്രാക്ക്-മാത്രം)\nഅപ്ഡേറ്റ് ചെയ്തതായി അടയാളപ്പെടുത്തുക",
|
||||||
|
"changeX": "{} മാറ്റുക",
|
||||||
|
"installUpdateApps": "ആപ്പുകൾ ഇൻസ്റ്റാൾ ചെയ്യുക/അപ്ഡേറ്റ് ചെയ്യുക",
|
||||||
|
"installUpdateSelectedApps": "തിരഞ്ഞെടുത്ത ആപ്പുകൾ ഇൻസ്റ്റാൾ ചെയ്യുക/അപ്ഡേറ്റ് ചെയ്യുക",
|
||||||
|
"markXSelectedAppsAsUpdated": "{} തിരഞ്ഞെടുത്ത ആപ്പുകൾ അപ്ഡേറ്റ് ചെയ്തതായി അടയാളപ്പെടുത്തണോ?",
|
||||||
|
"no": "ഇല്ല",
|
||||||
|
"yes": "അതെ",
|
||||||
|
"markSelectedAppsUpdated": "തിരഞ്ഞെടുത്ത ആപ്പുകൾ അപ്ഡേറ്റ് ചെയ്തതായി അടയാളപ്പെടുത്തുക",
|
||||||
|
"pinToTop": "മുകളിൽ പിൻ ചെയ്യുക",
|
||||||
|
"unpinFromTop": "മുകളിൽ നിന്ന് അൺപിൻ ചെയ്യുക",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "തിരഞ്ഞെടുത്ത ആപ്പുകളുടെ ഇൻസ്റ്റാൾ സ്റ്റാറ്റസ് റീസെറ്റ് ചെയ്യണോ?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "തിരഞ്ഞെടുത്ത ഏതെങ്കിലും ആപ്പുകളുടെ ഇൻസ്റ്റാൾ സ്റ്റാറ്റസ് റീസെറ്റ് ചെയ്യപ്പെടും.\n\nപരാജയപ്പെട്ട അപ്ഡേറ്റുകളോ മറ്റ് പ്രശ്നങ്ങളോ കാരണം ഒബ്റ്റേനിയത്തിൽ കാണിക്കുന്ന ആപ്പ് പതിപ്പ് തെറ്റായിരിക്കുമ്പോൾ ഇത് സഹായകമാകും.",
|
||||||
|
"customLinkMessage": "ഈ ലിങ്കുകൾ ഒബ്റ്റേനിയം ഇൻസ്റ്റാൾ ചെയ്ത ഉപകരണങ്ങളിൽ പ്രവർത്തിക്കും",
|
||||||
|
"shareAppConfigLinks": "ആപ്പ് കോൺഫിഗറേഷൻ HTML ലിങ്കായി പങ്കിടുക",
|
||||||
|
"shareSelectedAppURLs": "തിരഞ്ഞെടുത്ത ആപ്പ് URL-കൾ പങ്കിടുക",
|
||||||
|
"resetInstallStatus": "ഇൻസ്റ്റാൾ സ്റ്റാറ്റസ് റീസെറ്റ് ചെയ്യുക",
|
||||||
|
"more": "കൂടുതൽ",
|
||||||
|
"removeOutdatedFilter": "കാലഹരണപ്പെട്ട ആപ്പ് ഫിൽട്ടർ നീക്കം ചെയ്യുക",
|
||||||
|
"showOutdatedOnly": "കാലഹരണപ്പെട്ട ആപ്പുകൾ മാത്രം കാണിക്കുക",
|
||||||
|
"filter": "ഫിൽട്ടർ",
|
||||||
|
"filterApps": "ആപ്പുകൾ ഫിൽട്ടർ ചെയ്യുക",
|
||||||
|
"appName": "ആപ്പിന്റെ പേര്",
|
||||||
|
"author": "ഡെവലപ്പർ",
|
||||||
|
"upToDateApps": "ഏറ്റവും പുതിയ പതിപ്പിലുള്ള ആപ്പുകൾ",
|
||||||
|
"nonInstalledApps": "ഇൻസ്റ്റാൾ ചെയ്യാത്ത ആപ്പുകൾ",
|
||||||
|
"importExport": "ഇംപോർട്ട്/എക്സ്പോർട്ട്",
|
||||||
|
"settings": "ക്രമീകരണങ്ങൾ",
|
||||||
|
"exportedTo": "{} ലേക്ക് എക്സ്പോർട്ട് ചെയ്തു",
|
||||||
|
"obtainiumExport": "ഒബ്റ്റേനിയം എക്സ്പോർട്ട്",
|
||||||
|
"invalidInput": "തെറ്റായ ഇൻപുട്ട്",
|
||||||
|
"importedX": "{} ഇംപോർട്ട് ചെയ്തു",
|
||||||
|
"obtainiumImport": "ഒബ്റ്റേനിയം ഇംപോർട്ട്",
|
||||||
|
"importFromURLList": "URL ലിസ്റ്റിൽ നിന്ന് ഇംപോർട്ട് ചെയ്യുക",
|
||||||
|
"searchQuery": "തിരയൽ ചോദ്യം",
|
||||||
|
"appURLList": "ആപ്പ് URL ലിസ്റ്റ്",
|
||||||
|
"line": "വരി",
|
||||||
|
"searchX": "{} തിരയുക",
|
||||||
|
"noResults": "ഫലങ്ങളൊന്നും കണ്ടെത്തിയില്ല",
|
||||||
|
"importX": "{} ഇംപോർട്ട് ചെയ്യുക",
|
||||||
|
"importedAppsIdDisclaimer": "ഇംപോർട്ട് ചെയ്ത ആപ്പുകൾ \"ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല\" എന്ന് തെറ്റായി കാണിച്ചേക്കാം.\nഇത് ശരിയാക്കാൻ, ഒബ്റ്റേനിയം വഴി അവ വീണ്ടും ഇൻസ്റ്റാൾ ചെയ്യുക.\nഇത് ആപ്പ് ഡാറ്റയെ ബാധിക്കില്ല.\n\nURL, മൂന്നാം കക്ഷി ഇംപോർട്ട് രീതികളെ മാത്രം ബാധിക്കുന്നു.",
|
||||||
|
"importErrors": "ഇംപോർട്ട് പിശകുകൾ",
|
||||||
|
"importedXOfYApps": "{} ആപ്പുകളിൽ {} എണ്ണം ഇംപോർട്ട് ചെയ്തു.",
|
||||||
|
"followingURLsHadErrors": "ഇനിപ്പറയുന്ന URL-കളിൽ പിശകുകൾ ഉണ്ടായിരുന്നു:",
|
||||||
|
"selectURL": "URL തിരഞ്ഞെടുക്കുക",
|
||||||
|
"selectURLs": "URL-കൾ തിരഞ്ഞെടുക്കുക",
|
||||||
|
"pick": "തിരഞ്ഞെടുക്കുക",
|
||||||
|
"theme": "തീം",
|
||||||
|
"dark": "ഇരുണ്ട",
|
||||||
|
"light": "ഇളം",
|
||||||
|
"followSystem": "സിസ്റ്റം പിന്തുടരുക",
|
||||||
|
"followSystemThemeExplanation": "സിസ്റ്റം തീം പിന്തുടരുന്നത് തേർഡ് പാർട്ടി ആപ്ലിക്കേഷനുകൾ ഉപയോഗിച്ച് മാത്രമേ സാധ്യമാകൂ",
|
||||||
|
"useBlackTheme": "തനി കറുപ്പ് തീം ഉപയോഗിക്കുക",
|
||||||
|
"appSortBy": "ആപ്പ് അടുക്കേണ്ട രീതി",
|
||||||
|
"authorName": "ഡെവലപ്പർ/പേര്",
|
||||||
|
"nameAuthor": "പേര്/ഡെവലപ്പർ",
|
||||||
|
"asAdded": "ചേർത്ത ക്രമത്തിൽ",
|
||||||
|
"appSortOrder": "ആപ്പ് അടുക്കേണ്ട ക്രമം",
|
||||||
|
"ascending": "ആരോഹണ ക്രമത്തിൽ",
|
||||||
|
"descending": "അവരോഹണ ക്രമത്തിൽ",
|
||||||
|
"bgUpdateCheckInterval": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റ് പരിശോധന ഇടവേള",
|
||||||
|
"neverManualOnly": "ഒരിക്കലുമില്ല - സ്വമേധയാ മാത്രം",
|
||||||
|
"appearance": "രൂപം",
|
||||||
|
"showWebInAppView": "ആപ്പ് കാഴ്ചയിൽ ഉറവിട വെബ്പേജ് കാണിക്കുക",
|
||||||
|
"pinUpdates": "അപ്ഡേറ്റുകൾ ആപ്പ് കാഴ്ചയുടെ മുകളിൽ പിൻ ചെയ്യുക",
|
||||||
|
"updates": "അപ്ഡേറ്റുകൾ",
|
||||||
|
"sourceSpecific": "ഉറവിടം അടിസ്ഥാനമാക്കി",
|
||||||
|
"appSource": "ആപ്പ് ഉറവിടം",
|
||||||
|
"noLogs": "ലോഗുകളൊന്നുമില്ല",
|
||||||
|
"appLogs": "ആപ്പ് ലോഗുകൾ",
|
||||||
|
"close": "അടയ്ക്കുക",
|
||||||
|
"share": "പങ്കിടുക",
|
||||||
|
"appNotFound": "ആപ്പ് കണ്ടെത്തിയില്ല",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "ഒബ്റ്റേനിയം-എക്സ്പോർട്ട്",
|
||||||
|
"pickAnAPK": "ഒരു APK തിരഞ്ഞെടുക്കുക",
|
||||||
|
"appHasMoreThanOnePackage": "{}-ന് ഒന്നിൽ കൂടുതൽ പാക്കേജുകൾ ഉണ്ട്:",
|
||||||
|
"deviceSupportsXArch": "നിങ്ങളുടെ ഉപകരണം {} CPU ആർക്കിടെക്ചറിനെ പിന്തുണയ്ക്കുന്നു.",
|
||||||
|
"deviceSupportsFollowingArchs": "നിങ്ങളുടെ ഉപകരണം ഇനിപ്പറയുന്ന CPU ആർക്കിടെക്ചറുകളെ പിന്തുണയ്ക്കുന്നു:",
|
||||||
|
"warning": "മുന്നറിയിപ്പ്",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "ആപ്പ് ഉറവിടം '{}' ആണ്, പക്ഷേ റിലീസ് പാക്കേജ് '{}'-ൽ നിന്നാണ് വരുന്നത്. തുടരണോ?",
|
||||||
|
"updatesAvailable": "അപ്ഡേറ്റുകൾ ലഭ്യമാണ്",
|
||||||
|
"updatesAvailableNotifDescription": "ഒബ്റ്റേനിയം ട്രാക്ക് ചെയ്യുന്ന ഒന്നോ അതിലധികമോ ആപ്പുകൾക്ക് അപ്ഡേറ്റുകൾ ലഭ്യമാണെന്ന് ഉപയോക്താവിനെ അറിയിക്കുന്നു",
|
||||||
|
"noNewUpdates": "പുതിയ അപ്ഡേറ്റുകളൊന്നുമില്ല.",
|
||||||
|
"xHasAnUpdate": "{}-ന് ഒരു അപ്ഡേറ്റ് ഉണ്ട്.",
|
||||||
|
"appsUpdated": "ആപ്പുകൾ അപ്ഡേറ്റ് ചെയ്തു",
|
||||||
|
"appsNotUpdated": "ആപ്ലിക്കേഷനുകൾ അപ്ഡേറ്റ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു",
|
||||||
|
"appsUpdatedNotifDescription": "ഒന്നോ അതിലധികമോ ആപ്പുകളിലേക്കുള്ള അപ്ഡേറ്റുകൾ ബാക്ക്ഗ്രൗണ്ടിൽ പ്രയോഗിച്ചു എന്ന് ഉപയോക്താവിനെ അറിയിക്കുന്നു",
|
||||||
|
"xWasUpdatedToY": "{} {} ലേക്ക് അപ്ഡേറ്റ് ചെയ്തു.",
|
||||||
|
"xWasNotUpdatedToY": "{} {} ലേക്ക് അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു.",
|
||||||
|
"errorCheckingUpdates": "അപ്ഡേറ്റുകൾ പരിശോധിക്കുന്നതിൽ പിശക്",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റ് പരിശോധന പരാജയപ്പെടുമ്പോൾ കാണിക്കുന്ന അറിയിപ്പ്",
|
||||||
|
"appsRemoved": "ആപ്പുകൾ നീക്കം ചെയ്തു",
|
||||||
|
"appsRemovedNotifDescription": "ഒന്നോ അതിലധികമോ ആപ്പുകൾ ലോഡ് ചെയ്യുന്നതിൽ പിശകുകൾ കാരണം നീക്കം ചെയ്തു എന്ന് ഉപയോക്താവിനെ അറിയിക്കുന്നു",
|
||||||
|
"xWasRemovedDueToErrorY": "{} ഈ പിശക് കാരണം നീക്കം ചെയ്തു: {}",
|
||||||
|
"completeAppInstallation": "ആപ്പ് ഇൻസ്റ്റാളേഷൻ പൂർത്തിയാക്കുക",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "ആപ്പുകൾ ഇൻസ്റ്റാൾ ചെയ്യാൻ ഒബ്റ്റേനിയം തുറന്നിരിക്കണം",
|
||||||
|
"completeAppInstallationNotifDescription": "ഒരു ആപ്പ് ഇൻസ്റ്റാൾ ചെയ്യുന്നത് പൂർത്തിയാക്കാൻ ഒബ്റ്റേനിയത്തിലേക്ക് മടങ്ങാൻ ഉപയോക്താവിനോട് ആവശ്യപ്പെടുന്നു",
|
||||||
|
"checkingForUpdates": "അപ്ഡേറ്റുകൾ പരിശോധിക്കുന്നു",
|
||||||
|
"checkingForUpdatesNotifDescription": "അപ്ഡേറ്റുകൾ പരിശോധിക്കുമ്പോൾ ദൃശ്യമാകുന്ന താൽക്കാലിക അറിയിപ്പ്",
|
||||||
|
"pleaseAllowInstallPerm": "ആപ്പുകൾ ഇൻസ്റ്റാൾ ചെയ്യാൻ ഒബ്റ്റേനിയത്തിന് അനുമതി നൽകുക",
|
||||||
|
"trackOnly": "ട്രാക്ക്-മാത്രം",
|
||||||
|
"errorWithHttpStatusCode": "പിശക് {}",
|
||||||
|
"versionCorrectionDisabled": "പതിപ്പ് തിരുത്തൽ പ്രവർത്തനരഹിതമാക്കി (പ്ലഗിൻ പ്രവർത്തിക്കുന്നതായി തോന്നുന്നില്ല)",
|
||||||
|
"unknown": "അജ്ഞാതം",
|
||||||
|
"none": "ഒന്നുമില്ല",
|
||||||
|
"never": "ഒരിക്കലുമില്ല",
|
||||||
|
"latestVersionX": "ഏറ്റവും പുതിയത്: {}",
|
||||||
|
"installedVersionX": "ഇൻസ്റ്റാൾ ചെയ്തത്: {}",
|
||||||
|
"lastUpdateCheckX": "അവസാന അപ്ഡേറ്റ് പരിശോധന: {}",
|
||||||
|
"remove": "നീക്കം ചെയ്യുക",
|
||||||
|
"yesMarkUpdated": "അതെ, അപ്ഡേറ്റ് ചെയ്തതായി അടയാളപ്പെടുത്തുക",
|
||||||
|
"fdroid": "എഫ്-ഡ്രോയിഡ് ഒഫീഷ്യൽ",
|
||||||
|
"appIdOrName": "ആപ്പ് ഐഡി അല്ലെങ്കിൽ പേര്",
|
||||||
|
"appId": "ആപ്പ് ഐഡി",
|
||||||
|
"appWithIdOrNameNotFound": "ആ ഐഡിയിലോ പേരിലോ ആപ്പുകളൊന്നും കണ്ടെത്തിയില്ല",
|
||||||
|
"reposHaveMultipleApps": "റെപ്പോകളിൽ ഒന്നിലധികം ആപ്പുകൾ അടങ്ങിയിരിക്കാം",
|
||||||
|
"fdroidThirdPartyRepo": "എഫ്-ഡ്രോയിഡ് തേർഡ് പാർട്ടി റെപ്പോ",
|
||||||
|
"install": "ഇൻസ്റ്റാൾ ചെയ്യുക",
|
||||||
|
"markInstalled": "ഇൻസ്റ്റാൾ ചെയ്തതായി അടയാളപ്പെടുത്തുക",
|
||||||
|
"update": "അപ്ഡേറ്റ് ചെയ്യുക",
|
||||||
|
"markUpdated": "അപ്ഡേറ്റ് ചെയ്തതായി അടയാളപ്പെടുത്തുക",
|
||||||
|
"additionalOptions": "കൂടുതൽ ഓപ്ഷനുകൾ",
|
||||||
|
"disableVersionDetection": "പതിപ്പ് കണ്ടെത്തൽ പ്രവർത്തനരഹിതമാക്കുക",
|
||||||
|
"noVersionDetectionExplanation": "പതിപ്പ് കണ്ടെത്തൽ ശരിയായി പ്രവർത്തിക്കാത്ത ആപ്പുകൾക്ക് മാത്രമേ ഈ ഓപ്ഷൻ ഉപയോഗിക്കാവൂ.",
|
||||||
|
"downloadingX": "{} ഡൗൺലോഡ് ചെയ്യുന്നു",
|
||||||
|
"downloadX": "{} ഡൗൺലോഡ് ചെയ്യുക",
|
||||||
|
"downloadedX": "{} ഡൗൺലോഡ് ചെയ്തു",
|
||||||
|
"releaseAsset": "റിലീസ് അസറ്റ്",
|
||||||
|
"downloadNotifDescription": "ഒരു ആപ്പ് ഡൗൺലോഡ് ചെയ്യുന്നതിലെ പുരോഗതി ഉപയോക്താവിനെ അറിയിക്കുന്നു",
|
||||||
|
"noAPKFound": "APK കണ്ടെത്തിയില്ല",
|
||||||
|
"noVersionDetection": "പതിപ്പ് കണ്ടെത്തൽ ഇല്ല",
|
||||||
|
"categorize": "വിഭാഗീകരിക്കുക",
|
||||||
|
"categories": "വിഭാഗങ്ങൾ",
|
||||||
|
"category": "വിഭാഗം",
|
||||||
|
"noCategory": "വിഭാഗമൊന്നുമില്ല",
|
||||||
|
"noCategories": "വിഭാഗങ്ങളൊന്നുമില്ല",
|
||||||
|
"deleteCategoriesQuestion": "വിഭാഗങ്ങൾ ഇല്ലാതാക്കണോ?",
|
||||||
|
"categoryDeleteWarning": "ഇല്ലാതാക്കിയ വിഭാഗങ്ങളിലെ എല്ലാ ആപ്പുകളും വിഭാഗരഹിതമായി ക്രമീകരിക്കും.",
|
||||||
|
"addCategory": "വിഭാഗം ചേർക്കുക",
|
||||||
|
"label": "ലേബൽ",
|
||||||
|
"language": "ഭാഷ",
|
||||||
|
"copiedToClipboard": "ക്ലിപ്ബോർഡിലേക്ക് പകർത്തി",
|
||||||
|
"storagePermissionDenied": "സ്റ്റോറേജ് അനുമതി നിഷേധിച്ചു",
|
||||||
|
"selectedCategorizeWarning": "ഇത് തിരഞ്ഞെടുത്ത ആപ്പുകൾക്കായി നിലവിലുള്ള ഏതൊരു വിഭാഗ ക്രമീകരണങ്ങളെയും മാറ്റിസ്ഥാപിക്കും.",
|
||||||
|
"filterAPKsByRegEx": "റെഗുലർ എക്സ്പ്രഷൻ ഉപയോഗിച്ച് APK-കൾ ഫിൽട്ടർ ചെയ്യുക",
|
||||||
|
"removeFromObtainium": "ഒബ്റ്റേനിയത്തിൽ നിന്ന് നീക്കം ചെയ്യുക",
|
||||||
|
"uninstallFromDevice": "ഉപകരണത്തിൽ നിന്ന് അൺഇൻസ്റ്റാൾ ചെയ്യുക",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "പതിപ്പ് കണ്ടെത്തൽ പ്രവർത്തനരഹിതമാക്കിയ ആപ്പുകൾക്ക് മാത്രമേ ഇത് പ്രവർത്തിക്കൂ.",
|
||||||
|
"releaseDateAsVersion": "റിലീസ് തീയതി പതിപ്പ് സ്ട്രിംഗായി ഉപയോഗിക്കുക",
|
||||||
|
"releaseTitleAsVersion": "റിലീസ് ശീർഷകം പതിപ്പ് സ്ട്രിംഗായി ഉപയോഗിക്കുക",
|
||||||
|
"releaseDateAsVersionExplanation": "പതിപ്പ് കണ്ടെത്തൽ ശരിയായി പ്രവർത്തിക്കാത്തതും എന്നാൽ റിലീസ് തീയതി ലഭ്യമായതുമായ ആപ്പുകൾക്ക് മാത്രമേ ഈ ഓപ്ഷൻ ഉപയോഗിക്കാവൂ.",
|
||||||
|
"changes": "മാറ്റങ്ങൾ",
|
||||||
|
"releaseDate": "റിലീസ് തീയതി",
|
||||||
|
"importFromURLsInFile": "ഫയലിലെ URL-കളിൽ നിന്ന് ഇംപോർട്ട് ചെയ്യുക (OPML പോലെ)",
|
||||||
|
"versionDetectionExplanation": "കണ്ടെത്തിയ പതിപ്പ് സ്ട്രിംഗ് OS-ൽ നിന്ന് കണ്ടെത്തിയ പതിപ്പുമായി പൊരുത്തപ്പെടുത്തുക",
|
||||||
|
"versionDetection": "പതിപ്പ് കണ്ടെത്തൽ",
|
||||||
|
"standardVersionDetection": "സാധാരണ പതിപ്പ് കണ്ടെത്തൽ",
|
||||||
|
"groupByCategory": "വിഭാഗം തിരിച്ച് ഗ്രൂപ്പ് ചെയ്യുക",
|
||||||
|
"autoApkFilterByArch": "സാധ്യമെങ്കിൽ CPU ആർക്കിടെക്ചർ അനുസരിച്ച് APK-കൾ ഫിൽട്ടർ ചെയ്യാൻ ശ്രമിക്കുക",
|
||||||
|
"autoLinkFilterByArch": "സാധ്യമെങ്കിൽ CPU ആർക്കിടെക്ചർ അനുസരിച്ച് ലിങ്കുകൾ ഫിൽട്ടർ ചെയ്യാൻ ശ്രമിക്കുക",
|
||||||
|
"overrideSource": "ഉറവിടം അസാധുവാക്കുക",
|
||||||
|
"dontShowAgain": "ഇത് വീണ്ടും കാണിക്കരുത്",
|
||||||
|
"dontShowTrackOnlyWarnings": "'ട്രാക്ക്-മാത്രം' മുന്നറിയിപ്പുകൾ കാണിക്കരുത്",
|
||||||
|
"dontShowAPKOriginWarnings": "APK ഉറവിട മുന്നറിയിപ്പുകൾ കാണിക്കരുത്",
|
||||||
|
"moveNonInstalledAppsToBottom": "ഇൻസ്റ്റാൾ ചെയ്യാത്ത ആപ്പുകൾ ആപ്പ് കാഴ്ചയുടെ താഴേക്ക് മാറ്റുക",
|
||||||
|
"gitlabPATLabel": "GitLab പേഴ്സണൽ ആക്സസ് ടോക്കൺ",
|
||||||
|
"about": "വിവരങ്ങൾ",
|
||||||
|
"requiresCredentialsInSettings": "{}-ന് അധിക ക്രെഡൻഷ്യലുകൾ ആവശ്യമാണ് (ക്രമീകരണങ്ങളിൽ)",
|
||||||
|
"checkOnStart": "തുടങ്ങുമ്പോൾ അപ്ഡേറ്റുകൾക്കായി പരിശോധിക്കുക",
|
||||||
|
"tryInferAppIdFromCode": "സോഴ്സ് കോഡിൽ നിന്ന് ആപ്പ് ഐഡി അനുമാനിക്കാൻ ശ്രമിക്കുക",
|
||||||
|
"removeOnExternalUninstall": "പുറത്ത് നിന്ന് അൺഇൻസ്റ്റാൾ ചെയ്ത ആപ്പുകൾ സ്വയമേവ നീക്കം ചെയ്യുക",
|
||||||
|
"pickHighestVersionCode": "ഏറ്റവും ഉയർന്ന പതിപ്പ് കോഡ് APK സ്വയമേവ തിരഞ്ഞെടുക്കുക",
|
||||||
|
"checkUpdateOnDetailPage": "ഒരു ആപ്പ് വിശദാംശ പേജ് തുറക്കുമ്പോൾ അപ്ഡേറ്റുകൾക്കായി പരിശോധിക്കുക",
|
||||||
|
"disablePageTransitions": "പേജ് ട്രാൻസിഷൻ ആനിമേഷനുകൾ പ്രവർത്തനരഹിതമാക്കുക",
|
||||||
|
"reversePageTransitions": "പേജ് ട്രാൻസിഷൻ ആനിമേഷനുകൾ വിപരീതമാക്കുക",
|
||||||
|
"minStarCount": "കുറഞ്ഞ നക്ഷത്രങ്ങളുടെ എണ്ണം",
|
||||||
|
"addInfoBelow": "ഈ വിവരങ്ങൾ താഴെ ചേർക്കുക.",
|
||||||
|
"addInfoInSettings": "ഈ വിവരങ്ങൾ ക്രമീകരണങ്ങളിൽ ചേർക്കുക.",
|
||||||
|
"githubSourceNote": "API കീ ഉപയോഗിച്ച് GitHub നിരക്ക് പരിധി ഒഴിവാക്കാം.",
|
||||||
|
"sortByLastLinkSegment": "ലിങ്കിന്റെ അവസാന ഭാഗം മാത്രം ഉപയോഗിച്ച് അടുക്കുക",
|
||||||
|
"filterReleaseNotesByRegEx": "റെഗുലർ എക്സ്പ്രഷൻ ഉപയോഗിച്ച് റിലീസ് നോട്ടുകൾ ഫിൽട്ടർ ചെയ്യുക",
|
||||||
|
"customLinkFilterRegex": "റെഗുലർ എക്സ്പ്രഷൻ ഉപയോഗിച്ചുള്ള കസ്റ്റം APK ലിങ്ക് ഫിൽട്ടർ (സ്ഥിരസ്ഥിതി '.apk$')",
|
||||||
|
"appsPossiblyUpdated": "ആപ്പ് അപ്ഡേറ്റുകൾ ശ്രമിച്ചു",
|
||||||
|
"appsPossiblyUpdatedNotifDescription": "ഒന്നോ അതിലധികമോ ആപ്പുകളിലേക്കുള്ള അപ്ഡേറ്റുകൾ ബാക്ക്ഗ്രൗണ്ടിൽ പ്രയോഗിക്കാൻ സാധ്യതയുണ്ടെന്ന് ഉപയോക്താവിനെ അറിയിക്കുന്നു",
|
||||||
|
"xWasPossiblyUpdatedToY": "{} {} ലേക്ക് അപ്ഡേറ്റ് ചെയ്തിരിക്കാം.",
|
||||||
|
"enableBackgroundUpdates": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റുകൾ പ്രവർത്തനക്ഷമമാക്കുക",
|
||||||
|
"backgroundUpdateReqsExplanation": "എല്ലാ ആപ്പുകൾക്കും ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റുകൾ സാധ്യമായെന്ന് വരില്ല.",
|
||||||
|
"backgroundUpdateLimitsExplanation": "ബാക്ക്ഗ്രൗണ്ട് ഇൻസ്റ്റാളിന്റെ വിജയം ഒബ്റ്റേനിയം തുറക്കുമ്പോൾ മാത്രമേ നിർണ്ണയിക്കാൻ കഴിയൂ.",
|
||||||
|
"verifyLatestTag": "'ഏറ്റവും പുതിയ' ടാഗ് പരിശോധിക്കുക",
|
||||||
|
"intermediateLinkRegex": "സന്ദർശിക്കാൻ ഒരു 'ഇന്റർമീഡിയറ്റ്' ലിങ്കിനായി ഫിൽട്ടർ ചെയ്യുക",
|
||||||
|
"filterByLinkText": "ലിങ്ക് ടെക്സ്റ്റ് ഉപയോഗിച്ച് ലിങ്കുകൾ ഫിൽട്ടർ ചെയ്യുക",
|
||||||
|
"intermediateLinkNotFound": "ഇന്റർമീഡിയറ്റ് ലിങ്ക് കണ്ടെത്തിയില്ല",
|
||||||
|
"intermediateLink": "ഇന്റർമീഡിയറ്റ് ലിങ്ക്",
|
||||||
|
"exemptFromBackgroundUpdates": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റുകളിൽ നിന്ന് ഒഴിവാക്കുക (പ്രവർത്തനക്ഷമമാക്കിയിട്ടുണ്ടെങ്കിൽ)",
|
||||||
|
"bgUpdatesOnWiFiOnly": "വൈഫൈയിൽ അല്ലാത്തപ്പോൾ ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റുകൾ പ്രവർത്തനരഹിതമാക്കുക",
|
||||||
|
"bgUpdatesWhileChargingOnly": "ചാർജ് ചെയ്യാത്തപ്പോൾ ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റുകൾ പ്രവർത്തനരഹിതമാക്കുക",
|
||||||
|
"autoSelectHighestVersionCode": "ഏറ്റവും ഉയർന്ന versionCode APK സ്വയമേവ തിരഞ്ഞെടുക്കുക",
|
||||||
|
"versionExtractionRegEx": "പതിപ്പ് സ്ട്രിംഗ് വേർതിരിച്ചെടുക്കുന്നതിനുള്ള റെഗുലർ എക്സ്പ്രഷൻ",
|
||||||
|
"trimVersionString": "റെഗുലർ എക്സ്പ്രഷൻ ഉപയോഗിച്ച് പതിപ്പ് സ്ട്രിംഗ് ട്രിം ചെയ്യുക",
|
||||||
|
"matchGroupToUseForX": "\"{}\" നായി ഉപയോഗിക്കേണ്ട ഗ്രൂപ്പ് മാച്ച് ചെയ്യുക",
|
||||||
|
"matchGroupToUse": "പതിപ്പ് സ്ട്രിംഗ് വേർതിരിച്ചെടുക്കുന്നതിനുള്ള റെഗുലർ എക്സ്പ്രഷനായി ഉപയോഗിക്കേണ്ട ഗ്രൂപ്പ് മാച്ച് ചെയ്യുക",
|
||||||
|
"highlightTouchTargets": "കുറഞ്ഞ വ്യക്തതയുള്ള ടച്ച് ടാർഗെറ്റുകൾ ഹൈലൈറ്റ് ചെയ്യുക",
|
||||||
|
"pickExportDir": "എക്സ്പോർട്ട് ഡയറക്ടറി തിരഞ്ഞെടുക്കുക",
|
||||||
|
"autoExportOnChanges": "മാറ്റങ്ങൾ വരുമ്പോൾ സ്വയമേവ എക്സ്പോർട്ട് ചെയ്യുക",
|
||||||
|
"includeSettings": "ക്രമീകരണങ്ങൾ ഉൾപ്പെടുത്തുക",
|
||||||
|
"filterVersionsByRegEx": "റെഗുലർ എക്സ്പ്രഷൻ ഉപയോഗിച്ച് പതിപ്പുകൾ ഫിൽട്ടർ ചെയ്യുക",
|
||||||
|
"trySelectingSuggestedVersionCode": "നിർദ്ദേശിച്ച versionCode APK തിരഞ്ഞെടുക്കാൻ ശ്രമിക്കുക",
|
||||||
|
"dontSortReleasesList": "API-യിൽ നിന്നുള്ള റിലീസ് ഓർഡർ നിലനിർത്തുക",
|
||||||
|
"reverseSort": "റിവേഴ്സ് സോർട്ടിംഗ്",
|
||||||
|
"takeFirstLink": "ആദ്യ ലിങ്ക് എടുക്കുക",
|
||||||
|
"skipSort": "സോർട്ടിംഗ് ഒഴിവാക്കുക",
|
||||||
|
"debugMenu": "ഡീബഗ് മെനു",
|
||||||
|
"bgTaskStarted": "ബാക്ക്ഗ്രൗണ്ട് ടാസ്ക് ആരംഭിച്ചു - ലോഗുകൾ പരിശോധിക്കുക.",
|
||||||
|
"runBgCheckNow": "ഇപ്പോൾ ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റ് പരിശോധന പ്രവർത്തിപ്പിക്കുക",
|
||||||
|
"versionExtractWholePage": "മുഴുവൻ പേജിലും പതിപ്പ് സ്ട്രിംഗ് വേർതിരിച്ചെടുക്കുന്ന റെഗുലർ എക്സ്പ്രഷൻ പ്രയോഗിക്കുക",
|
||||||
|
"installing": "ഇൻസ്റ്റാൾ ചെയ്യുന്നു",
|
||||||
|
"skipUpdateNotifications": "അപ്ഡേറ്റ് അറിയിപ്പുകൾ ഒഴിവാക്കുക",
|
||||||
|
"updatesAvailableNotifChannel": "അപ്ഡേറ്റുകൾ ലഭ്യമാണ്",
|
||||||
|
"appsUpdatedNotifChannel": "ആപ്പുകൾ അപ്ഡേറ്റ് ചെയ്തു",
|
||||||
|
"appsPossiblyUpdatedNotifChannel": "ആപ്പ് അപ്ഡേറ്റുകൾ ശ്രമിച്ചു",
|
||||||
|
"errorCheckingUpdatesNotifChannel": "അപ്ഡേറ്റുകൾ പരിശോധിക്കുന്നതിൽ പിശക്",
|
||||||
|
"appsRemovedNotifChannel": "ആപ്പുകൾ നീക്കം ചെയ്തു",
|
||||||
|
"downloadingXNotifChannel": "{} ഡൗൺലോഡ് ചെയ്യുന്നു",
|
||||||
|
"completeAppInstallationNotifChannel": "ആപ്പ് ഇൻസ്റ്റാളേഷൻ പൂർത്തിയാക്കുക",
|
||||||
|
"checkingForUpdatesNotifChannel": "അപ്ഡേറ്റുകൾ പരിശോധിക്കുന്നു",
|
||||||
|
"onlyCheckInstalledOrTrackOnlyApps": "ഇൻസ്റ്റാൾ ചെയ്തതും ട്രാക്ക്-മാത്രം ആപ്പുകളും മാത്രം അപ്ഡേറ്റുകൾക്കായി പരിശോധിക്കുക",
|
||||||
|
"supportFixedAPKURL": "സ്ഥിരമായ APK URL-കളെ പിന്തുണയ്ക്കുക",
|
||||||
|
"selectX": "{} തിരഞ്ഞെടുക്കുക",
|
||||||
|
"parallelDownloads": "പാരലൽ ഡൗൺലോഡുകൾ അനുവദിക്കുക",
|
||||||
|
"useShizuku": "ഇൻസ്റ്റാൾ ചെയ്യാൻ ഷിസുകു അല്ലെങ്കിൽ സ്യൂ ഉപയോഗിക്കുക",
|
||||||
|
"shizukuBinderNotFound": "ഷിസുകു സേവനം പ്രവർത്തിക്കുന്നില്ല",
|
||||||
|
"shizukuOld": "പഴയ ഷിസുകു പതിപ്പ് (<11) - അത് അപ്ഡേറ്റ് ചെയ്യുക",
|
||||||
|
"shizukuOldAndroidWithADB": "ആൻഡ്രോയിഡ് < 8.1 ഉള്ള ഷിസുകു ADB ഉപയോഗിച്ച് പ്രവർത്തിക്കുന്നു - ആൻഡ്രോയിഡ് അപ്ഡേറ്റ് ചെയ്യുക അല്ലെങ്കിൽ പകരം സ്യൂ ഉപയോഗിക്കുക",
|
||||||
|
"shizukuPretendToBeGooglePlay": "Google Play ഇൻസ്റ്റാളേഷൻ ഉറവിടമായി സജ്ജമാക്കുക (ഷിസുകു ഉപയോഗിക്കുകയാണെങ്കിൽ)",
|
||||||
|
"useSystemFont": "സിസ്റ്റം ഫോണ്ട് ഉപയോഗിക്കുക",
|
||||||
|
"useVersionCodeAsOSVersion": "ആപ്പ് versionCode OS-ൽ കണ്ടെത്തിയ പതിപ്പായി ഉപയോഗിക്കുക",
|
||||||
|
"requestHeader": "അഭ്യർത്ഥന തലക്കെട്ട്",
|
||||||
|
"useLatestAssetDateAsReleaseDate": "ഏറ്റവും പുതിയ അസറ്റ് അപ്ലോഡ് റിലീസ് തീയതിയായി ഉപയോഗിക്കുക",
|
||||||
|
"defaultPseudoVersioningMethod": "സ്ഥിരസ്ഥിതി സ്യൂഡോ-പതിപ്പ് രീതി",
|
||||||
|
"partialAPKHash": "ഭാഗിക APK ഹാഷ്",
|
||||||
|
"APKLinkHash": "APK ലിങ്ക് ഹാഷ്",
|
||||||
|
"directAPKLink": "നേരിട്ടുള്ള APK ലിങ്ക്",
|
||||||
|
"pseudoVersionInUse": "ഒരു സ്യൂഡോ-പതിപ്പ് ഉപയോഗത്തിലുണ്ട്",
|
||||||
|
"installed": "ഇൻസ്റ്റാൾ ചെയ്തത്",
|
||||||
|
"latest": "ഏറ്റവും പുതിയത്",
|
||||||
|
"invertRegEx": "റെഗുലർ എക്സ്പ്രഷൻ വിപരീതമാക്കുക",
|
||||||
|
"note": "ശ്രദ്ധിക്കുക",
|
||||||
|
"selfHostedNote": "ഏത് ഉറവിടത്തിന്റെയും സെൽഫ്-ഹോസ്റ്റ് ചെയ്ത/കസ്റ്റം ഇൻസ്റ്റൻസുകളിലേക്ക് എത്താൻ \"{}\" ഡ്രോപ്പ്ഡൗൺ ഉപയോഗിക്കാം.",
|
||||||
|
"badDownload": "APK പാഴ്സ് ചെയ്യാൻ കഴിഞ്ഞില്ല (അനുയോജ്യമല്ലാത്തതോ ഭാഗികമായ ഡൗൺലോഡോ)",
|
||||||
|
"beforeNewInstallsShareToAppVerifier": "പുതിയ ആപ്പുകൾ AppVerifier-ലേക്ക് പങ്കിടുക (ലഭ്യമെങ്കിൽ)",
|
||||||
|
"appVerifierInstructionToast": "AppVerifier-ലേക്ക് പങ്കിടുക, തുടർന്ന് തയ്യാറാകുമ്പോൾ ഇവിടെ തിരിച്ചെത്തുക.",
|
||||||
|
"wiki": "സഹായം/വിക്കി",
|
||||||
|
"crowdsourcedConfigsLabel": "ക്രൗഡ്സോഴ്സ് ചെയ്ത ആപ്പ് കോൺഫിഗറേഷനുകൾ (സ്വന്തം ഉത്തരവാദിത്തത്തിൽ ഉപയോഗിക്കുക)",
|
||||||
|
"crowdsourcedConfigsShort": "ക്രൗഡ്സോഴ്സ് ചെയ്ത ആപ്പ് കോൺഫിഗറേഷനുകൾ",
|
||||||
|
"allowInsecure": "സുരക്ഷിതമല്ലാത്ത HTTP അഭ്യർത്ഥനകൾ അനുവദിക്കുക",
|
||||||
|
"stayOneVersionBehind": "ഏറ്റവും പുതിയ പതിപ്പിന് ഒരു പതിപ്പ് പിന്നിൽ തുടരുക",
|
||||||
|
"useFirstApkOfVersion": "ഒന്നിലധികം APK-കളിൽ നിന്ന് ആദ്യത്തേത് സ്വയമേവ തിരഞ്ഞെടുക്കുക",
|
||||||
|
"refreshBeforeDownload": "ഡൗൺലോഡ് ചെയ്യുന്നതിന് മുമ്പ് ആപ്പ് വിവരങ്ങൾ പുതുക്കുക",
|
||||||
|
"tencentAppStore": "ടെൻസെന്റ് ആപ്പ് സ്റ്റോർ",
|
||||||
|
"coolApk": "കൂൾ APK",
|
||||||
|
"vivoAppStore": "വിവോ ആപ്പ് സ്റ്റോർ (CN)",
|
||||||
|
"name": "പേര്",
|
||||||
|
"smartname": "പേര് (സ്മാർട്ട്)",
|
||||||
|
"sortMethod": "അടുക്കുന്ന രീതി",
|
||||||
|
"welcome": "സ്വാഗതം",
|
||||||
|
"documentationLinksNote": "താഴെ ലിങ്ക് ചെയ്തിട്ടുള്ള ഒബ്റ്റേനിയം GitHub പേജിൽ ആപ്പ് എങ്ങനെ ഉപയോഗിക്കാമെന്ന് മനസ്സിലാക്കാൻ സഹായിക്കുന്ന വീഡിയോകൾ, ലേഖനങ്ങൾ, ചർച്ചകൾ, മറ്റ് വിഭവങ്ങൾ എന്നിവയുടെ ലിങ്കുകൾ അടങ്ങിയിരിക്കുന്നു.",
|
||||||
|
"batteryOptimizationNote": "ഒബ്റ്റേനിയത്തിനായുള്ള OS ബാറ്ററി ഒപ്റ്റിമൈസേഷനുകൾ പ്രവർത്തനരഹിതമാക്കുകയാണെങ്കിൽ ബാക്ക്ഗ്രൗണ്ട് ഡൗൺലോഡുകൾ കൂടുതൽ വിശ്വസനീയമായി പ്രവർത്തിച്ചേക്കാം എന്നത് ശ്രദ്ധിക്കുക.",
|
||||||
|
"fileDeletionError": "ഫയൽ ഇല്ലാതാക്കാൻ പരാജയപ്പെട്ടു (സ്വമേധയാ ഇല്ലാതാക്കാൻ ശ്രമിക്കുക, എന്നിട്ട് വീണ്ടും ശ്രമിക്കുക): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Use a foreground service for update checking (more reliable, consumes more power)",
|
||||||
|
"fgServiceNotice": "This notification is required for background update checking (it can be hidden in the OS settings)",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "ആപ്പ് നീക്കം ചെയ്യണോ?",
|
||||||
|
"other": "ആപ്പുകൾ നീക്കം ചെയ്യണോ?"
|
||||||
|
},
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "നിരവധി അഭ്യർത്ഥനകൾ (നിരക്ക് പരിമിതപ്പെടുത്തി) - {} മിനിറ്റിനുള്ളിൽ വീണ്ടും ശ്രമിക്കുക",
|
||||||
|
"other": "നിരവധി അഭ്യർത്ഥനകൾ (നിരക്ക് പരിമിതപ്പെടുത്തി) - {} മിനിറ്റുകൾക്കുള്ളിൽ വീണ്ടും ശ്രമിക്കുക"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റ് പരിശോധനയിൽ ഒരു {} നേരിട്ടു, {} മിനിറ്റിനുള്ളിൽ വീണ്ടും ശ്രമിക്കാൻ ക്രമീകരിക്കും",
|
||||||
|
"other": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റ് പരിശോധനയിൽ ഒരു {} നേരിട്ടു, {} മിനിറ്റുകൾക്കുള്ളിൽ വീണ്ടും ശ്രമിക്കാൻ ക്രമീകരിക്കും"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റ് പരിശോധനയിൽ {} അപ്ഡേറ്റ് കണ്ടെത്തി - ആവശ്യമെങ്കിൽ ഉപയോക്താവിനെ അറിയിക്കും",
|
||||||
|
"other": "ബാക്ക്ഗ്രൗണ്ട് അപ്ഡേറ്റ് പരിശോധനയിൽ {} അപ്ഡേറ്റുകൾ കണ്ടെത്തി - ആവശ്യമെങ്കിൽ ഉപയോക്താവിനെ അറിയിക്കും"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{} ആപ്പ്",
|
||||||
|
"other": "{} ആപ്പുകൾ"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} URL",
|
||||||
|
"other": "{} URL-കൾ"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} മിനിറ്റ്",
|
||||||
|
"other": "{} മിനിറ്റുകൾ"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} മണിക്കൂർ",
|
||||||
|
"other": "{} മണിക്കൂർ"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} ദിവസം",
|
||||||
|
"other": "{} ദിവസങ്ങൾ"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "{n} ലോഗ് മായ്ച്ചു (മുമ്പ് = {before}, ശേഷം = {after})",
|
||||||
|
"other": "{n} ലോഗുകൾ മായ്ച്ചു (മുമ്പ് = {before}, ശേഷം = {after})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "{} കൂടാതെ 1 ആപ്പിന് കൂടി അപ്ഡേറ്റുകൾ ലഭ്യമാണ്.",
|
||||||
|
"other": "{} കൂടാതെ {} ആപ്പുകൾക്ക് കൂടി അപ്ഡേറ്റുകൾ ലഭ്യമാണ്."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "{} കൂടാതെ 1 ആപ്പ് കൂടി അപ്ഡേറ്റ് ചെയ്തു.",
|
||||||
|
"other": "{} കൂടാതെ {} ആപ്പുകൾ കൂടി അപ്ഡേറ്റ് ചെയ്തു."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesFailed": {
|
||||||
|
"one": "{} കൂടാതെ 1 ആപ്പ് കൂടി അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു.",
|
||||||
|
"other": "{} കൂടാതെ {} ആപ്പുകൾ കൂടി അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||||
|
"one": "{} കൂടാതെ 1 ആപ്പ് കൂടി അപ്ഡേറ്റ് ചെയ്തിരിക്കാം.",
|
||||||
|
"other": "{} കൂടാതെ {} ആപ്പുകൾ കൂടി അപ്ഡേറ്റ് ചെയ്തിരിക്കാം."
|
||||||
|
},
|
||||||
|
"apk": {
|
||||||
|
"one": "{} APK",
|
||||||
|
"other": "{} APK-കൾ"
|
||||||
|
}
|
||||||
|
}
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "De GitHub pagina van Obtainium waarnaar hieronder wordt gelinkt bevat links naar video's, artikelen, discussies en andere bronnen die je zullen helpen begrijpen hoe je de app kunt gebruiken.",
|
"documentationLinksNote": "De GitHub pagina van Obtainium waarnaar hieronder wordt gelinkt bevat links naar video's, artikelen, discussies en andere bronnen die je zullen helpen begrijpen hoe je de app kunt gebruiken.",
|
||||||
"batteryOptimizationNote": "Merk op dat downloads op de achtergrond mogelijk betrouwbaarder werken als je de batterijoptimalisatie van het besturingssysteem voor Obtainium uitschakelt.",
|
"batteryOptimizationNote": "Merk op dat downloads op de achtergrond mogelijk betrouwbaarder werken als je de batterijoptimalisatie van het besturingssysteem voor Obtainium uitschakelt.",
|
||||||
"fileDeletionError": "Bestand is niet verwijderd (probeer het handmatig te verwijderen en probeer het opnieuw): \"{}\"",
|
"fileDeletionError": "Bestand is niet verwijderd (probeer het handmatig te verwijderen en probeer het opnieuw): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Gebruik een voorgronddienst voor het controleren van updates (betrouwbaarder, verbruikt meer stroom)",
|
||||||
|
"fgServiceNotice": "Deze melding is nodig voor het controleren van updates op de achtergrond (kan worden verborgen in de OS-instellingen)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App verwijderen?",
|
"one": "App verwijderen?",
|
||||||
"other": "Apps verwijderen?"
|
"other": "Apps verwijderen?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Strona Obtainium GitHub, do której link znajduje się poniżej, zawiera linki do filmów, artykułów, dyskusji i innych zasobów, które pomogą ci zrozumieć, jak korzystać z aplikacji.",
|
"documentationLinksNote": "Strona Obtainium GitHub, do której link znajduje się poniżej, zawiera linki do filmów, artykułów, dyskusji i innych zasobów, które pomogą ci zrozumieć, jak korzystać z aplikacji.",
|
||||||
"batteryOptimizationNote": "Należy pamiętać, że pobieranie w tle może działać bardziej niezawodnie po wyłączeniu optymalizacji baterii systemu operacyjnego dla Obtainium.",
|
"batteryOptimizationNote": "Należy pamiętać, że pobieranie w tle może działać bardziej niezawodnie po wyłączeniu optymalizacji baterii systemu operacyjnego dla Obtainium.",
|
||||||
"fileDeletionError": "Nie udało się usunąć pliku (spróbuj usunąć go ręcznie, a następnie spróbuj ponownie): \"{}\"",
|
"fileDeletionError": "Nie udało się usunąć pliku (spróbuj usunąć go ręcznie, a następnie spróbuj ponownie): \"{}\"",
|
||||||
|
"foregroundService": "Usługa nowej generacji Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Używanie usługi pierwszoplanowej do sprawdzania aktualizacji (bardziej niezawodne, zużywa więcej energii)",
|
||||||
|
"fgServiceNotice": "To powiadomienie jest wymagane do sprawdzania aktualizacji w tle (można je ukryć w ustawieniach systemu operacyjnego).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Usunąć aplikację?",
|
"one": "Usunąć aplikację?",
|
||||||
"few": "Usunąć aplikacje?",
|
"few": "Usunąć aplikacje?",
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "A página do Obtainium no GitHub visível abaixo contém links de vídeos, artigos, discussões, e outros recursos que podem te ajudar ao usar o app.",
|
"documentationLinksNote": "A página do Obtainium no GitHub visível abaixo contém links de vídeos, artigos, discussões, e outros recursos que podem te ajudar ao usar o app.",
|
||||||
"batteryOptimizationNote": "Observe que os downloads em segundo plano podem funcionar de forma mais confiável se você desativar as otimizações de bateria do sistema operacional para o Obtainium.",
|
"batteryOptimizationNote": "Observe que os downloads em segundo plano podem funcionar de forma mais confiável se você desativar as otimizações de bateria do sistema operacional para o Obtainium.",
|
||||||
"fileDeletionError": "Falha ao excluir o arquivo (tente excluí-lo manualmente e tente novamente): \"{}\"",
|
"fileDeletionError": "Falha ao excluir o arquivo (tente excluí-lo manualmente e tente novamente): \"{}\"",
|
||||||
|
"foregroundService": "Serviço de primeiro plano do Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Usar um serviço em primeiro plano para verificação de atualizações (mais confiável, consome mais energia)",
|
||||||
|
"fgServiceNotice": "Essa notificação é necessária para a verificação de atualizações em segundo plano (ela pode ser ocultada nas configurações do sistema operacional)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remover app?",
|
"one": "Remover app?",
|
||||||
"other": "Remover apps?"
|
"other": "Remover apps?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "A página do Obtainium no GitHub com a ligação abaixo contém ligações para vídeos, artigos, discussões e outros recursos que o ajudarão a compreender como utilizar a aplicação.",
|
"documentationLinksNote": "A página do Obtainium no GitHub com a ligação abaixo contém ligações para vídeos, artigos, discussões e outros recursos que o ajudarão a compreender como utilizar a aplicação.",
|
||||||
"batteryOptimizationNote": "Note que os downloads em segundo plano podem funcionar de forma mais fiável se desativar as optimizações da bateria do SO para o Obtainium.",
|
"batteryOptimizationNote": "Note que os downloads em segundo plano podem funcionar de forma mais fiável se desativar as optimizações da bateria do SO para o Obtainium.",
|
||||||
"fileDeletionError": "Falha ao eliminar o ficheiro (tente eliminá-lo manualmente e depois tente novamente): \"{}\"",
|
"fileDeletionError": "Falha ao eliminar o ficheiro (tente eliminá-lo manualmente e depois tente novamente): \"{}\"",
|
||||||
|
"foregroundService": "Serviço de primeiro plano Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Utilizar um serviço em primeiro plano para verificação de actualizações (mais fiável, consome mais energia)",
|
||||||
|
"fgServiceNotice": "Esta notificação é necessária para a verificação de actualizações em segundo plano (pode ser ocultada nas definições do SO)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remover aplicativo?",
|
"one": "Remover aplicativo?",
|
||||||
"other": "Remover aplicativos?"
|
"other": "Remover aplicativos?"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"invalidURLForSource": "Неверный URL-адрес приложения: {}",
|
"invalidURLForSource": "Неверный URL-адрес приложения: {}",
|
||||||
"noReleaseFound": "Не удалось найти подходящий релиз",
|
"noReleaseFound": "Не удалось найти подходящий выпуск",
|
||||||
"noVersionFound": "Не удалось определить версию релиза",
|
"noVersionFound": "Не удалось определить версию выпуска",
|
||||||
"urlMatchesNoSource": "URL-адрес не соответствует известному источнику",
|
"urlMatchesNoSource": "URL-адрес не соответствует известному источнику",
|
||||||
"cantInstallOlderVersion": "Невозможно установить более старую версию приложения",
|
"cantInstallOlderVersion": "Невозможно установить более старую версию приложения",
|
||||||
"appIdMismatch": "ID загруженного пакета не совпадает с существующим ID приложения",
|
"appIdMismatch": "ID загруженного пакета не совпадает с существующим ID приложения",
|
||||||
@ -9,12 +9,12 @@
|
|||||||
"placeholder": "Заполнитель",
|
"placeholder": "Заполнитель",
|
||||||
"someErrors": "Возникли некоторые ошибки",
|
"someErrors": "Возникли некоторые ошибки",
|
||||||
"unexpectedError": "Неожиданная ошибка",
|
"unexpectedError": "Неожиданная ошибка",
|
||||||
"ok": "Ок",
|
"ok": "OK",
|
||||||
"and": "и",
|
"and": "и",
|
||||||
"githubPATLabel": "Персональный токен доступа GitHub\n(увеличивает лимит запросов)",
|
"githubPATLabel": "Персональный токен доступа GitHub\n(увеличивает лимит запросов)",
|
||||||
"includePrereleases": "Включить предварительные релизы",
|
"includePrereleases": "Включать предварительные выпуски",
|
||||||
"fallbackToOlderReleases": "Откатываться к предыдущей версии",
|
"fallbackToOlderReleases": "Откатываться к предыдущей версии",
|
||||||
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\n(регулярное выражение)",
|
"filterReleaseTitlesByRegEx": "Фильтровать заголовки выпусков\n(регулярное выражение)",
|
||||||
"invalidRegEx": "Неверное регулярное выражение",
|
"invalidRegEx": "Неверное регулярное выражение",
|
||||||
"noDescription": "Нет описания",
|
"noDescription": "Нет описания",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
@ -32,13 +32,13 @@
|
|||||||
"source": "Источник",
|
"source": "Источник",
|
||||||
"app": "Приложение",
|
"app": "Приложение",
|
||||||
"appsFromSourceAreTrackOnly": "Приложения из этого источника настроены только для отслеживания",
|
"appsFromSourceAreTrackOnly": "Приложения из этого источника настроены только для отслеживания",
|
||||||
"youPickedTrackOnly": "Вы выбрали опцию 'Только для отслеживания'",
|
"youPickedTrackOnly": "Выбрана настройка «только для отслеживания»",
|
||||||
"trackOnlyAppDescription": "Приложение будет отслеживаться на предмет обновлений, но Obtainium не сможет загрузить или установить его",
|
"trackOnlyAppDescription": "Приложение будет отслеживаться на предмет обновлений, но Obtainium не сможет загрузить или установить его",
|
||||||
"cancelled": "Отменено",
|
"cancelled": "Отменено",
|
||||||
"appAlreadyAdded": "Приложение уже добавлено",
|
"appAlreadyAdded": "Приложение уже добавлено",
|
||||||
"alreadyUpToDateQuestion": "Приложение уже обновлено?",
|
"alreadyUpToDateQuestion": "Приложение уже обновлено?",
|
||||||
"addApp": "Добавить",
|
"addApp": "Добавить",
|
||||||
"appSourceURL": "URL-источник приложения",
|
"appSourceURL": "URL-адрес источника приложения",
|
||||||
"error": "Ошибка",
|
"error": "Ошибка",
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
"searchSomeSourcesLabel": "Поиск (в некоторых источниках)",
|
"searchSomeSourcesLabel": "Поиск (в некоторых источниках)",
|
||||||
@ -63,10 +63,10 @@
|
|||||||
"removeSelectedApps": "Удалить выбранные приложения",
|
"removeSelectedApps": "Удалить выбранные приложения",
|
||||||
"updateX": "Обновить {}",
|
"updateX": "Обновить {}",
|
||||||
"installX": "Установить {}",
|
"installX": "Установить {}",
|
||||||
"markXTrackOnlyAsUpdated": "Отметить {}\n(Только для отслеживания)\nкак обновленное",
|
"markXTrackOnlyAsUpdated": "Отметить {}\n(только для отслеживания)\nкак обновлённое",
|
||||||
"changeX": "Изменить {}",
|
"changeX": "Изменить {}",
|
||||||
"installUpdateApps": "Установить/Обновить приложения",
|
"installUpdateApps": "Установить/обновить приложения",
|
||||||
"installUpdateSelectedApps": "Установить/Обновить выбранные приложения",
|
"installUpdateSelectedApps": "Установить/обновить выбранные приложения",
|
||||||
"markXSelectedAppsAsUpdated": "Выбрано приложений: {}. Отметить как обновлённые?",
|
"markXSelectedAppsAsUpdated": "Выбрано приложений: {}. Отметить как обновлённые?",
|
||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
"yes": "Да",
|
"yes": "Да",
|
||||||
@ -74,10 +74,10 @@
|
|||||||
"pinToTop": "Закрепить сверху",
|
"pinToTop": "Закрепить сверху",
|
||||||
"unpinFromTop": "Открепить",
|
"unpinFromTop": "Открепить",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Сбросить статус установки для выбранных приложений?",
|
"resetInstallStatusForSelectedAppsQuestion": "Сбросить статус установки для выбранных приложений?",
|
||||||
"installStatusOfXWillBeResetExplanation": "Статус установки для выбранных приложений будет сброшен.\n\nЭто может помочь, если версия приложения, отображаемая в Obtainium, некорректная — из-за неудачных обновлений или других проблем",
|
"installStatusOfXWillBeResetExplanation": "Статус установки для выбранных приложений будет сброшен.\n\nЭто может помочь, если версия приложения, отображаемая в Obtainium, некорректная (из-за неудачных обновлений или других проблем).",
|
||||||
"customLinkMessage": "Эти ссылки работают на устройствах с установленным Obtainium",
|
"customLinkMessage": "Эти ссылки работают на устройствах с установленным Obtainium",
|
||||||
"shareAppConfigLinks": "Поделиться конфигурацией приложения в виде HTML-ссылки",
|
"shareAppConfigLinks": "Поделиться конфигурацией приложения в виде HTML-ссылки",
|
||||||
"shareSelectedAppURLs": "Поделиться выбранными URL-адресами приложений",
|
"shareSelectedAppURLs": "Поделиться URL-адресами выбранных приложений",
|
||||||
"resetInstallStatus": "Сбросить статус установки",
|
"resetInstallStatus": "Сбросить статус установки",
|
||||||
"more": "Ещё",
|
"more": "Ещё",
|
||||||
"removeOutdatedFilter": "Удалить фильтр для устаревших приложений",
|
"removeOutdatedFilter": "Удалить фильтр для устаревших приложений",
|
||||||
@ -86,7 +86,7 @@
|
|||||||
"filterApps": "Фильтровать приложения",
|
"filterApps": "Фильтровать приложения",
|
||||||
"appName": "Название приложения",
|
"appName": "Название приложения",
|
||||||
"author": "Автор",
|
"author": "Автор",
|
||||||
"upToDateApps": "Приложения со свежими обновлениями",
|
"upToDateApps": "Приложения с обновлениями",
|
||||||
"nonInstalledApps": "Неустановленные приложения",
|
"nonInstalledApps": "Неустановленные приложения",
|
||||||
"importExport": "Данные",
|
"importExport": "Данные",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
@ -97,12 +97,12 @@
|
|||||||
"obtainiumImport": "Импорт в Obtainium",
|
"obtainiumImport": "Импорт в Obtainium",
|
||||||
"importFromURLList": "Импорт из списка URL-адресов",
|
"importFromURLList": "Импорт из списка URL-адресов",
|
||||||
"searchQuery": "Поисковый запрос",
|
"searchQuery": "Поисковый запрос",
|
||||||
"appURLList": "Список URL приложений",
|
"appURLList": "Список URL-адресов приложений",
|
||||||
"line": "Строка",
|
"line": "Строка",
|
||||||
"searchX": "Поиск {}",
|
"searchX": "Поиск {}",
|
||||||
"noResults": "Результатов не найдено",
|
"noResults": "Ничего не найдено",
|
||||||
"importX": "Импорт {}",
|
"importX": "Импорт {}",
|
||||||
"importedAppsIdDisclaimer": "Импортированные приложения могут неверно отображаться как неустановленные.\nДля исправления этой проблемы повторно установите их через Obtainium.\nЭто не должно повлиять на данные приложения.\n\nПроблемы возникают только при импорте из URL-адреса и сторонних источников",
|
"importedAppsIdDisclaimer": "Импортированные приложения могут неверно отображаться как неустановленные.\nДля исправления этой проблемы повторно установите их через Obtainium.\nЭто не должно повлиять на данные приложения.\n\nПроблемы возникают только при импорте из URL-адреса и сторонних источников.",
|
||||||
"importErrors": "Ошибка импорта",
|
"importErrors": "Ошибка импорта",
|
||||||
"importedXOfYApps": "Импортировано приложений: {} из {}",
|
"importedXOfYApps": "Импортировано приложений: {} из {}",
|
||||||
"followingURLsHadErrors": "При импорте следующие URL-адреса содержали ошибки:",
|
"followingURLsHadErrors": "При импорте следующие URL-адреса содержали ошибки:",
|
||||||
@ -112,12 +112,12 @@
|
|||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
"dark": "Тёмная",
|
"dark": "Тёмная",
|
||||||
"light": "Светлая",
|
"light": "Светлая",
|
||||||
"followSystem": "Системная",
|
"followSystem": "Как в системе",
|
||||||
"followSystemThemeExplanation": "Следование системной теме возможно только при использовании сторонних приложений",
|
"followSystemThemeExplanation": "Следование системной теме возможно только при использовании сторонних приложений",
|
||||||
"useBlackTheme": "Использовать чёрную тему",
|
"useBlackTheme": "Использовать чёрную тему",
|
||||||
"appSortBy": "Сортировка приложений",
|
"appSortBy": "Сортировка приложений",
|
||||||
"authorName": "Автор/Название",
|
"authorName": "Автор/название",
|
||||||
"nameAuthor": "Название/Автор",
|
"nameAuthor": "Название/автор",
|
||||||
"asAdded": "В порядке добавления",
|
"asAdded": "В порядке добавления",
|
||||||
"appSortOrder": "Порядок",
|
"appSortOrder": "Порядок",
|
||||||
"ascending": "По возрастанию",
|
"ascending": "По возрастанию",
|
||||||
@ -126,12 +126,12 @@
|
|||||||
"neverManualOnly": "Никогда — только вручную",
|
"neverManualOnly": "Никогда — только вручную",
|
||||||
"appearance": "Внешний вид",
|
"appearance": "Внешний вид",
|
||||||
"showWebInAppView": "Показывать исходную веб-страницу на странице приложения",
|
"showWebInAppView": "Показывать исходную веб-страницу на странице приложения",
|
||||||
"pinUpdates": "Отображать обновления приложений сверху списка",
|
"pinUpdates": "Показывать обновления приложений сверху списка",
|
||||||
"updates": "Обновления",
|
"updates": "Обновления",
|
||||||
"sourceSpecific": "Настройки источников",
|
"sourceSpecific": "Настройки источников",
|
||||||
"appSource": "Источник",
|
"appSource": "Источник",
|
||||||
"noLogs": "Нет журналов",
|
"noLogs": "Нет журналов",
|
||||||
"appLogs": "Логи",
|
"appLogs": "Журналы",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"share": "Поделиться",
|
"share": "Поделиться",
|
||||||
"appNotFound": "Приложение не найдено",
|
"appNotFound": "Приложение не найдено",
|
||||||
@ -141,21 +141,21 @@
|
|||||||
"deviceSupportsXArch": "Ваше устройство поддерживает архитектуру процессора {}",
|
"deviceSupportsXArch": "Ваше устройство поддерживает архитектуру процессора {}",
|
||||||
"deviceSupportsFollowingArchs": "Ваше устройство поддерживает следующие архитектуры процессора:",
|
"deviceSupportsFollowingArchs": "Ваше устройство поддерживает следующие архитектуры процессора:",
|
||||||
"warning": "Предупреждение",
|
"warning": "Предупреждение",
|
||||||
"sourceIsXButPackageFromYPrompt": "Источник приложения — '{}', но пакет для установки получен из '{}'. Продолжить?",
|
"sourceIsXButPackageFromYPrompt": "Источник приложения — «{}», но пакет для установки получен из «{}». Продолжить?",
|
||||||
"updatesAvailable": "Доступны обновления",
|
"updatesAvailable": "Доступны обновления",
|
||||||
"updatesAvailableNotifDescription": "Уведомляет о наличии обновлений для одного или нескольких приложений в Obtainium",
|
"updatesAvailableNotifDescription": "Уведомляет о наличии обновлений для одного или нескольких приложений в Obtainium",
|
||||||
"noNewUpdates": "Нет новых обновлений",
|
"noNewUpdates": "Нет обновлений",
|
||||||
"xHasAnUpdate": "{} есть обновление",
|
"xHasAnUpdate": "Для {} есть обновление",
|
||||||
"appsUpdated": "Приложения обновлены",
|
"appsUpdated": "Приложения обновлены",
|
||||||
"appsNotUpdated": "Не удалось обновить приложения",
|
"appsNotUpdated": "Не удалось обновить приложения",
|
||||||
"appsUpdatedNotifDescription": "Уведомляет об обновлении одного или нескольких приложений в фоновом режиме",
|
"appsUpdatedNotifDescription": "Уведомляет об обновлении одного или нескольких приложений в фоновом режиме",
|
||||||
"xWasUpdatedToY": "{} была обновлена до версии {}",
|
"xWasUpdatedToY": "{} обновлено до версии {}",
|
||||||
"xWasNotUpdatedToY": "Не удалось обновить {} до версии {}",
|
"xWasNotUpdatedToY": "Не удалось обновить {} до версии {}",
|
||||||
"errorCheckingUpdates": "Ошибка при проверке обновлений",
|
"errorCheckingUpdates": "Ошибка при проверке обновлений",
|
||||||
"errorCheckingUpdatesNotifDescription": "Уведомление о завершении проверки обновлений в фоновом режиме с ошибкой",
|
"errorCheckingUpdatesNotifDescription": "Уведомление о завершении проверки обновлений в фоновом режиме с ошибкой",
|
||||||
"appsRemoved": "Приложение удалено",
|
"appsRemoved": "Приложение удалено",
|
||||||
"appsRemovedNotifDescription": "Уведомление об удалении одного или несколько приложений из-за ошибок при их загрузке",
|
"appsRemovedNotifDescription": "Уведомление об удалении одного или несколько приложений из-за ошибок при их загрузке",
|
||||||
"xWasRemovedDueToErrorY": "{} был удален из-за ошибки: {}",
|
"xWasRemovedDueToErrorY": "{} удалено из-за ошибки: {}",
|
||||||
"completeAppInstallation": "Завершение установки приложения",
|
"completeAppInstallation": "Завершение установки приложения",
|
||||||
"obtainiumMustBeOpenToInstallApps": "Obtainium должен быть открыт для установки приложений",
|
"obtainiumMustBeOpenToInstallApps": "Obtainium должен быть открыт для установки приложений",
|
||||||
"completeAppInstallationNotifDescription": "Уведомление о необходимости открыть Obtainium для завершения установки приложения",
|
"completeAppInstallationNotifDescription": "Уведомление о необходимости открыть Obtainium для завершения установки приложения",
|
||||||
@ -172,7 +172,7 @@
|
|||||||
"installedVersionX": "Установленная версия: {}",
|
"installedVersionX": "Установленная версия: {}",
|
||||||
"lastUpdateCheckX": "Последняя проверка: {}",
|
"lastUpdateCheckX": "Последняя проверка: {}",
|
||||||
"remove": "Удалить",
|
"remove": "Удалить",
|
||||||
"yesMarkUpdated": "Да, отметить как обновленное",
|
"yesMarkUpdated": "Да, отметить как обновлённое",
|
||||||
"fdroid": "Официальные репозитории F-Droid",
|
"fdroid": "Официальные репозитории F-Droid",
|
||||||
"appIdOrName": "ID или название приложения",
|
"appIdOrName": "ID или название приложения",
|
||||||
"appId": "ID приложения",
|
"appId": "ID приложения",
|
||||||
@ -180,39 +180,39 @@
|
|||||||
"reposHaveMultipleApps": "В хранилище несколько приложений",
|
"reposHaveMultipleApps": "В хранилище несколько приложений",
|
||||||
"fdroidThirdPartyRepo": "Сторонние репозитории F-Droid",
|
"fdroidThirdPartyRepo": "Сторонние репозитории F-Droid",
|
||||||
"install": "Установить",
|
"install": "Установить",
|
||||||
"markInstalled": "Пометить как установленное",
|
"markInstalled": "Отметить установленным",
|
||||||
"update": "Обновить",
|
"update": "Обновить",
|
||||||
"markUpdated": "Отметить обновлённым",
|
"markUpdated": "Отметить обновлённым",
|
||||||
"additionalOptions": "Дополнительные настройки",
|
"additionalOptions": "Дополнительные настройки",
|
||||||
"disableVersionDetection": "Отключить обнаружение версии",
|
"disableVersionDetection": "Отключить определение версии",
|
||||||
"noVersionDetectionExplanation": "Эта настройка должна использоваться только для приложений, где обнаружение версии не работает корректно",
|
"noVersionDetectionExplanation": "Эта настройка должна использоваться только для приложений, где определение версии не работает корректно",
|
||||||
"downloadingX": "Загрузка {}",
|
"downloadingX": "Загрузка {}",
|
||||||
"downloadX": "Скачать {}",
|
"downloadX": "Загрузить {}",
|
||||||
"downloadedX": "Загружено {}",
|
"downloadedX": "Загружено {}",
|
||||||
"releaseAsset": "Релизный объект",
|
"releaseAsset": "Объект выпуска",
|
||||||
"downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения",
|
"downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения",
|
||||||
"noAPKFound": "APK не найден",
|
"noAPKFound": "APK-файл не найден",
|
||||||
"noVersionDetection": "Обнаружение версий отключено",
|
"noVersionDetection": "Определение версий отключено",
|
||||||
"categorize": "Категоризировать",
|
"categorize": "Категоризировать",
|
||||||
"categories": "Категории",
|
"categories": "Категории",
|
||||||
"category": "Категория",
|
"category": "Категория",
|
||||||
"noCategory": "Без категории",
|
"noCategory": "Без категории",
|
||||||
"noCategories": "Без категорий",
|
"noCategories": "Без категорий",
|
||||||
"deleteCategoriesQuestion": "Удалить категории?",
|
"deleteCategoriesQuestion": "Удалить категории?",
|
||||||
"categoryDeleteWarning": "Все приложения в удаленных категориях будут помечены как без категории",
|
"categoryDeleteWarning": "Все приложения в удалённых категориях будут помечены как «без категории»",
|
||||||
"addCategory": "Добавить категорию",
|
"addCategory": "Добавить категорию",
|
||||||
"label": "Метка",
|
"label": "Метка",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"copiedToClipboard": "Скопировано в буфер обмена",
|
"copiedToClipboard": "Скопировано в буфер обмена",
|
||||||
"storagePermissionDenied": "Отказано в доступе к хранилищу",
|
"storagePermissionDenied": "Отказано в доступе к хранилищу",
|
||||||
"selectedCategorizeWarning": "Это заменит все текущие настройки категорий для выбранных приложений",
|
"selectedCategorizeWarning": "Это заменит все текущие настройки категорий для выбранных приложений",
|
||||||
"filterAPKsByRegEx": "Отфильтровать APK-файлы\n(регулярное выражение)",
|
"filterAPKsByRegEx": "Фильтровать APK-файлы\n(регулярное выражение)",
|
||||||
"removeFromObtainium": "Удалить из Obtainium",
|
"removeFromObtainium": "Удалить из Obtainium",
|
||||||
"uninstallFromDevice": "Удалить с устройства",
|
"uninstallFromDevice": "Удалить с устройства",
|
||||||
"onlyWorksWithNonVersionDetectApps": "Работает только для приложений с отключенным определением версии",
|
"onlyWorksWithNonVersionDetectApps": "Работает только для приложений с отключённым определением версии",
|
||||||
"releaseDateAsVersion": "Дата выпуска вместо версии",
|
"releaseDateAsVersion": "Использовать дату выпуска в качестве версии",
|
||||||
"releaseTitleAsVersion": "Используйте название релиза в качестве строки версии",
|
"releaseTitleAsVersion": "Использовать название выпуска в качестве версии",
|
||||||
"releaseDateAsVersionExplanation": "Этот параметр следует использовать только для приложений, в которых определение версии не работает правильно, но имеется дата выпуска",
|
"releaseDateAsVersionExplanation": "Эта настройка должна использоваться только для приложений, в которых определение версии не работает правильно, но имеется дата выпуска",
|
||||||
"changes": "Изменения",
|
"changes": "Изменения",
|
||||||
"releaseDate": "Дата выпуска",
|
"releaseDate": "Дата выпуска",
|
||||||
"importFromURLsInFile": "Импорт из файла URL-адресов (например: OPML)",
|
"importFromURLsInFile": "Импорт из файла URL-адресов (например: OPML)",
|
||||||
@ -220,56 +220,56 @@
|
|||||||
"versionDetection": "Определение версии",
|
"versionDetection": "Определение версии",
|
||||||
"standardVersionDetection": "Стандартное",
|
"standardVersionDetection": "Стандартное",
|
||||||
"groupByCategory": "Группировать по категориям",
|
"groupByCategory": "Группировать по категориям",
|
||||||
"autoApkFilterByArch": "Попытаться отфильтровать APK-файлы по архитектуре процессора",
|
"autoApkFilterByArch": "Пытаться фильтровать APK по архитектуре процессора",
|
||||||
"autoLinkFilterByArch": "Попытайтесь отфильтровать ссылки по архитектуре процессора, если это возможно",
|
"autoLinkFilterByArch": "Пытаться фильтровать ссылки по архитектуре процессора, если это возможно",
|
||||||
"overrideSource": "Переопределить источник",
|
"overrideSource": "Переопределить источник",
|
||||||
"dontShowAgain": "Не показывать снова",
|
"dontShowAgain": "Не показывать снова",
|
||||||
"dontShowTrackOnlyWarnings": "Не показывать предупреждения о только отслеживаемых приложениях",
|
"dontShowTrackOnlyWarnings": "Не показывать предупреждения о только отслеживаемых приложениях",
|
||||||
"dontShowAPKOriginWarnings": "Не показывать предупреждения об отличающемся источнике APK-файлов",
|
"dontShowAPKOriginWarnings": "Не показывать предупреждения об отличающемся источнике APK-файлов",
|
||||||
"moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка",
|
"moveNonInstalledAppsToBottom": "Показывать неустановленные приложения внизу списка",
|
||||||
"gitlabPATLabel": "Персональный токен доступа GitLab",
|
"gitlabPATLabel": "Персональный токен доступа GitLab",
|
||||||
"about": "Описание",
|
"about": "Описание",
|
||||||
"requiresCredentialsInSettings": "{}: Для этого требуются дополнительные учетные данные (в настройках)",
|
"requiresCredentialsInSettings": "{}: для этого требуются дополнительные учётные данные (в настройках)",
|
||||||
"checkOnStart": "Проверять наличие обновлений при запуске",
|
"checkOnStart": "Проверять наличие обновлений при запуске",
|
||||||
"tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода",
|
"tryInferAppIdFromCode": "Пытаться определить ID приложения из исходного кода",
|
||||||
"removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения",
|
"removeOnExternalUninstall": "Автоматически убирать из списка удалённые извне приложения",
|
||||||
"pickHighestVersionCode": "Автовыбор актуальной версии кода APK",
|
"pickHighestVersionCode": "Автовыбор актуальной версии кода APK",
|
||||||
"checkUpdateOnDetailPage": "Проверять наличие обновлений при открытии страницы приложения",
|
"checkUpdateOnDetailPage": "Проверять наличие обновлений при открытии страницы приложения",
|
||||||
"disablePageTransitions": "Отключить анимацию перехода между страницами",
|
"disablePageTransitions": "Отключить анимацию перехода между страницами",
|
||||||
"reversePageTransitions": "Реверс анимации перехода между страницами",
|
"reversePageTransitions": "Реверс анимации перехода между страницами",
|
||||||
"minStarCount": "Минимальное количество звёзд",
|
"minStarCount": "Минимальное количество звёзд",
|
||||||
"addInfoBelow": "Добавьте эту информацию ниже",
|
"addInfoBelow": "Добавьте эту информацию ниже",
|
||||||
"addInfoInSettings": "Добавьте эту информацию в Настройки",
|
"addInfoInSettings": "Добавьте эту информацию в «Настройки»",
|
||||||
"githubSourceNote": "Используя ключ API можно обойти лимит запросов GitHub",
|
"githubSourceNote": "Используя ключ API, можно обойти ограничение запросов GitHub",
|
||||||
"sortByLastLinkSegment": "Сортировать только по последнему сегменту ссылки",
|
"sortByLastLinkSegment": "Сортировать только по последнему сегменту ссылки",
|
||||||
"filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
|
"filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
|
||||||
"customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: '.apk$')",
|
"customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: «.apk$»)",
|
||||||
"appsPossiblyUpdated": "Попытки обновления приложений",
|
"appsPossiblyUpdated": "Попытки обновления приложений",
|
||||||
"appsPossiblyUpdatedNotifDescription": "Уведомление о возможных обновлениях одного или нескольких приложений в фоновом режиме",
|
"appsPossiblyUpdatedNotifDescription": "Уведомление о возможных обновлениях одного или нескольких приложений в фоновом режиме",
|
||||||
"xWasPossiblyUpdatedToY": "{} возможно был обновлен до {}",
|
"xWasPossiblyUpdatedToY": "{} возможно был обновлён до {}",
|
||||||
"enableBackgroundUpdates": "Включить обновления в фоне",
|
"enableBackgroundUpdates": "Включить обновления в фоне",
|
||||||
"backgroundUpdateReqsExplanation": "Фоновые обновления могут быть возможны не для всех приложений",
|
"backgroundUpdateReqsExplanation": "Фоновые обновления могут быть возможны не для всех приложений",
|
||||||
"backgroundUpdateLimitsExplanation": "Успешность фоновой установки можно определить только после открытия Obtainium",
|
"backgroundUpdateLimitsExplanation": "Успешность фоновой установки можно определить только после открытия Obtainium",
|
||||||
"verifyLatestTag": "Проверять тег 'latest'",
|
"verifyLatestTag": "Проверять метку «latest»",
|
||||||
"intermediateLinkRegex": "Фильтр для \"промежуточной\" ссылки для посещения",
|
"intermediateLinkRegex": "Фильтр для «промежуточной» ссылки для посещения",
|
||||||
"filterByLinkText": "Фильтрация ссылок по тексту ссылки",
|
"filterByLinkText": "Фильтрация ссылок по тексту ссылки",
|
||||||
"intermediateLinkNotFound": "Промежуточная ссылка не найдена",
|
"intermediateLinkNotFound": "Промежуточная ссылка не найдена",
|
||||||
"intermediateLink": "Промежуточная ссылка",
|
"intermediateLink": "Промежуточная ссылка",
|
||||||
"exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
|
"exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
|
||||||
"bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет соединения с Wi-Fi",
|
"bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет подключения к Wi-Fi",
|
||||||
"bgUpdatesWhileChargingOnly": "Отключение фоновых обновлений при отсутствии зарядки",
|
"bgUpdatesWhileChargingOnly": "Отключить фоновые обновления, если не идёт зарядка",
|
||||||
"autoSelectHighestVersionCode": "Автоматически выбирать APK с актуальной версией кода",
|
"autoSelectHighestVersionCode": "Автовыбор APK с актуальной версией кода",
|
||||||
"versionExtractionRegEx": "Регулярное выражение для извлечения версии",
|
"versionExtractionRegEx": "Извлечение версии\n(регулярное выражение)",
|
||||||
"trimVersionString": "Обрезка строки версии с помощью RegEx",
|
"trimVersionString": "Обрезка строки версии\n(регулярное выражение)",
|
||||||
"matchGroupToUseForX": "Группа соответствия, которую следует использовать для \"{}\"",
|
"matchGroupToUseForX": "Группа соответствия для «{}»",
|
||||||
"matchGroupToUse": "Выберите группу для использования",
|
"matchGroupToUse": "Выбрать группу соответствия",
|
||||||
"highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
|
"highlightTouchTargets": "Выделять менее очевидные элементы управления касанием",
|
||||||
"pickExportDir": "Выбрать каталог для экспорта",
|
"pickExportDir": "Выбрать каталог для экспорта",
|
||||||
"autoExportOnChanges": "Автоэкспорт при изменениях",
|
"autoExportOnChanges": "Автоэкспорт при изменениях",
|
||||||
"includeSettings": "Включить настройки",
|
"includeSettings": "Включить настройки",
|
||||||
"filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
|
"filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
|
||||||
"trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
|
"trySelectingSuggestedVersionCode": "Пробовать выбрать предложенный код версии APK",
|
||||||
"dontSortReleasesList": "Сохранить порядок релизов от API",
|
"dontSortReleasesList": "Сохранить порядок выпусков от API",
|
||||||
"reverseSort": "Обратная сортировка",
|
"reverseSort": "Обратная сортировка",
|
||||||
"takeFirstLink": "Взять первую ссылку",
|
"takeFirstLink": "Взять первую ссылку",
|
||||||
"skipSort": "Пропустить сортировку",
|
"skipSort": "Пропустить сортировку",
|
||||||
@ -290,18 +290,18 @@
|
|||||||
"onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
|
"onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
|
||||||
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
|
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
|
||||||
"selectX": "Выбрать {}",
|
"selectX": "Выбрать {}",
|
||||||
"parallelDownloads": "Разрешить параллельные загрузки",
|
"parallelDownloads": "Использовать параллельные загрузки",
|
||||||
"useShizuku": "Использовать Shizuku или Sui для установки",
|
"useShizuku": "Использовать Shizuku или Sui для установки",
|
||||||
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден, возможно он не запущен",
|
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден, возможно он не запущен",
|
||||||
"shizukuOld": "Устаревшая версия Shizuku (<11), обновите",
|
"shizukuOld": "Устаревшая версия Shizuku (<11), обновите",
|
||||||
"shizukuOldAndroidWithADB": "Shizuku работает на Android < 8.1 с ADB, обновите Android или используйте Sui",
|
"shizukuOldAndroidWithADB": "Shizuku работает на Android < 8.1 с ADB, обновите Android или используйте Sui",
|
||||||
"shizukuPretendToBeGooglePlay": "Указать Google Play как источник установки (если используется Shizuku)",
|
"shizukuPretendToBeGooglePlay": "Указывать Google Play как источник установки (если используется Shizuku)",
|
||||||
"useSystemFont": "Использовать системный шрифт",
|
"useSystemFont": "Использовать системный шрифт",
|
||||||
"useVersionCodeAsOSVersion": "Использовать код версии приложения как версию, обнаруженную ОС",
|
"useVersionCodeAsOSVersion": "Использовать код версии приложения как версию, обнаруженную ОС",
|
||||||
"requestHeader": "Заголовок запроса",
|
"requestHeader": "Заголовок запроса",
|
||||||
"useLatestAssetDateAsReleaseDate": "Использовать последнюю загрузку ресурса в качестве даты выпуска",
|
"useLatestAssetDateAsReleaseDate": "Использовать последнюю загрузку объекта в качестве даты выпуска",
|
||||||
"defaultPseudoVersioningMethod": "Метод псевдоверсионирования по умолчанию",
|
"defaultPseudoVersioningMethod": "Метод псевдоверсионирования по умолчанию",
|
||||||
"partialAPKHash": "Частичный хэш APK",
|
"partialAPKHash": "Частичный хеш APK",
|
||||||
"APKLinkHash": "Хеш ссылки APK",
|
"APKLinkHash": "Хеш ссылки APK",
|
||||||
"directAPKLink": "Прямая ссылка на APK",
|
"directAPKLink": "Прямая ссылка на APK",
|
||||||
"pseudoVersionInUse": "Используется псевдоверсия",
|
"pseudoVersionInUse": "Используется псевдоверсия",
|
||||||
@ -309,17 +309,17 @@
|
|||||||
"latest": "Последний",
|
"latest": "Последний",
|
||||||
"invertRegEx": "Инвертировать регулярное выражение",
|
"invertRegEx": "Инвертировать регулярное выражение",
|
||||||
"note": "Примечание",
|
"note": "Примечание",
|
||||||
"selfHostedNote": "Выпадающий список \"{}\" можно использовать для доступа к самостоятельно размещенным/настроенным экземплярам любого источника.",
|
"selfHostedNote": "Выпадающий список «{}» можно использовать для доступа к самостоятельно размещённым/настроенным экземплярам любого источника.",
|
||||||
"badDownload": "APK не удалось разобрать (несовместимая или неполная загрузка)",
|
"badDownload": "Не удалось разобрать APK (несовместимая или неполная загрузка)",
|
||||||
"beforeNewInstallsShareToAppVerifier": "Поделитесь новыми приложениями с AppVerifier (если доступно)",
|
"beforeNewInstallsShareToAppVerifier": "Делиться новыми приложениями с AppVerifier (если доступно)",
|
||||||
"appVerifierInstructionToast": "Поделитесь с AppVerifier, а затем вернитесь сюда, когда будете готовы.",
|
"appVerifierInstructionToast": "Поделитесь с AppVerifier, а затем вернитесь сюда, когда будете готовы.",
|
||||||
"wiki": "Помощь/Вики",
|
"wiki": "Помощь/Вики",
|
||||||
"crowdsourcedConfigsLabel": "Конфигурации приложений на основе краудсорсинга (используйте на свой страх и риск)",
|
"crowdsourcedConfigsLabel": "Конфигурации приложений на основе краудсорсинга (используйте на свой страх и риск)",
|
||||||
"crowdsourcedConfigsShort": "Конфиги приложений с помощью краудсорсинга",
|
"crowdsourcedConfigsShort": "Краудсорсинг конфигураций приложений",
|
||||||
"allowInsecure": "Разрешить небезопасные HTTP-запросы",
|
"allowInsecure": "Разрешить небезопасные HTTP-запросы",
|
||||||
"stayOneVersionBehind": "Не отставайте от последней версии",
|
"stayOneVersionBehind": "Оставаться на одну версию ниже последней",
|
||||||
"useFirstApkOfVersion": "Автоматический выбор первого из нескольких APK",
|
"useFirstApkOfVersion": "Автовыбор первого из нескольких APK",
|
||||||
"refreshBeforeDownload": "Обновляйте информацию о приложении перед загрузкой",
|
"refreshBeforeDownload": "Обновлять информацию о приложении перед загрузкой",
|
||||||
"tencentAppStore": "Tencent App Store",
|
"tencentAppStore": "Tencent App Store",
|
||||||
"coolApk": "CoolApk",
|
"coolApk": "CoolApk",
|
||||||
"vivoAppStore": "vivo App Store (CN)",
|
"vivoAppStore": "vivo App Store (CN)",
|
||||||
@ -327,67 +327,82 @@
|
|||||||
"smartname": "Имя (умное)",
|
"smartname": "Имя (умное)",
|
||||||
"sortMethod": "Метод сортировки",
|
"sortMethod": "Метод сортировки",
|
||||||
"welcome": "Добро пожаловать",
|
"welcome": "Добро пожаловать",
|
||||||
"documentationLinksNote": "На странице Obtainium GitHub, ссылка на которую приведена ниже, содержатся ссылки на видео, статьи, обсуждения и другие ресурсы, которые помогут вам понять, как пользоваться приложением.",
|
"documentationLinksNote": "На странице Obtainium в GitHub, ссылка на которую приведена ниже, содержатся ссылки на видео, статьи, обсуждения и другие ресурсы, которые помогут вам понять, как пользоваться приложением.",
|
||||||
"batteryOptimizationNote": "Обратите внимание, что фоновая загрузка может работать более надежно, если отключить оптимизацию батареи ОС для Obtainium.",
|
"batteryOptimizationNote": "Обратите внимание, что фоновая загрузка может работать более надёжно, если отключить оптимизацию батареи ОС для Obtainium.",
|
||||||
"fileDeletionError": "Не удалось удалить файл (попробуйте удалить его вручную, а затем повторите попытку): \"{}\"",
|
"fileDeletionError": "Не удалось удалить файл (попробуйте удалить его вручную, а затем повторите попытку): «{}»",
|
||||||
|
"foregroundService": "Передний план службы Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Используйте службу переднего плана для проверки обновлений (более надежно, потребляет больше энергии).",
|
||||||
|
"fgServiceNotice": "Это уведомление необходимо для фоновой проверки обновлений (его можно скрыть в настройках ОС)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Удалить приложение?",
|
"one": "Удалить приложение?",
|
||||||
"other": "Удалить приложения?"
|
"other": "Удалить приложения?"
|
||||||
},
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Слишком много запросов (ограничение скорости) — попробуйте снова через {} минуту",
|
"one": "Слишком много запросов (ограничение скорости) — попробуйте снова через {} минуту",
|
||||||
"other": "Слишком много запросов (ограничение скорости) — попробуйте снова через {} минуты"
|
"few": "Слишком много запросов (ограничение скорости) — попробуйте снова через {} минуты",
|
||||||
|
"other": "Слишком много запросов (ограничение скорости) — попробуйте снова через {} минут"
|
||||||
},
|
},
|
||||||
"bgUpdateGotErrorRetryInMinutes": {
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
"one": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет запланирована через {} минуту",
|
"one": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет через {} минуту",
|
||||||
"other": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет запланирована через {} минуты"
|
"few": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет через {} минуты",
|
||||||
|
"other": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет через {} минут"
|
||||||
},
|
},
|
||||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
"one": "В ходе проверки обновления в фоновом режиме было обнаружено {} обновление — Пользователю будет отправлено уведомление, если это необходимо",
|
"one": "При проверке в фоновом режиме было обнаружено {} обновления — пользователь будет уведомлен по необходимости",
|
||||||
"other": "В ходе проверки обновления в фоновом режиме было обнаружено {} обновлений — Пользователю будет отправлено уведомление, если это необходимо"
|
"other": "При проверке в фоновом режиме было обнаружено {} обновлений — пользователь будет уведомлен по необходимости"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"one": "{} приложение",
|
"one": "{} приложение",
|
||||||
|
"few": "{} приложения",
|
||||||
"other": "{} приложений"
|
"other": "{} приложений"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"one": "{} URL-адрес",
|
"one": "{} URL-адрес",
|
||||||
"other": "{} URL-адреса"
|
"few": "{} URL-адреса",
|
||||||
|
"other": "{} URL-адресов"
|
||||||
},
|
},
|
||||||
"minute": {
|
"minute": {
|
||||||
"one": "{} минута",
|
"one": "{} минута",
|
||||||
"other": "{} минуты"
|
"few": "{} минуты",
|
||||||
|
"other": "{} минут"
|
||||||
},
|
},
|
||||||
"hour": {
|
"hour": {
|
||||||
"one": "{} час",
|
"one": "{} час",
|
||||||
|
"few": "{} часа",
|
||||||
"other": "{} часов"
|
"other": "{} часов"
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"one": "{} день",
|
"one": "{} день",
|
||||||
|
"few": "{} дня",
|
||||||
"other": "{} дней"
|
"other": "{} дней"
|
||||||
},
|
},
|
||||||
"clearedNLogsBeforeXAfterY": {
|
"clearedNLogsBeforeXAfterY": {
|
||||||
"one": "Очищен {n} журнал (до = {before}, после = {after})",
|
"one": "Очищен {n} журнал (до = {before}, после = {after})",
|
||||||
|
"few": "Очищены {n} журнала (до = {before}, после = {after})",
|
||||||
"other": "Очищено {n} журналов (до = {before}, после = {after})"
|
"other": "Очищено {n} журналов (до = {before}, после = {after})"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesAvailable": {
|
"xAndNMoreUpdatesAvailable": {
|
||||||
"one": "У {} и еще 1 приложения есть обновление",
|
"one": "У {} и ещё 1 приложения есть обновление",
|
||||||
"other": "У {} и ещё {} приложений есть обновления"
|
"other": "У {} и ещё {} приложений есть обновления"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesInstalled": {
|
"xAndNMoreUpdatesInstalled": {
|
||||||
"one": "{} и ещё 1 приложение были обновлены",
|
"one": "{} и ещё 1 приложение были обновлены",
|
||||||
|
"few": "{} и ещё {} приложения были обновлены",
|
||||||
"other": "{} и ещё {} приложений были обновлены"
|
"other": "{} и ещё {} приложений были обновлены"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesFailed": {
|
"xAndNMoreUpdatesFailed": {
|
||||||
"one": "Не удалось обновить {} и ещё 1 приложение",
|
"one": "Не удалось обновить {} и ещё 1 приложение",
|
||||||
|
"few": "Не удалось обновить {} и ещё {} приложения",
|
||||||
"other": "Не удалось обновить {} и ещё {} приложений"
|
"other": "Не удалось обновить {} и ещё {} приложений"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||||
"one": "{} и ещё 1 приложение могли быть обновлены",
|
"one": "{} и ещё 1 приложение могли быть обновлены",
|
||||||
|
"few": "{} и ещё {} приложения могли быть обновлены",
|
||||||
"other": "{} и ещё {} приложений могли быть обновлены"
|
"other": "{} и ещё {} приложений могли быть обновлены"
|
||||||
},
|
},
|
||||||
"apk": {
|
"apk": {
|
||||||
"one": "{} APK",
|
"one": "{} APK-файл",
|
||||||
"other": "{} APKи"
|
"few": "{} APK-файла",
|
||||||
|
"other": "{} APK-файлов"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,11 @@ const neverAutoTranslate = {
|
|||||||
placeholder: ['pl'],
|
placeholder: ['pl'],
|
||||||
importExport: ['fr'],
|
importExport: ['fr'],
|
||||||
url: ['fr'],
|
url: ['fr'],
|
||||||
vivoAppStore: ['cs', 'ja', 'ko'],
|
vivoAppStore: ['cs', 'ja', 'ko', 'vi'],
|
||||||
coolapk: ['ja'],
|
coolapk: ['ja'],
|
||||||
obtainiumImport: ['nl'],
|
obtainiumImport: ['nl'],
|
||||||
appLogs: ['nl'],
|
appLogs: ['nl'],
|
||||||
|
apks: ['vi'],
|
||||||
tencentAppStore: ['*']
|
tencentAppStore: ['*']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Obtainium GitHub-sidan som länkas nedan innehåller länkar till videor, artiklar, diskussioner och andra resurser som hjälper dig att förstå hur du använder appen.",
|
"documentationLinksNote": "Obtainium GitHub-sidan som länkas nedan innehåller länkar till videor, artiklar, diskussioner och andra resurser som hjälper dig att förstå hur du använder appen.",
|
||||||
"batteryOptimizationNote": "Observera att nedladdningar i bakgrunden kan fungera mer tillförlitligt om du inaktiverar OS-batterioptimeringar för Obtainium.",
|
"batteryOptimizationNote": "Observera att nedladdningar i bakgrunden kan fungera mer tillförlitligt om du inaktiverar OS-batterioptimeringar för Obtainium.",
|
||||||
"fileDeletionError": "Misslyckades med att radera filen (försök radera den manuellt och försök sedan igen): \"{}\"",
|
"fileDeletionError": "Misslyckades med att radera filen (försök radera den manuellt och försök sedan igen): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium förgrundstjänst",
|
||||||
|
"foregroundServiceExplanation": "Använd en förgrundstjänst för uppdateringskontroll (mer tillförlitlig, förbrukar mer ström)",
|
||||||
|
"fgServiceNotice": "Detta meddelande krävs för bakgrundsuppdateringskontroll (det kan döljas i OS-inställningarna)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Ta Bort App?",
|
"one": "Ta Bort App?",
|
||||||
"other": "Ta Bort Appar?"
|
"other": "Ta Bort Appar?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Aşağıda bağlantısı verilen Obtainium GitHub sayfası, uygulamayı nasıl kullanacağınızı anlamanıza yardımcı olacak videolara, makalelere, tartışmalara ve diğer kaynaklara bağlantılar içerir.",
|
"documentationLinksNote": "Aşağıda bağlantısı verilen Obtainium GitHub sayfası, uygulamayı nasıl kullanacağınızı anlamanıza yardımcı olacak videolara, makalelere, tartışmalara ve diğer kaynaklara bağlantılar içerir.",
|
||||||
"batteryOptimizationNote": "Obtainium için işletim sistemi pil optimizasyonlarını devre dışı bırakırsanız arka planda indirmelerin daha güvenilir şekilde çalışabileceğini unutmayın.",
|
"batteryOptimizationNote": "Obtainium için işletim sistemi pil optimizasyonlarını devre dışı bırakırsanız arka planda indirmelerin daha güvenilir şekilde çalışabileceğini unutmayın.",
|
||||||
"fileDeletionError": "Dosya silinemedi (elle silmeyi deneyin ve sonra tekrar deneyin): \"{}\"",
|
"fileDeletionError": "Dosya silinemedi (elle silmeyi deneyin ve sonra tekrar deneyin): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium ön plan hizmeti",
|
||||||
|
"foregroundServiceExplanation": "Güncelleme denetimi için bir ön plan hizmeti kullanın (daha güvenilir, daha fazla güç tüketir)",
|
||||||
|
"fgServiceNotice": "Bu bildirim arka planda güncelleme kontrolü için gereklidir (işletim sistemi ayarlarından gizlenebilir)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Uygulamayı Kaldır?",
|
"one": "Uygulamayı Kaldır?",
|
||||||
"other": "Uygulamaları Kaldır?"
|
"other": "Uygulamaları Kaldır?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "Сторінка Obtainium на GitHub, посилання на яку наведено нижче, містить посилання на відео, статті, дискусії та інші ресурси, які допоможуть вам зрозуміти, як користуватися додатком.",
|
"documentationLinksNote": "Сторінка Obtainium на GitHub, посилання на яку наведено нижче, містить посилання на відео, статті, дискусії та інші ресурси, які допоможуть вам зрозуміти, як користуватися додатком.",
|
||||||
"batteryOptimizationNote": "Зауважте, що фонові завантаження можуть працювати надійніше, якщо ви вимкнете оптимізацію батареї ОС для Obtainium.",
|
"batteryOptimizationNote": "Зауважте, що фонові завантаження можуть працювати надійніше, якщо ви вимкнете оптимізацію батареї ОС для Obtainium.",
|
||||||
"fileDeletionError": "Не вдалося видалити файл (спробуйте видалити його вручну, а потім спробуйте ще раз): \"{}\"",
|
"fileDeletionError": "Не вдалося видалити файл (спробуйте видалити його вручну, а потім спробуйте ще раз): \"{}\"",
|
||||||
|
"foregroundService": "Обслуговування переднього плану Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Використовуйте службу переднього плану для перевірки оновлень (надійніша, споживає більше енергії)",
|
||||||
|
"fgServiceNotice": "Це сповіщення необхідне для фонової перевірки оновлень (його можна приховати в налаштуваннях ОС)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Видалити застосунок?",
|
"one": "Видалити застосунок?",
|
||||||
"other": "Видалити застосунки?"
|
"other": "Видалити застосунки?"
|
||||||
|
@ -113,7 +113,7 @@
|
|||||||
"dark": "Tối",
|
"dark": "Tối",
|
||||||
"light": "Sáng",
|
"light": "Sáng",
|
||||||
"followSystem": "Theo hệ thống",
|
"followSystem": "Theo hệ thống",
|
||||||
"followSystemThemeExplanation": "Following system theme is possible only by using third-party applications",
|
"followSystemThemeExplanation": "Việc áp dụng chủ đề hệ thống chỉ có thể thực hiện được thông qua việc sử dụng các ứng dụng của bên thứ ba.",
|
||||||
"useBlackTheme": "Nền đen",
|
"useBlackTheme": "Nền đen",
|
||||||
"appSortBy": "Sắp xếp ứng dụng",
|
"appSortBy": "Sắp xếp ứng dụng",
|
||||||
"authorName": "Tác giả",
|
"authorName": "Tác giả",
|
||||||
@ -211,7 +211,7 @@
|
|||||||
"uninstallFromDevice": "Gỡ cài đặt khỏi thiết bị",
|
"uninstallFromDevice": "Gỡ cài đặt khỏi thiết bị",
|
||||||
"onlyWorksWithNonVersionDetectApps": "Chỉ hoạt động với Ứng dụng đã tắt tính năng phát hiện phiên bản.",
|
"onlyWorksWithNonVersionDetectApps": "Chỉ hoạt động với Ứng dụng đã tắt tính năng phát hiện phiên bản.",
|
||||||
"releaseDateAsVersion": "Sử dụng ngày phát hành làm phiên bản",
|
"releaseDateAsVersion": "Sử dụng ngày phát hành làm phiên bản",
|
||||||
"releaseTitleAsVersion": "Use release title as version string",
|
"releaseTitleAsVersion": "Sử dụng tiêu đề bản phát hành làm chuỗi phiên bản",
|
||||||
"releaseDateAsVersionExplanation": "Chỉ nên sử dụng tùy chọn này cho Ứng dụng trong đó tính năng phát hiện phiên bản không hoạt động chính xác nhưng đã có ngày phát hành.",
|
"releaseDateAsVersionExplanation": "Chỉ nên sử dụng tùy chọn này cho Ứng dụng trong đó tính năng phát hiện phiên bản không hoạt động chính xác nhưng đã có ngày phát hành.",
|
||||||
"changes": "Thay đổi",
|
"changes": "Thay đổi",
|
||||||
"releaseDate": "Ngày phát hành",
|
"releaseDate": "Ngày phát hành",
|
||||||
@ -221,7 +221,7 @@
|
|||||||
"standardVersionDetection": "Phát hiện phiên bản tiêu chuẩn",
|
"standardVersionDetection": "Phát hiện phiên bản tiêu chuẩn",
|
||||||
"groupByCategory": "Nhóm theo danh mục",
|
"groupByCategory": "Nhóm theo danh mục",
|
||||||
"autoApkFilterByArch": "Cố gắng lọc APK theo kiến trúc CPU nếu có thể",
|
"autoApkFilterByArch": "Cố gắng lọc APK theo kiến trúc CPU nếu có thể",
|
||||||
"autoLinkFilterByArch": "Attempt to filter links by CPU architecture if possible",
|
"autoLinkFilterByArch": "Cố gắng lọc các liên kết theo kiến trúc CPU nếu có thể.",
|
||||||
"overrideSource": "Ghi đè nguồn",
|
"overrideSource": "Ghi đè nguồn",
|
||||||
"dontShowAgain": "Đừng hiển thị thông tin này nữa",
|
"dontShowAgain": "Đừng hiển thị thông tin này nữa",
|
||||||
"dontShowTrackOnlyWarnings": "Không hiển thị cảnh báo 'Chỉ theo dõi'",
|
"dontShowTrackOnlyWarnings": "Không hiển thị cảnh báo 'Chỉ theo dõi'",
|
||||||
@ -257,7 +257,7 @@
|
|||||||
"intermediateLink": "Liên kết trung gian",
|
"intermediateLink": "Liên kết trung gian",
|
||||||
"exemptFromBackgroundUpdates": "Miễn cập nhật nền (nếu được bật)",
|
"exemptFromBackgroundUpdates": "Miễn cập nhật nền (nếu được bật)",
|
||||||
"bgUpdatesOnWiFiOnly": "Tắt cập nhật nền khi không có Wi-Fi",
|
"bgUpdatesOnWiFiOnly": "Tắt cập nhật nền khi không có Wi-Fi",
|
||||||
"bgUpdatesWhileChargingOnly": "Disable background updates when not charging",
|
"bgUpdatesWhileChargingOnly": "Tắt cập nhật nền khi không sạc pin.",
|
||||||
"autoSelectHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
|
"autoSelectHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
|
||||||
"versionExtractionRegEx": "Trích xuất phiên bản RegEx",
|
"versionExtractionRegEx": "Trích xuất phiên bản RegEx",
|
||||||
"trimVersionString": "Trim Version String With RegEx",
|
"trimVersionString": "Trim Version String With RegEx",
|
||||||
@ -295,7 +295,7 @@
|
|||||||
"shizukuBinderNotFound": "Shizuku chưa khởi động",
|
"shizukuBinderNotFound": "Shizuku chưa khởi động",
|
||||||
"shizukuOld": "Phiên bản Shizuku lỗi thời (<11) - hãy cập nhật nó",
|
"shizukuOld": "Phiên bản Shizuku lỗi thời (<11) - hãy cập nhật nó",
|
||||||
"shizukuOldAndroidWithADB": "Shizuku chạy trên Android < 8.1 với ADB - hãy cập nhật Android hoặc thay bằng Sui",
|
"shizukuOldAndroidWithADB": "Shizuku chạy trên Android < 8.1 với ADB - hãy cập nhật Android hoặc thay bằng Sui",
|
||||||
"shizukuPretendToBeGooglePlay": "Set Google Play as the installation source (if Shizuku is used)",
|
"shizukuPretendToBeGooglePlay": "Đặt Google Play làm nguồn cài đặt (nếu sử dụng Shizuku)",
|
||||||
"useSystemFont": "Sử dụng phông chữ hệ thống",
|
"useSystemFont": "Sử dụng phông chữ hệ thống",
|
||||||
"useVersionCodeAsOSVersion": "Sử dụng Mã phiên bản ứng dụng làm phiên bản do hệ điều hành phát hiện",
|
"useVersionCodeAsOSVersion": "Sử dụng Mã phiên bản ứng dụng làm phiên bản do hệ điều hành phát hiện",
|
||||||
"requestHeader": "Tiêu đề yêu cầu",
|
"requestHeader": "Tiêu đề yêu cầu",
|
||||||
@ -316,20 +316,23 @@
|
|||||||
"wiki": "Trợ giúp/Wiki",
|
"wiki": "Trợ giúp/Wiki",
|
||||||
"crowdsourcedConfigsLabel": "Crowdsourced App Configurations (use at your own risk)",
|
"crowdsourcedConfigsLabel": "Crowdsourced App Configurations (use at your own risk)",
|
||||||
"crowdsourcedConfigsShort": "Crowdsourced App Configurations",
|
"crowdsourcedConfigsShort": "Crowdsourced App Configurations",
|
||||||
"allowInsecure": "Allow insecure HTTP requests",
|
"allowInsecure": "Cho phép các yêu cầu HTTP không an toàn",
|
||||||
"stayOneVersionBehind": "Stay one version behind latest",
|
"stayOneVersionBehind": "Luôn sử dụng phiên bản cũ hơn một phiên bản so với phiên bản mới nhất.",
|
||||||
"useFirstApkOfVersion": "Auto-select first of multiple APKs",
|
"useFirstApkOfVersion": "Tự động chọn tệp APK đầu tiên trong danh sách nhiều tệp APK.",
|
||||||
"refreshBeforeDownload": "Refresh app details before download",
|
"refreshBeforeDownload": "Cập nhật thông tin ứng dụng trước khi tải xuống",
|
||||||
"tencentAppStore": "Tencent App Store",
|
"tencentAppStore": "Tencent App Store",
|
||||||
"coolApk": "CoolApk",
|
"coolApk": "CoolApk",
|
||||||
"vivoAppStore": "vivo App Store (CN)",
|
"vivoAppStore": "vivo App Store (CN)",
|
||||||
"name": "Name",
|
"name": "Tên",
|
||||||
"smartname": "Name (Smart)",
|
"smartname": "Name (Smart)",
|
||||||
"sortMethod": "Sort Method",
|
"sortMethod": "Sort Method",
|
||||||
"welcome": "Welcome",
|
"welcome": "Chào mừng",
|
||||||
"documentationLinksNote": "The Obtainium GitHub page linked below contains links to videos, articles, discussions and other resources that will help you understand how to use the app.",
|
"documentationLinksNote": "Trang GitHub của Obtainium được liên kết bên dưới chứa các liên kết đến video, bài viết, thảo luận và các tài nguyên khác sẽ giúp bạn hiểu cách sử dụng ứng dụng.",
|
||||||
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
||||||
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
"fileDeletionError": "Không thể xóa tệp (hãy thử xóa tệp thủ công rồi thử lại): \"{}\"",
|
||||||
|
"foregroundService": "Dịch vụ nền Obtainium",
|
||||||
|
"foregroundServiceExplanation": "Sử dụng dịch vụ nền trước để kiểm tra cập nhật (đáng tin cậy hơn, tiêu tốn nhiều pin hơn)",
|
||||||
|
"fgServiceNotice": "Thông báo này là bắt buộc để kiểm tra cập nhật nền (có thể ẩn trong cài đặt hệ điều hành).",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Gỡ ứng dụng?",
|
"one": "Gỡ ứng dụng?",
|
||||||
"other": "Gỡ ứng dụng?"
|
"other": "Gỡ ứng dụng?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "下方連結的 Obtainium GitHub 頁面包含影片、文章、討論及其他資源,能幫助你瞭解如何使用這款應用程式。",
|
"documentationLinksNote": "下方連結的 Obtainium GitHub 頁面包含影片、文章、討論及其他資源,能幫助你瞭解如何使用這款應用程式。",
|
||||||
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
"batteryOptimizationNote": "Note that background downloads may work more reliably if you disable OS battery optimizations for Obtainium.",
|
||||||
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
"fileDeletionError": "Failed to delete file (try deleting it manually then try again): \"{}\"",
|
||||||
|
"foregroundService": "Obtainium foreground service",
|
||||||
|
"foregroundServiceExplanation": "Use a foreground service for update checking (more reliable, consumes more power)",
|
||||||
|
"fgServiceNotice": "This notification is required for background update checking (it can be hidden in the OS settings)",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "移除應用程式?",
|
"one": "移除應用程式?",
|
||||||
"other": "移除應用程式?"
|
"other": "移除應用程式?"
|
||||||
|
@ -330,6 +330,9 @@
|
|||||||
"documentationLinksNote": "下面链接的 Obtainium GitHub 页面包含视频、文章、讨论和其他资源的链接,可帮助您了解如何使用该应用程序。",
|
"documentationLinksNote": "下面链接的 Obtainium GitHub 页面包含视频、文章、讨论和其他资源的链接,可帮助您了解如何使用该应用程序。",
|
||||||
"batteryOptimizationNote": "请注意,如果为 Obtainium 禁用操作系统电池优化功能,后台下载可能会更稳定。",
|
"batteryOptimizationNote": "请注意,如果为 Obtainium 禁用操作系统电池优化功能,后台下载可能会更稳定。",
|
||||||
"fileDeletionError": "删除文件失败(尝试手动删除,然后再试一次):\"{}\"",
|
"fileDeletionError": "删除文件失败(尝试手动删除,然后再试一次):\"{}\"",
|
||||||
|
"foregroundService": "获取前台服务",
|
||||||
|
"foregroundServiceExplanation": "使用前台服务进行更新检查(更可靠,耗电量更大)",
|
||||||
|
"fgServiceNotice": "后台更新检查需要此通知(可在操作系统设置中隐藏)。",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "是否删除应用?",
|
"one": "是否删除应用?",
|
||||||
"other": "是否删除应用?"
|
"other": "是否删除应用?"
|
||||||
|
48
fastlane/metadata/android/de/full_description.txt
Normal file
48
fastlane/metadata/android/de/full_description.txt
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<p>Obtainium erlaubt Ihnen die Installation und Aktualisierung von Apps direkt von deren Veröffentlichungsseiten aus; Sie erhalten außerdem Benachrichtigungen, wenn neue Versionen verfügbar sind.</p>
|
||||||
|
<p>Lesen Sie das <a href="https://wiki.obtainium.imranr.dev/de/">Wiki</a></p>
|
||||||
|
<p>
|
||||||
|
<b>Derzeit unterstützte App-Quellen:</b>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Open Source – Allgemein:</p>
|
||||||
|
<ul>
|
||||||
|
<li>GitHub</li>
|
||||||
|
<li>GitLab</li>
|
||||||
|
<li>Forgejo (Codeberg)</li>
|
||||||
|
<li>F-Droid</li>
|
||||||
|
<li>Third Party F-Droid Repos</li>
|
||||||
|
<li>IzzyOnDroid</li>
|
||||||
|
<li>SourceHut</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Andere – Allgemein:</p>
|
||||||
|
<ul>
|
||||||
|
<li>APKPure</li>
|
||||||
|
<li>Aptoide</li>
|
||||||
|
<li>Uptodown</li>
|
||||||
|
<li>APKMirror (Track-Only)</li>
|
||||||
|
<li>Huawei AppGallery</li>
|
||||||
|
<li>Tencent App Store</li>
|
||||||
|
<li>CoolApk</li>
|
||||||
|
<li>vivo App Store (China)</li>
|
||||||
|
<li>Jenkins Jobs</li>
|
||||||
|
<li>RuStore</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Andere - App-Spezifisch:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Telegram App</li>
|
||||||
|
<li>Neutron Code</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><p>„HTML“ (Fallback): Jede andere URL, die eine HTML-Seite mit Links zu APK-Dateien zurückgibt.</p></li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<b>Einschränkungen:</b>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Bei einigen Quellen werden die Daten mit Hilfe von Web Scraping gesammelt und können aufgrund von Änderungen am Design der Website leicht beschädigt werden. In solchen Fällen sind zuverlässigere Methoden nicht verfügbar.
|
||||||
|
</p>
|
1
fastlane/metadata/android/de/short_description.txt
Normal file
1
fastlane/metadata/android/de/short_description.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
… Android-App-Updates direkt von der Quelle erhalten
|
1
fastlane/metadata/android/de/title.txt
Normal file
1
fastlane/metadata/android/de/title.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Obtainium
|
@ -12,8 +12,9 @@ class APKCombo extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
var match = standardUrlRegEx.firstMatch(url);
|
var match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -22,25 +23,30 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
return {
|
return {
|
||||||
"User-Agent": "curl/8.0.1",
|
"User-Agent": "curl/8.0.1",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Host": hosts[0]
|
"Host": hosts[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<MapEntry<String, String>>> getApkUrls(
|
Future<List<MapEntry<String, String>>> getApkUrls(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest('$standardUrl/download/apk', {});
|
var res = await sourceRequest('$standardUrl/download/apk', {});
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -65,7 +71,9 @@ class APKCombo extends AppSource {
|
|||||||
String verCode =
|
String verCode =
|
||||||
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
|
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
|
||||||
return MapEntry<String, String>(
|
return MapEntry<String, String>(
|
||||||
arch != null ? '$arch-$verCode.apk' : '', url ?? '');
|
arch != null ? '$arch-$verCode.apk' : '',
|
||||||
|
url ?? '',
|
||||||
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
})
|
})
|
||||||
.reduce((value, element) => [...value, ...element])
|
.reduce((value, element) => [...value, ...element])
|
||||||
@ -74,8 +82,11 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Map<String, dynamic> additionalSettings) async {
|
String apkUrl,
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var freshURLs = await getApkUrls(standardUrl, additionalSettings);
|
var freshURLs = await getApkUrls(standardUrl, additionalSettings);
|
||||||
var path2Match = Uri.parse(apkUrl).path;
|
var path2Match = Uri.parse(apkUrl).path;
|
||||||
for (var url in freshURLs) {
|
for (var url in freshURLs) {
|
||||||
@ -116,9 +127,10 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
await getApkUrls(standardUrl, additionalSettings),
|
await getApkUrls(standardUrl, additionalSettings),
|
||||||
AppNames(author, appName),
|
AppNames(author, appName),
|
||||||
releaseDate: releaseDate);
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,37 +17,44 @@ class APKMirror extends AppSource {
|
|||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
GeneratedFormTextField(
|
||||||
label: tr('filterReleaseTitlesByRegEx'),
|
'filterReleaseTitlesByRegEx',
|
||||||
required: false,
|
label: tr('filterReleaseTitlesByRegEx'),
|
||||||
additionalValidators: [
|
required: false,
|
||||||
(value) {
|
additionalValidators: [
|
||||||
return regExValidator(value);
|
(value) {
|
||||||
}
|
return regExValidator(value);
|
||||||
])
|
},
|
||||||
]
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
return {
|
return {
|
||||||
"User-Agent":
|
"User-Agent":
|
||||||
"Obtainium/${(await getInstalledInfo(obtainiumId))?.versionName ?? '1.0.0'}"
|
"Obtainium/${(await getInstalledInfo(obtainiumId))?.versionName ?? '1.0.0'}",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -68,12 +75,14 @@ class APKMirror extends AppSource {
|
|||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
String? regexFilter =
|
String? regexFilter =
|
||||||
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
||||||
?.isNotEmpty ==
|
?.isNotEmpty ==
|
||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('$standardUrl/feed/', additionalSettings);
|
'$standardUrl/feed/',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
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;
|
||||||
@ -95,11 +104,14 @@ class APKMirror extends AppSource {
|
|||||||
.split(' ')
|
.split(' ')
|
||||||
.sublist(0, 5)
|
.sublist(0, 5)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
DateTime? releaseDate =
|
DateTime? releaseDate = dateString != null
|
||||||
dateString != null ? HttpDate.parse('$dateString GMT') : null;
|
? HttpDate.parse('$dateString GMT')
|
||||||
|
: null;
|
||||||
String? version = titleString
|
String? version = titleString
|
||||||
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
?.substring(
|
||||||
RegExp(' by ').allMatches(titleString).last.start)
|
RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
||||||
|
RegExp(' by ').allMatches(titleString).last.start,
|
||||||
|
)
|
||||||
.trim();
|
.trim();
|
||||||
if (version == null || version.isEmpty) {
|
if (version == null || version.isEmpty) {
|
||||||
version = titleString;
|
version = titleString;
|
||||||
@ -107,8 +119,12 @@ class APKMirror extends AppSource {
|
|||||||
if (version == null || version.isEmpty) {
|
if (version == null || version.isEmpty) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, [], getAppNames(standardUrl),
|
return APKDetails(
|
||||||
releaseDate: releaseDate);
|
version,
|
||||||
|
[],
|
||||||
|
getAppNames(standardUrl),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
extension Unique<E, Id> on List<E> {
|
extension Unique<E, Id> on List<E> {
|
||||||
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
|
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
|
||||||
final ids = Set();
|
final ids = <dynamic>{};
|
||||||
var list = inplace ? this : List<E>.from(this);
|
var list = inplace ? this : List<E>.from(this);
|
||||||
list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
|
list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
|
||||||
return list;
|
return list;
|
||||||
@ -23,33 +23,44 @@ class APKPure extends AppSource {
|
|||||||
showReleaseDateAsVersionToggle = true;
|
showReleaseDateAsVersionToggle = true;
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('stayOneVersionBehind',
|
GeneratedFormSwitch(
|
||||||
label: tr('stayOneVersionBehind'), defaultValue: false)
|
'stayOneVersionBehind',
|
||||||
|
label: tr('stayOneVersionBehind'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('useFirstApkOfVersion',
|
GeneratedFormSwitch(
|
||||||
label: tr('useFirstApkOfVersion'), defaultValue: true)
|
'useFirstApkOfVersion',
|
||||||
]
|
label: tr('useFirstApkOfVersion'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://m.${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
'^https?://m.${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
var uri = Uri.parse(url);
|
var uri = Uri.parse(url);
|
||||||
url = 'https://${uri.host.substring(2)}${uri.path}';
|
url = 'https://${uri.host.substring(2)}${uri.path}';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExA.firstMatch(url);
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -58,15 +69,18 @@ class APKPure extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDetailsForVersion(
|
Future<APKDetails> getDetailsForVersion(
|
||||||
List<Map<String, dynamic>> versionVariants,
|
List<Map<String, dynamic>> versionVariants,
|
||||||
List<String> supportedArchs,
|
List<String> supportedArchs,
|
||||||
Map<String, dynamic> additionalSettings) async {
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var apkUrls = versionVariants
|
var apkUrls = versionVariants
|
||||||
.map((e) {
|
.map((e) {
|
||||||
String appId = e['package_name'];
|
String appId = e['package_name'];
|
||||||
@ -88,8 +102,9 @@ class APKPure extends AppSource {
|
|||||||
String downloadUri = e['asset']['url'];
|
String downloadUri = e['asset']['url'];
|
||||||
|
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
'$appId-$versionCode-$architectureString.${type.toLowerCase()}',
|
'$appId-$versionCode-$architectureString.${type.toLowerCase()}',
|
||||||
downloadUri);
|
downloadUri,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.nonNulls
|
.nonNulls
|
||||||
.toList()
|
.toList()
|
||||||
@ -114,14 +129,20 @@ class APKPure extends AppSource {
|
|||||||
apkUrls = [apkUrls.first];
|
apkUrls = [apkUrls.first];
|
||||||
}
|
}
|
||||||
|
|
||||||
return APKDetails(version, apkUrls, AppNames(author, appName),
|
return APKDetails(
|
||||||
releaseDate: releaseDate, changeLog: changeLog);
|
version,
|
||||||
|
apkUrls,
|
||||||
|
AppNames(author, appName),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
changeLog: changeLog,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
if (forAPKDownload) {
|
if (forAPKDownload) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
@ -145,19 +166,22 @@ class APKPure extends AppSource {
|
|||||||
|
|
||||||
// request versions from API
|
// request versions from API
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
"https://tapi.pureapk.com/v3/get_app_his_version?package_name=$appId&hl=en",
|
"https://tapi.pureapk.com/v3/get_app_his_version?package_name=$appId&hl=en",
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
List<Map<String, dynamic>> apks =
|
List<Map<String, dynamic>> apks = jsonDecode(
|
||||||
jsonDecode(res.body)['version_list'].cast<Map<String, dynamic>>();
|
res.body,
|
||||||
|
)['version_list'].cast<Map<String, dynamic>>();
|
||||||
|
|
||||||
// group by version
|
// group by version
|
||||||
List<List<Map<String, dynamic>>> versions = apks
|
List<List<Map<String, dynamic>>> versions = apks
|
||||||
.fold<Map<String, List<Map<String, dynamic>>>>({},
|
.fold<Map<String, List<Map<String, dynamic>>>>({}, (
|
||||||
(Map<String, List<Map<String, dynamic>>> val,
|
Map<String, List<Map<String, dynamic>>> val,
|
||||||
Map<String, dynamic> element) {
|
Map<String, dynamic> element,
|
||||||
|
) {
|
||||||
String v = element['version_name'];
|
String v = element['version_name'];
|
||||||
if (!val.containsKey(v)) {
|
if (!val.containsKey(v)) {
|
||||||
val[v] = [];
|
val[v] = [];
|
||||||
@ -179,7 +203,10 @@ class APKPure extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
return await getDetailsForVersion(
|
return await getDetailsForVersion(
|
||||||
v, supportedArchs, additionalSettings);
|
v,
|
||||||
|
supportedArchs,
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (additionalSettings['fallbackToOlderReleases'] != true ||
|
if (additionalSettings['fallbackToOlderReleases'] != true ||
|
||||||
i == versions.length - 1) {
|
i == versions.length - 1) {
|
||||||
|
@ -16,8 +16,9 @@ class Aptoide extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -26,14 +27,20 @@ class Aptoide extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return (await getAppDetailsJSON(
|
return (await getAppDetailsJSON(
|
||||||
standardUrl, additionalSettings))['package'];
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
))['package'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getAppDetailsJSON(
|
Future<Map<String, dynamic>> getAppDetailsJSON(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest(standardUrl, additionalSettings);
|
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -46,7 +53,9 @@ class Aptoide extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var res2 = await sourceRequest(
|
var res2 = await sourceRequest(
|
||||||
'https://ws2.aptoide.com/api/7/getApp/app_id/$id', additionalSettings);
|
'https://ws2.aptoide.com/api/7/getApp/app_id/$id',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res2.statusCode != 200) {
|
if (res2.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@ -76,7 +85,10 @@ class Aptoide extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
version,
|
||||||
releaseDate: relDate);
|
getApkUrlsFromUrls([apkUrl]),
|
||||||
|
AppNames(author, appName),
|
||||||
|
releaseDate: relDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,9 @@ class Codeberg extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -36,8 +37,9 @@ class Codeberg extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings, (
|
||||||
(bool useTagUrl) async {
|
bool useTagUrl,
|
||||||
|
) async {
|
||||||
return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
@ -49,12 +51,15 @@ class Codeberg extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
return gh.searchCommon(
|
return gh.searchCommon(
|
||||||
query,
|
query,
|
||||||
'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
||||||
'data',
|
'data',
|
||||||
querySettings: querySettings);
|
querySettings: querySettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,9 @@ class CoolApk extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
r'^https?://(www\.)?coolapk\.com/apk/[^/]+',
|
r'^https?://(www\.)?coolapk\.com/apk/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
var match = standardUrlRegEx.firstMatch(url);
|
var match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -30,17 +31,19 @@ class CoolApk extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
String appId = Uri.parse(standardUrl).pathSegments.last;
|
String appId = Uri.parse(standardUrl).pathSegments.last;
|
||||||
return appId;
|
return appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String appId = (await tryInferringAppId(standardUrl))!;
|
String appId = (await tryInferringAppId(standardUrl))!;
|
||||||
String apiUrl = 'https://api2.coolapk.com';
|
String apiUrl = 'https://api2.coolapk.com';
|
||||||
|
|
||||||
@ -65,13 +68,19 @@ class CoolApk extends AppSource {
|
|||||||
String changelog = detail['changelog']?.toString() ?? '';
|
String changelog = detail['changelog']?.toString() ?? '';
|
||||||
int? releaseDate = detail['lastupdate'] != null
|
int? releaseDate = detail['lastupdate'] != null
|
||||||
? (detail['lastupdate'] is int
|
? (detail['lastupdate'] is int
|
||||||
? detail['lastupdate'] * 1000
|
? detail['lastupdate'] * 1000
|
||||||
: int.parse(detail['lastupdate'].toString()) * 1000)
|
: int.parse(detail['lastupdate'].toString()) * 1000)
|
||||||
: null;
|
: null;
|
||||||
String aid = detail['id'].toString();
|
String aid = detail['id'].toString();
|
||||||
|
|
||||||
// get apk url
|
// get apk url
|
||||||
String apkUrl = await _getLatestApkUrl(apiUrl, appId, aid, version, headers);
|
String apkUrl = await _getLatestApkUrl(
|
||||||
|
apiUrl,
|
||||||
|
appId,
|
||||||
|
aid,
|
||||||
|
version,
|
||||||
|
headers,
|
||||||
|
);
|
||||||
if (apkUrl.isEmpty) {
|
if (apkUrl.isEmpty) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
@ -89,8 +98,13 @@ class CoolApk extends AppSource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getLatestApkUrl(String apiUrl, String appId, String aid,
|
Future<String> _getLatestApkUrl(
|
||||||
String version, Map<String, String>? headers) async {
|
String apiUrl,
|
||||||
|
String appId,
|
||||||
|
String aid,
|
||||||
|
String version,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
) async {
|
||||||
String url = '$apiUrl/v6/apk/download?pn=$appId&aid=$aid';
|
String url = '$apiUrl/v6/apk/download?pn=$appId&aid=$aid';
|
||||||
var res = await sourceRequest(url, {}, followRedirects: false);
|
var res = await sourceRequest(url, {}, followRedirects: false);
|
||||||
if (res.statusCode >= 300 && res.statusCode < 400) {
|
if (res.statusCode >= 300 && res.statusCode < 400) {
|
||||||
@ -102,13 +116,14 @@ class CoolApk extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
var tokenPair = _getToken();
|
var tokenPair = _getToken();
|
||||||
// CoolAPK header
|
// CoolAPK header
|
||||||
return {
|
return {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/12.4.2-2208241-universal',
|
'Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/12.4.2-2208241-universal',
|
||||||
'X-App-Id': 'com.coolapk.market',
|
'X-App-Id': 'com.coolapk.market',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'X-Sdk-Int': '30',
|
'X-Sdk-Int': '30',
|
||||||
@ -128,14 +143,15 @@ class CoolApk extends AppSource {
|
|||||||
Map<String, String> _getToken() {
|
Map<String, String> _getToken() {
|
||||||
final rand = Random();
|
final rand = Random();
|
||||||
|
|
||||||
String randHexString(int n) =>
|
String randHexString(int n) => List.generate(
|
||||||
List.generate(n, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'))
|
n,
|
||||||
.join()
|
(_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
||||||
.toUpperCase();
|
).join().toUpperCase();
|
||||||
|
|
||||||
String randMacAddress() =>
|
String randMacAddress() => List.generate(
|
||||||
List.generate(6, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'))
|
6,
|
||||||
.join(':');
|
(_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
||||||
|
).join(':');
|
||||||
|
|
||||||
// 加密算法来自 https://github.com/XiaoMengXinX/FuckCoolapkTokenV2、https://github.com/Coolapk-UWP/Coolapk-UWP
|
// 加密算法来自 https://github.com/XiaoMengXinX/FuckCoolapkTokenV2、https://github.com/Coolapk-UWP/Coolapk-UWP
|
||||||
// device
|
// device
|
||||||
@ -147,11 +163,13 @@ class CoolApk extends AppSource {
|
|||||||
const buildNumber = 'SQ1D.220105.007';
|
const buildNumber = 'SQ1D.220105.007';
|
||||||
|
|
||||||
// generate deviceCode
|
// generate deviceCode
|
||||||
String deviceCode =
|
String deviceCode = base64.encode(
|
||||||
base64.encode('$aid; ; ; $mac; $manufactor; $brand; $model; $buildNumber'.codeUnits);
|
'$aid; ; ; $mac; $manufactor; $brand; $model; $buildNumber'.codeUnits,
|
||||||
|
);
|
||||||
|
|
||||||
// generate timestamp
|
// generate timestamp
|
||||||
String timeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
|
String timeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000)
|
||||||
|
.toString();
|
||||||
String base64TimeStamp = base64.encode(timeStamp.codeUnits);
|
String base64TimeStamp = base64.encode(timeStamp.codeUnits);
|
||||||
String md5TimeStamp = md5.convert(timeStamp.codeUnits).toString();
|
String md5TimeStamp = md5.convert(timeStamp.codeUnits).toString();
|
||||||
String md5DeviceCode = md5.convert(deviceCode.codeUnits).toString();
|
String md5DeviceCode = md5.convert(deviceCode.codeUnits).toString();
|
||||||
@ -164,7 +182,8 @@ class CoolApk extends AppSource {
|
|||||||
String md5Token = md5.convert(token.codeUnits).toString();
|
String md5Token = md5.convert(token.codeUnits).toString();
|
||||||
|
|
||||||
// generate salt and hash
|
// generate salt and hash
|
||||||
String bcryptSalt = '\$2a\$10\$${base64TimeStamp.substring(0, 14)}/${md5Token.substring(0, 6)}u';
|
String bcryptSalt =
|
||||||
|
'\$2a\$10\$${base64TimeStamp.substring(0, 14)}/${md5Token.substring(0, 6)}u';
|
||||||
String bcryptResult = BCrypt.hashpw(md5Base64Token, bcryptSalt);
|
String bcryptResult = BCrypt.hashpw(md5Base64Token, bcryptSalt);
|
||||||
String reBcryptResult = bcryptResult.replaceRange(0, 3, '\$2y');
|
String reBcryptResult = bcryptResult.replaceRange(0, 3, '\$2y');
|
||||||
String finalToken = 'v2${base64.encode(reBcryptResult.codeUnits)}';
|
String finalToken = 'v2${base64.encode(reBcryptResult.codeUnits)}';
|
||||||
|
@ -11,20 +11,23 @@ class DirectAPKLink extends AppSource {
|
|||||||
name = tr('directAPKLink');
|
name = tr('directAPKLink');
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
...html.additionalSourceAppSpecificSettingFormItems
|
...html.additionalSourceAppSpecificSettingFormItems
|
||||||
.where((element) => element
|
.where(
|
||||||
.where((element) => element.key == 'requestHeader')
|
(element) => element
|
||||||
.isNotEmpty)
|
.where((element) => element.key == 'requestHeader')
|
||||||
.toList(),
|
.isNotEmpty,
|
||||||
|
)
|
||||||
|
,
|
||||||
[
|
[
|
||||||
GeneratedFormDropdown(
|
GeneratedFormDropdown(
|
||||||
'defaultPseudoVersioningMethod',
|
'defaultPseudoVersioningMethod',
|
||||||
[
|
[
|
||||||
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
||||||
MapEntry('ETag', 'ETag')
|
MapEntry('ETag', 'ETag'),
|
||||||
],
|
],
|
||||||
label: tr('defaultPseudoVersioningMethod'),
|
label: tr('defaultPseudoVersioningMethod'),
|
||||||
defaultValue: 'partialAPKHash')
|
defaultValue: 'partialAPKHash',
|
||||||
]
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
excludeCommonSettingKeys = [
|
excludeCommonSettingKeys = [
|
||||||
'versionExtractionRegEx',
|
'versionExtractionRegEx',
|
||||||
@ -32,7 +35,7 @@ class DirectAPKLink extends AppSource {
|
|||||||
'versionDetection',
|
'versionDetection',
|
||||||
'useVersionCodeAsOSVersion',
|
'useVersionCodeAsOSVersion',
|
||||||
'apkFilterRegEx',
|
'apkFilterRegEx',
|
||||||
'autoApkFilterByArch'
|
'autoApkFilterByArch',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,10 +54,13 @@ class DirectAPKLink extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) {
|
bool forAPKDownload = false,
|
||||||
return html.getRequestHeaders(additionalSettings,
|
}) {
|
||||||
forAPKDownload: forAPKDownload);
|
return html.getRequestHeaders(
|
||||||
|
additionalSettings,
|
||||||
|
forAPKDownload: forAPKDownload,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -62,8 +68,9 @@ class DirectAPKLink extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var additionalSettingsNew =
|
var additionalSettingsNew = getDefaultValuesFromFormItems(
|
||||||
getDefaultValuesFromFormItems(html.combinedAppSpecificSettingFormItems);
|
html.combinedAppSpecificSettingFormItems,
|
||||||
|
);
|
||||||
for (var s in additionalSettings.keys) {
|
for (var s in additionalSettings.keys) {
|
||||||
if (additionalSettingsNew.containsKey(s)) {
|
if (additionalSettingsNew.containsKey(s)) {
|
||||||
additionalSettingsNew[s] = additionalSettings[s];
|
additionalSettingsNew[s] = additionalSettings[s];
|
||||||
|
@ -17,22 +17,28 @@ class FDroid extends AppSource {
|
|||||||
canSearch = true;
|
canSearch = true;
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterVersionsByRegEx',
|
GeneratedFormTextField(
|
||||||
label: tr('filterVersionsByRegEx'),
|
'filterVersionsByRegEx',
|
||||||
required: false,
|
label: tr('filterVersionsByRegEx'),
|
||||||
additionalValidators: [
|
required: false,
|
||||||
(value) {
|
additionalValidators: [
|
||||||
return regExValidator(value);
|
(value) {
|
||||||
}
|
return regExValidator(value);
|
||||||
])
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('trySelectingSuggestedVersionCode',
|
GeneratedFormSwitch(
|
||||||
label: tr('trySelectingSuggestedVersionCode'))
|
'trySelectingSuggestedVersionCode',
|
||||||
|
label: tr('trySelectingSuggestedVersionCode'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('autoSelectHighestVersionCode',
|
GeneratedFormSwitch(
|
||||||
label: tr('autoSelectHighestVersionCode'))
|
'autoSelectHighestVersionCode',
|
||||||
|
label: tr('autoSelectHighestVersionCode'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -40,16 +46,18 @@ class FDroid extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url =
|
url =
|
||||||
'https://${Uri.parse(match.group(0)!).host}/packages/${Uri.parse(url).pathSegments.where((s) => s.trim().isNotEmpty).last}';
|
'https://${Uri.parse(match.group(0)!).host}/packages/${Uri.parse(url).pathSegments.where((s) => s.trim().isNotEmpty).last}';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExA.firstMatch(url);
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -58,8 +66,10 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,22 +81,28 @@ class FDroid extends AppSource {
|
|||||||
String? appId = await tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
String host = Uri.parse(standardUrl).host;
|
String host = Uri.parse(standardUrl).host;
|
||||||
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest(
|
await sourceRequest(
|
||||||
'https://$host/api/v1/packages/$appId', additionalSettings),
|
'https://$host/api/v1/packages/$appId',
|
||||||
'https://$host/repo/$appId',
|
additionalSettings,
|
||||||
standardUrl,
|
),
|
||||||
name,
|
'https://$host/repo/$appId',
|
||||||
additionalSettings: additionalSettings);
|
standardUrl,
|
||||||
|
name,
|
||||||
|
additionalSettings: additionalSettings,
|
||||||
|
);
|
||||||
if (!hostChanged) {
|
if (!hostChanged) {
|
||||||
try {
|
try {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
|
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
var lines = res.body.split('\n');
|
var lines = res.body.split('\n');
|
||||||
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
|
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
|
||||||
if (authorLines.isNotEmpty) {
|
if (authorLines.isNotEmpty) {
|
||||||
details.names.author =
|
details.names.author = authorLines.first
|
||||||
authorLines.first.split(': ').sublist(1).join(': ');
|
.split(': ')
|
||||||
|
.sublist(1)
|
||||||
|
.join(': ');
|
||||||
}
|
}
|
||||||
var changelogUrls = lines
|
var changelogUrls = lines
|
||||||
.where((l) => l.startsWith('Changelog: '))
|
.where((l) => l.startsWith('Changelog: '))
|
||||||
@ -110,9 +126,9 @@ class FDroid extends AppSource {
|
|||||||
if ((isGitHub || isGitLab) &&
|
if ((isGitHub || isGitLab) &&
|
||||||
(details.changeLog?.indexOf('/blob/') ?? -1) >= 0) {
|
(details.changeLog?.indexOf('/blob/') ?? -1) >= 0) {
|
||||||
details.changeLog = (await sourceRequest(
|
details.changeLog = (await sourceRequest(
|
||||||
details.changeLog!.replaceFirst('/blob/', '/raw/'),
|
details.changeLog!.replaceFirst('/blob/', '/raw/'),
|
||||||
additionalSettings))
|
additionalSettings,
|
||||||
.body;
|
)).body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -126,10 +142,14 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
|
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}',
|
||||||
|
{},
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||||
@ -145,7 +165,7 @@ class FDroid extends AppSource {
|
|||||||
urlsWithDescriptions[url] = [
|
urlsWithDescriptions[url] = [
|
||||||
e.querySelector('.package-name')?.text.trim() ?? '',
|
e.querySelector('.package-name')?.text.trim() ?? '',
|
||||||
e.querySelector('.package-summary')?.text.trim() ??
|
e.querySelector('.package-summary')?.text.trim() ??
|
||||||
tr('noDescription')
|
tr('noDescription'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -156,29 +176,36 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
Response res, String apkUrlPrefix, String standardUrl, String sourceName,
|
Response res,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
String apkUrlPrefix,
|
||||||
|
String standardUrl,
|
||||||
|
String sourceName, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) {
|
||||||
var autoSelectHighestVersionCode =
|
var autoSelectHighestVersionCode =
|
||||||
additionalSettings['autoSelectHighestVersionCode'] == true;
|
additionalSettings['autoSelectHighestVersionCode'] == true;
|
||||||
var trySelectingSuggestedVersionCode =
|
var trySelectingSuggestedVersionCode =
|
||||||
additionalSettings['trySelectingSuggestedVersionCode'] == true;
|
additionalSettings['trySelectingSuggestedVersionCode'] == true;
|
||||||
var filterVersionsByRegEx =
|
var filterVersionsByRegEx =
|
||||||
(additionalSettings['filterVersionsByRegEx'] as String?)?.isNotEmpty ==
|
(additionalSettings['filterVersionsByRegEx'] as String?)?.isNotEmpty ==
|
||||||
true
|
true
|
||||||
? additionalSettings['filterVersionsByRegEx']
|
? additionalSettings['filterVersionsByRegEx']
|
||||||
: null;
|
: null;
|
||||||
var apkFilterRegEx =
|
var apkFilterRegEx =
|
||||||
(additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true
|
(additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true
|
||||||
? additionalSettings['apkFilterRegEx']
|
? additionalSettings['apkFilterRegEx']
|
||||||
: null;
|
: null;
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var response = jsonDecode(res.body);
|
var response = jsonDecode(res.body);
|
||||||
List<dynamic> releases = response['packages'] ?? [];
|
List<dynamic> releases = response['packages'] ?? [];
|
||||||
if (apkFilterRegEx != null) {
|
if (apkFilterRegEx != null) {
|
||||||
releases = releases.where((rel) {
|
releases = releases.where((rel) {
|
||||||
String apk = '${apkUrlPrefix}_${rel['versionCode']}.apk';
|
String apk = '${apkUrlPrefix}_${rel['versionCode']}.apk';
|
||||||
return filterApks([MapEntry(apk, apk)], apkFilterRegEx, false)
|
return filterApks(
|
||||||
.isNotEmpty;
|
[MapEntry(apk, apk)],
|
||||||
|
apkFilterRegEx,
|
||||||
|
false,
|
||||||
|
).isNotEmpty;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
if (releases.isEmpty) {
|
if (releases.isEmpty) {
|
||||||
@ -191,8 +218,10 @@ class FDroid extends AppSource {
|
|||||||
if (trySelectingSuggestedVersionCode &&
|
if (trySelectingSuggestedVersionCode &&
|
||||||
response['suggestedVersionCode'] != null &&
|
response['suggestedVersionCode'] != null &&
|
||||||
filterVersionsByRegEx == null) {
|
filterVersionsByRegEx == null) {
|
||||||
var suggestedReleases = releases.where((element) =>
|
var suggestedReleases = releases.where(
|
||||||
element['versionCode'] == response['suggestedVersionCode']);
|
(element) =>
|
||||||
|
element['versionCode'] == response['suggestedVersionCode'],
|
||||||
|
);
|
||||||
if (suggestedReleases.isNotEmpty) {
|
if (suggestedReleases.isNotEmpty) {
|
||||||
releaseChoices = suggestedReleases;
|
releaseChoices = suggestedReleases;
|
||||||
version = suggestedReleases.first['versionName'];
|
version = suggestedReleases.first['versionName'];
|
||||||
@ -203,8 +232,9 @@ class FDroid extends AppSource {
|
|||||||
version = null;
|
version = null;
|
||||||
releaseChoices = [];
|
releaseChoices = [];
|
||||||
for (var i = 0; i < releases.length; i++) {
|
for (var i = 0; i < releases.length; i++) {
|
||||||
if (RegExp(filterVersionsByRegEx!)
|
if (RegExp(
|
||||||
.hasMatch(releases[i]['versionName'])) {
|
filterVersionsByRegEx!,
|
||||||
|
).hasMatch(releases[i]['versionName'])) {
|
||||||
version = releases[i]['versionName'];
|
version = releases[i]['versionName'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,8 +249,9 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
// If a suggested release was not already picked, pick all those with the selected version
|
// If a suggested release was not already picked, pick all those with the selected version
|
||||||
if (releaseChoices.isEmpty) {
|
if (releaseChoices.isEmpty) {
|
||||||
releaseChoices =
|
releaseChoices = releases.where(
|
||||||
releases.where((element) => element['versionName'] == version);
|
(element) => element['versionName'] == version,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// For the remaining releases, use the toggles to auto-select one if possible
|
// For the remaining releases, use the toggles to auto-select one if possible
|
||||||
if (releaseChoices.length > 1) {
|
if (releaseChoices.length > 1) {
|
||||||
@ -228,8 +259,10 @@ class FDroid extends AppSource {
|
|||||||
releaseChoices = [releaseChoices.first];
|
releaseChoices = [releaseChoices.first];
|
||||||
} else if (trySelectingSuggestedVersionCode &&
|
} else if (trySelectingSuggestedVersionCode &&
|
||||||
response['suggestedVersionCode'] != null) {
|
response['suggestedVersionCode'] != null) {
|
||||||
var suggestedReleases = releaseChoices.where((element) =>
|
var suggestedReleases = releaseChoices.where(
|
||||||
element['versionCode'] == response['suggestedVersionCode']);
|
(element) =>
|
||||||
|
element['versionCode'] == response['suggestedVersionCode'],
|
||||||
|
);
|
||||||
if (suggestedReleases.isNotEmpty) {
|
if (suggestedReleases.isNotEmpty) {
|
||||||
releaseChoices = suggestedReleases;
|
releaseChoices = suggestedReleases;
|
||||||
}
|
}
|
||||||
@ -241,8 +274,11 @@ class FDroid extends AppSource {
|
|||||||
List<String> apkUrls = releaseChoices
|
List<String> apkUrls = releaseChoices
|
||||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
|
return APKDetails(
|
||||||
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
|
version,
|
||||||
|
getApkUrlsFromUrls(apkUrls.toSet().toList()),
|
||||||
|
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,20 @@ class FDroidRepo extends AppSource {
|
|||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('appIdOrName',
|
GeneratedFormTextField(
|
||||||
label: tr('appIdOrName'),
|
'appIdOrName',
|
||||||
hint: tr('reposHaveMultipleApps'),
|
label: tr('appIdOrName'),
|
||||||
required: true)
|
hint: tr('reposHaveMultipleApps'),
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('pickHighestVersionCode',
|
GeneratedFormSwitch(
|
||||||
label: tr('pickHighestVersionCode'), defaultValue: false)
|
'pickHighestVersionCode',
|
||||||
]
|
label: tr('pickHighestVersionCode'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,8 +59,10 @@ class FDroidRepo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
String? url = querySettings['url'];
|
String? url = querySettings['url'];
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
@ -73,11 +80,8 @@ class FDroidRepo extends AppSource {
|
|||||||
appId.contains(query) ||
|
appId.contains(query) ||
|
||||||
appName.contains(query) ||
|
appName.contains(query) ||
|
||||||
appDesc.contains(query)) {
|
appDesc.contains(query)) {
|
||||||
results[
|
results['${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] =
|
||||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
|
[appName, appDesc];
|
||||||
appName,
|
|
||||||
appDesc
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
@ -90,21 +94,21 @@ class FDroidRepo extends AppSource {
|
|||||||
void runOnAddAppInputChange(String userInput) {
|
void runOnAddAppInputChange(String userInput) {
|
||||||
additionalSourceAppSpecificSettingFormItems =
|
additionalSourceAppSpecificSettingFormItems =
|
||||||
additionalSourceAppSpecificSettingFormItems.map((row) {
|
additionalSourceAppSpecificSettingFormItems.map((row) {
|
||||||
row = row.map((item) {
|
row = row.map((item) {
|
||||||
if (item.key == 'appIdOrName') {
|
if (item.key == 'appIdOrName') {
|
||||||
try {
|
try {
|
||||||
var appId = Uri.parse(userInput).queryParameters['appId'];
|
var appId = Uri.parse(userInput).queryParameters['appId'];
|
||||||
if (appId != null && item is GeneratedFormTextField) {
|
if (appId != null && item is GeneratedFormTextField) {
|
||||||
item.required = false;
|
item.required = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return item;
|
||||||
//
|
}).toList();
|
||||||
}
|
return row;
|
||||||
}
|
}).toList();
|
||||||
return item;
|
|
||||||
}).toList();
|
|
||||||
return row;
|
|
||||||
}).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -119,8 +123,11 @@ class FDroidRepo extends AppSource {
|
|||||||
if (appId != null) {
|
if (appId != null) {
|
||||||
app.url = uri
|
app.url = uri
|
||||||
.replace(
|
.replace(
|
||||||
queryParameters: Map.fromEntries(
|
queryParameters: Map.fromEntries([
|
||||||
[...uri.queryParameters.entries, MapEntry('appId', appId)]))
|
...uri.queryParameters.entries,
|
||||||
|
MapEntry('appId', appId),
|
||||||
|
]),
|
||||||
|
)
|
||||||
.toString();
|
.toString();
|
||||||
app.additionalSettings['appIdOrName'] = appId;
|
app.additionalSettings['appIdOrName'] = appId;
|
||||||
app.id = appId;
|
app.id = appId;
|
||||||
@ -133,8 +140,9 @@ class FDroidRepo extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'$url${url.endsWith('/index.xml') ? '' : '/index.xml'}',
|
'$url${url.endsWith('/index.xml') ? '' : '/index.xml'}',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var base = url.endsWith('/index.xml')
|
var base = url.endsWith('/index.xml')
|
||||||
? url.split('/').reversed.toList().sublist(1).reversed.join('/')
|
? url.split('/').reversed.toList().sublist(1).reversed.join('/')
|
||||||
@ -142,7 +150,9 @@ class FDroidRepo extends AppSource {
|
|||||||
res = await sourceRequest('$base/repo/index.xml', additionalSettings);
|
res = await sourceRequest('$base/repo/index.xml', additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
res = await sourceRequest(
|
res = await sourceRequest(
|
||||||
'$base/fdroid/repo/index.xml', additionalSettings);
|
'$base/fdroid/repo/index.xml',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
@ -164,8 +174,10 @@ class FDroidRepo extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
additionalSettings['appIdOrName'] = appIdOrName;
|
additionalSettings['appIdOrName'] = appIdOrName;
|
||||||
var res =
|
var res = await sourceRequestWithURLVariants(
|
||||||
await sourceRequestWithURLVariants(standardUrl, additionalSettings);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
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) {
|
||||||
@ -202,24 +214,32 @@ class FDroidRepo extends AppSource {
|
|||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
var latestVersionReleases = releases
|
var latestVersionReleases = releases
|
||||||
.where((element) =>
|
.where(
|
||||||
element.querySelector('version')?.innerHtml == latestVersion &&
|
(element) =>
|
||||||
element.querySelector('apkname') != null)
|
element.querySelector('version')?.innerHtml == latestVersion &&
|
||||||
|
element.querySelector('apkname') != null,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (latestVersionReleases.length > 1 && pickHighestVersionCode) {
|
if (latestVersionReleases.length > 1 && pickHighestVersionCode) {
|
||||||
latestVersionReleases.sort((e1, e2) {
|
latestVersionReleases.sort((e1, e2) {
|
||||||
return int.parse(e2.querySelector('versioncode')!.innerHtml)
|
return int.parse(
|
||||||
.compareTo(int.parse(e1.querySelector('versioncode')!.innerHtml));
|
e2.querySelector('versioncode')!.innerHtml,
|
||||||
|
).compareTo(int.parse(e1.querySelector('versioncode')!.innerHtml));
|
||||||
});
|
});
|
||||||
latestVersionReleases = [latestVersionReleases[0]];
|
latestVersionReleases = [latestVersionReleases[0]];
|
||||||
}
|
}
|
||||||
List<String> apkUrls = latestVersionReleases
|
List<String> apkUrls = latestVersionReleases
|
||||||
.map((e) =>
|
.map(
|
||||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
|
(e) =>
|
||||||
|
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}',
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
return APKDetails(
|
||||||
AppNames(authorName, appName),
|
latestVersion,
|
||||||
releaseDate: releaseDate);
|
getApkUrlsFromUrls(apkUrls),
|
||||||
|
AppNames(authorName, appName),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -19,145 +19,185 @@ class GitHub extends AppSource {
|
|||||||
showReleaseDateAsVersionToggle = true;
|
showReleaseDateAsVersionToggle = true;
|
||||||
|
|
||||||
sourceConfigSettingFormItems = [
|
sourceConfigSettingFormItems = [
|
||||||
GeneratedFormTextField('github-creds',
|
GeneratedFormTextField(
|
||||||
label: tr('githubPATLabel'),
|
'github-creds',
|
||||||
password: true,
|
label: tr('githubPATLabel'),
|
||||||
required: false,
|
password: true,
|
||||||
belowWidgets: [
|
required: false,
|
||||||
const SizedBox(
|
belowWidgets: [
|
||||||
height: 4,
|
const SizedBox(height: 4),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(
|
||||||
|
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
tr('about'),
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
),
|
||||||
onTap: () {
|
const SizedBox(height: 4),
|
||||||
launchUrlString(
|
],
|
||||||
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-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 = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('includePrereleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('includePrereleases'), defaultValue: false)
|
'includePrereleases',
|
||||||
|
label: tr('includePrereleases'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
GeneratedFormTextField(
|
||||||
label: tr('filterReleaseTitlesByRegEx'),
|
'filterReleaseTitlesByRegEx',
|
||||||
required: false,
|
label: tr('filterReleaseTitlesByRegEx'),
|
||||||
additionalValidators: [
|
required: false,
|
||||||
(value) {
|
additionalValidators: [
|
||||||
return regExValidator(value);
|
(value) {
|
||||||
}
|
return regExValidator(value);
|
||||||
])
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterReleaseNotesByRegEx',
|
GeneratedFormTextField(
|
||||||
label: tr('filterReleaseNotesByRegEx'),
|
'filterReleaseNotesByRegEx',
|
||||||
required: false,
|
label: tr('filterReleaseNotesByRegEx'),
|
||||||
additionalValidators: [
|
required: false,
|
||||||
(value) {
|
additionalValidators: [
|
||||||
return regExValidator(value);
|
(value) {
|
||||||
}
|
return regExValidator(value);
|
||||||
])
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[GeneratedFormSwitch('verifyLatestTag', label: tr('verifyLatestTag'))],
|
[GeneratedFormSwitch('verifyLatestTag', label: tr('verifyLatestTag'))],
|
||||||
[
|
[
|
||||||
GeneratedFormDropdown(
|
GeneratedFormDropdown(
|
||||||
'sortMethodChoice',
|
'sortMethodChoice',
|
||||||
[
|
[
|
||||||
MapEntry('date', tr('releaseDate')),
|
MapEntry('date', tr('releaseDate')),
|
||||||
MapEntry('smartname', tr('smartname')),
|
MapEntry('smartname', tr('smartname')),
|
||||||
MapEntry('none', tr('none')),
|
MapEntry('none', tr('none')),
|
||||||
MapEntry('smartname-datefallback',
|
MapEntry(
|
||||||
'${tr('smartname')} x ${tr('releaseDate')}'),
|
'smartname-datefallback',
|
||||||
MapEntry('name', tr('name')),
|
'${tr('smartname')} x ${tr('releaseDate')}',
|
||||||
],
|
),
|
||||||
label: tr('sortMethod'),
|
MapEntry('name', tr('name')),
|
||||||
defaultValue: 'date')
|
],
|
||||||
|
label: tr('sortMethod'),
|
||||||
|
defaultValue: 'date',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('useLatestAssetDateAsReleaseDate',
|
GeneratedFormSwitch(
|
||||||
label: tr('useLatestAssetDateAsReleaseDate'), defaultValue: false)
|
'useLatestAssetDateAsReleaseDate',
|
||||||
|
label: tr('useLatestAssetDateAsReleaseDate'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('releaseTitleAsVersion',
|
GeneratedFormSwitch(
|
||||||
label: tr('releaseTitleAsVersion'), defaultValue: false)
|
'releaseTitleAsVersion',
|
||||||
]
|
label: tr('releaseTitleAsVersion'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
canSearch = true;
|
canSearch = true;
|
||||||
searchQuerySettingFormItems = [
|
searchQuerySettingFormItems = [
|
||||||
GeneratedFormTextField('minStarCount',
|
GeneratedFormTextField(
|
||||||
label: tr('minStarCount'),
|
'minStarCount',
|
||||||
defaultValue: '0',
|
label: tr('minStarCount'),
|
||||||
additionalValidators: [
|
defaultValue: '0',
|
||||||
(value) {
|
additionalValidators: [
|
||||||
try {
|
(value) {
|
||||||
int.parse(value ?? '0');
|
try {
|
||||||
} catch (e) {
|
int.parse(value ?? '0');
|
||||||
return tr('invalidInput');
|
} catch (e) {
|
||||||
}
|
return tr('invalidInput');
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
])
|
return null;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
const possibleBuildGradleLocations = [
|
const possibleBuildGradleLocations = [
|
||||||
'/app/build.gradle',
|
'/app/build.gradle',
|
||||||
'android/app/build.gradle',
|
'android/app/build.gradle',
|
||||||
'src/app/build.gradle'
|
'src/app/build.gradle',
|
||||||
];
|
];
|
||||||
for (var path in possibleBuildGradleLocations) {
|
for (var path in possibleBuildGradleLocations) {
|
||||||
try {
|
try {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
|
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
try {
|
try {
|
||||||
var body = jsonDecode(res.body);
|
var body = jsonDecode(res.body);
|
||||||
var trimmedLines = utf8
|
var trimmedLines = utf8
|
||||||
.decode(base64
|
.decode(
|
||||||
.decode(body['content'].toString().split('\n').join('')))
|
base64.decode(
|
||||||
|
body['content'].toString().split('\n').join(''),
|
||||||
|
),
|
||||||
|
)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((e) => e.trim());
|
.map((e) => e.trim());
|
||||||
var appIds = trimmedLines.where((l) =>
|
var appIds = trimmedLines.where(
|
||||||
l.startsWith('applicationId "') ||
|
(l) =>
|
||||||
l.startsWith('applicationId \''));
|
l.startsWith('applicationId "') ||
|
||||||
appIds = appIds.map((appId) => appId
|
l.startsWith('applicationId \''),
|
||||||
.split(appId.startsWith('applicationId "') ? '"' : '\'')[1]);
|
);
|
||||||
appIds = appIds.map((appId) {
|
appIds = appIds.map(
|
||||||
if (appId.startsWith('\${') && appId.endsWith('}')) {
|
(appId) => appId.split(
|
||||||
appId = trimmedLines
|
appId.startsWith('applicationId "') ? '"' : '\'',
|
||||||
.where((l) => l.startsWith(
|
)[1],
|
||||||
'def ${appId.substring(2, appId.length - 1)}'))
|
);
|
||||||
.first;
|
appIds = appIds
|
||||||
appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
|
.map((appId) {
|
||||||
}
|
if (appId.startsWith('\${') && appId.endsWith('}')) {
|
||||||
return appId;
|
appId = trimmedLines
|
||||||
}).where((appId) => appId.isNotEmpty);
|
.where(
|
||||||
|
(l) => l.startsWith(
|
||||||
|
'def ${appId.substring(2, appId.length - 1)}',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.first;
|
||||||
|
appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
|
||||||
|
}
|
||||||
|
return appId;
|
||||||
|
})
|
||||||
|
.where((appId) => appId.isNotEmpty);
|
||||||
if (appIds.length == 1) {
|
if (appIds.length == 1) {
|
||||||
return appIds.first;
|
return appIds.first;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
LogsProvider().add(
|
LogsProvider().add(
|
||||||
'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}');
|
'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -170,8 +210,9 @@ class GitHub extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -181,8 +222,9 @@ class GitHub extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
var token = await getTokenIfAny(additionalSettings);
|
var token = await getTokenIfAny(additionalSettings);
|
||||||
var headers = <String, String>{};
|
var headers = <String, String>{};
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
@ -201,14 +243,17 @@ class GitHub extends AppSource {
|
|||||||
Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
|
Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
|
||||||
SettingsProvider settingsProvider = SettingsProvider();
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
await settingsProvider.initializeSettings();
|
await settingsProvider.initializeSettings();
|
||||||
var sourceConfig =
|
var sourceConfig = await getSourceConfigValues(
|
||||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
additionalSettings,
|
||||||
|
settingsProvider,
|
||||||
|
);
|
||||||
String? creds = sourceConfig['github-creds'];
|
String? creds = sourceConfig['github-creds'];
|
||||||
if (creds != null) {
|
if (creds != null) {
|
||||||
var userNameEndIndex = creds.indexOf(':');
|
var userNameEndIndex = creds.indexOf(':');
|
||||||
if (userNameEndIndex > 0) {
|
if (userNameEndIndex > 0) {
|
||||||
creds = creds.substring(
|
creds = creds.substring(
|
||||||
userNameEndIndex + 1); // For old username-included token inputs
|
userNameEndIndex + 1,
|
||||||
|
); // For old username-included token inputs
|
||||||
}
|
}
|
||||||
return creds;
|
return creds;
|
||||||
} else {
|
} else {
|
||||||
@ -228,31 +273,36 @@ class GitHub extends AppSource {
|
|||||||
'https://api.${hosts[0]}';
|
'https://api.${hosts[0]}';
|
||||||
|
|
||||||
Future<String> convertStandardUrlToAPIUrl(
|
Future<String> convertStandardUrlToAPIUrl(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async =>
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async =>
|
||||||
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}';
|
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'$standardUrl/releases';
|
'$standardUrl/releases';
|
||||||
|
|
||||||
Future<APKDetails> getLatestAPKDetailsCommon(String requestUrl,
|
Future<APKDetails> getLatestAPKDetailsCommon(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings,
|
String requestUrl,
|
||||||
{Function(Response)? onHttpErrorCode}) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings, {
|
||||||
|
Function(Response)? onHttpErrorCode,
|
||||||
|
}) async {
|
||||||
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
String? regexFilter =
|
String? regexFilter =
|
||||||
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
||||||
?.isNotEmpty ==
|
?.isNotEmpty ==
|
||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
String? regexNotesFilter =
|
String? regexNotesFilter =
|
||||||
(additionalSettings['filterReleaseNotesByRegEx'] as String?)
|
(additionalSettings['filterReleaseNotesByRegEx'] as String?)
|
||||||
?.isNotEmpty ==
|
?.isNotEmpty ==
|
||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseNotesByRegEx']
|
? additionalSettings['filterReleaseNotesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
|
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
|
||||||
bool useLatestAssetDateAsReleaseDate =
|
bool useLatestAssetDateAsReleaseDate =
|
||||||
additionalSettings['useLatestAssetDateAsReleaseDate'] == true;
|
additionalSettings['useLatestAssetDateAsReleaseDate'] == true;
|
||||||
@ -262,8 +312,9 @@ class GitHub extends AppSource {
|
|||||||
if (verifyLatestTag) {
|
if (verifyLatestTag) {
|
||||||
var temp = requestUrl.split('?');
|
var temp = requestUrl.split('?');
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}',
|
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
if (onHttpErrorCode != null) {
|
if (onHttpErrorCode != null) {
|
||||||
onHttpErrorCode(res);
|
onHttpErrorCode(res);
|
||||||
@ -278,8 +329,10 @@ class GitHub extends AppSource {
|
|||||||
if (latestRelease != null) {
|
if (latestRelease != null) {
|
||||||
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
|
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
|
||||||
if (releases
|
if (releases
|
||||||
.where((element) =>
|
.where(
|
||||||
(element['tag_name'] ?? element['name']) == latestTag)
|
(element) =>
|
||||||
|
(element['tag_name'] ?? element['name']) == latestTag,
|
||||||
|
)
|
||||||
.isEmpty) {
|
.isEmpty) {
|
||||||
releases = [latestRelease, ...releases];
|
releases = [latestRelease, ...releases];
|
||||||
}
|
}
|
||||||
@ -299,10 +352,10 @@ class GitHub extends AppSource {
|
|||||||
|
|
||||||
DateTime? getPublishDateFromRelease(dynamic rel) =>
|
DateTime? getPublishDateFromRelease(dynamic rel) =>
|
||||||
rel?['published_at'] != null
|
rel?['published_at'] != null
|
||||||
? DateTime.parse(rel['published_at'])
|
? DateTime.parse(rel['published_at'])
|
||||||
: rel?['commit']?['created'] != null
|
: rel?['commit']?['created'] != null
|
||||||
? DateTime.parse(rel['commit']['created'])
|
? DateTime.parse(rel['commit']['created'])
|
||||||
: null;
|
: null;
|
||||||
DateTime? getNewestAssetDateFromRelease(dynamic rel) {
|
DateTime? getNewestAssetDateFromRelease(dynamic rel) {
|
||||||
var allAssets = rel['assets'] as List<dynamic>?;
|
var allAssets = rel['assets'] as List<dynamic>?;
|
||||||
var filteredAssets = rel['filteredAssets'] as List<dynamic>?;
|
var filteredAssets = rel['filteredAssets'] as List<dynamic>?;
|
||||||
@ -323,8 +376,8 @@ class GitHub extends AppSource {
|
|||||||
|
|
||||||
DateTime? getReleaseDateFromRelease(dynamic rel, bool useAssetDate) =>
|
DateTime? getReleaseDateFromRelease(dynamic rel, bool useAssetDate) =>
|
||||||
!useAssetDate
|
!useAssetDate
|
||||||
? getPublishDateFromRelease(rel)
|
? getPublishDateFromRelease(rel)
|
||||||
: getNewestAssetDateFromRelease(rel);
|
: getNewestAssetDateFromRelease(rel);
|
||||||
|
|
||||||
if (sortMethod == 'none') {
|
if (sortMethod == 'none') {
|
||||||
releases = releases.reversed.toList();
|
releases = releases.reversed.toList();
|
||||||
@ -340,29 +393,40 @@ class GitHub extends AppSource {
|
|||||||
} else {
|
} else {
|
||||||
var nameA = a['tag_name'] ?? a['name'];
|
var nameA = a['tag_name'] ?? a['name'];
|
||||||
var nameB = b['tag_name'] ?? b['name'];
|
var nameB = b['tag_name'] ?? b['name'];
|
||||||
var stdFormats = findStandardFormatsForVersion(nameA, false)
|
var stdFormats = findStandardFormatsForVersion(
|
||||||
.intersection(findStandardFormatsForVersion(nameB, false));
|
nameA,
|
||||||
|
false,
|
||||||
|
).intersection(findStandardFormatsForVersion(nameB, false));
|
||||||
if (sortMethod == 'date' ||
|
if (sortMethod == 'date' ||
|
||||||
(sortMethod == 'smartname-datefallback' &&
|
(sortMethod == 'smartname-datefallback' &&
|
||||||
stdFormats.isEmpty)) {
|
stdFormats.isEmpty)) {
|
||||||
return (getReleaseDateFromRelease(
|
return (getReleaseDateFromRelease(
|
||||||
a, useLatestAssetDateAsReleaseDate) ??
|
a,
|
||||||
|
useLatestAssetDateAsReleaseDate,
|
||||||
|
) ??
|
||||||
DateTime(1))
|
DateTime(1))
|
||||||
.compareTo(getReleaseDateFromRelease(
|
.compareTo(
|
||||||
b, useLatestAssetDateAsReleaseDate) ??
|
getReleaseDateFromRelease(
|
||||||
DateTime(0));
|
b,
|
||||||
|
useLatestAssetDateAsReleaseDate,
|
||||||
|
) ??
|
||||||
|
DateTime(0),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (sortMethod != 'name' && stdFormats.isNotEmpty) {
|
if (sortMethod != 'name' && stdFormats.isNotEmpty) {
|
||||||
var reg = RegExp(stdFormats.last);
|
var reg = RegExp(stdFormats.last);
|
||||||
var matchA = reg.firstMatch(nameA);
|
var matchA = reg.firstMatch(nameA);
|
||||||
var matchB = reg.firstMatch(nameB);
|
var matchB = reg.firstMatch(nameB);
|
||||||
return compareAlphaNumeric(
|
return compareAlphaNumeric(
|
||||||
(nameA as String).substring(matchA!.start, matchA.end),
|
(nameA as String).substring(matchA!.start, matchA.end),
|
||||||
(nameB as String).substring(matchB!.start, matchB.end));
|
(nameB as String).substring(matchB!.start, matchB.end),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 'name'
|
// 'name'
|
||||||
return compareAlphaNumeric(
|
return compareAlphaNumeric(
|
||||||
(nameA as String), (nameB as String));
|
(nameA as String),
|
||||||
|
(nameB as String),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -374,9 +438,11 @@ class GitHub extends AppSource {
|
|||||||
latestRelease !=
|
latestRelease !=
|
||||||
(releases[releases.length - 1]['tag_name'] ??
|
(releases[releases.length - 1]['tag_name'] ??
|
||||||
releases[0]['name'])) {
|
releases[0]['name'])) {
|
||||||
var ind = releases.indexWhere((element) =>
|
var ind = releases.indexWhere(
|
||||||
(latestRelease['tag_name'] ?? latestRelease['name']) ==
|
(element) =>
|
||||||
(element['tag_name'] ?? element['name']));
|
(latestRelease['tag_name'] ?? latestRelease['name']) ==
|
||||||
|
(element['tag_name'] ?? element['name']),
|
||||||
|
);
|
||||||
if (ind >= 0) {
|
if (ind >= 0) {
|
||||||
releases.add(releases.removeAt(ind));
|
releases.add(releases.removeAt(ind));
|
||||||
}
|
}
|
||||||
@ -404,8 +470,9 @@ class GitHub extends AppSource {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (regexNotesFilter != null &&
|
if (regexNotesFilter != null &&
|
||||||
!RegExp(regexNotesFilter)
|
!RegExp(
|
||||||
.hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
regexNotesFilter,
|
||||||
|
).hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var allAssetsWithUrls = findReleaseAssetUrls(releases[i]);
|
var allAssetsWithUrls = findReleaseAssetUrls(releases[i]);
|
||||||
@ -413,24 +480,31 @@ class GitHub extends AppSource {
|
|||||||
.map((e) => e['final_url'] as MapEntry<String, String>)
|
.map((e) => e['final_url'] as MapEntry<String, String>)
|
||||||
.toList();
|
.toList();
|
||||||
var apkAssetsWithUrls = allAssetsWithUrls
|
var apkAssetsWithUrls = allAssetsWithUrls
|
||||||
.where((element) =>
|
.where(
|
||||||
(element['final_url'] as MapEntry<String, String>)
|
(element) => (element['final_url'] as MapEntry<String, String>)
|
||||||
.key
|
.key
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.endsWith('.apk'))
|
.endsWith('.apk'),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
var filteredApkUrls = filterApks(
|
var filteredApkUrls = filterApks(
|
||||||
apkAssetsWithUrls
|
apkAssetsWithUrls
|
||||||
.map((e) => e['final_url'] as MapEntry<String, String>)
|
.map((e) => e['final_url'] as MapEntry<String, String>)
|
||||||
.toList(),
|
.toList(),
|
||||||
additionalSettings['apkFilterRegEx'],
|
additionalSettings['apkFilterRegEx'],
|
||||||
additionalSettings['invertAPKFilter']);
|
additionalSettings['invertAPKFilter'],
|
||||||
|
);
|
||||||
var filteredApks = apkAssetsWithUrls
|
var filteredApks = apkAssetsWithUrls
|
||||||
.where((e) => filteredApkUrls
|
.where(
|
||||||
.where((e2) =>
|
(e) => filteredApkUrls
|
||||||
e2.key == (e['final_url'] as MapEntry<String, String>).key)
|
.where(
|
||||||
.isNotEmpty)
|
(e2) =>
|
||||||
|
e2.key ==
|
||||||
|
(e['final_url'] as MapEntry<String, String>).key,
|
||||||
|
)
|
||||||
|
.isNotEmpty,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (filteredApks.isEmpty && additionalSettings['trackOnly'] != true) {
|
if (filteredApks.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||||
@ -441,17 +515,23 @@ class GitHub extends AppSource {
|
|||||||
targetRelease['filteredAssets'] = filteredApks;
|
targetRelease['filteredAssets'] = filteredApks;
|
||||||
targetRelease['version'] =
|
targetRelease['version'] =
|
||||||
additionalSettings['releaseTitleAsVersion'] == true
|
additionalSettings['releaseTitleAsVersion'] == true
|
||||||
? nameToFilter
|
? nameToFilter
|
||||||
: targetRelease['tag_name'] ?? targetRelease['name'];
|
: targetRelease['tag_name'] ?? targetRelease['name'];
|
||||||
if (targetRelease['tarball_url'] != null) {
|
if (targetRelease['tarball_url'] != null) {
|
||||||
allAssetUrls.add(MapEntry(
|
allAssetUrls.add(
|
||||||
|
MapEntry(
|
||||||
(targetRelease['version'] ?? 'source') + '.tar.gz',
|
(targetRelease['version'] ?? 'source') + '.tar.gz',
|
||||||
targetRelease['tarball_url']));
|
targetRelease['tarball_url'],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (targetRelease['zipball_url'] != null) {
|
if (targetRelease['zipball_url'] != null) {
|
||||||
allAssetUrls.add(MapEntry(
|
allAssetUrls.add(
|
||||||
|
MapEntry(
|
||||||
(targetRelease['version'] ?? 'source') + '.zip',
|
(targetRelease['version'] ?? 'source') + '.zip',
|
||||||
targetRelease['zipball_url']));
|
targetRelease['zipball_url'],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
targetRelease['allAssetUrls'] = allAssetUrls;
|
targetRelease['allAssetUrls'] = allAssetUrls;
|
||||||
break;
|
break;
|
||||||
@ -462,19 +542,22 @@ class GitHub extends AppSource {
|
|||||||
String? version = targetRelease['version'];
|
String? version = targetRelease['version'];
|
||||||
|
|
||||||
DateTime? releaseDate = getReleaseDateFromRelease(
|
DateTime? releaseDate = getReleaseDateFromRelease(
|
||||||
targetRelease, useLatestAssetDateAsReleaseDate);
|
targetRelease,
|
||||||
|
useLatestAssetDateAsReleaseDate,
|
||||||
|
);
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
var changeLog = (targetRelease['body'] ?? '').toString();
|
var changeLog = (targetRelease['body'] ?? '').toString();
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
|
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
|
||||||
getAppNames(standardUrl),
|
getAppNames(standardUrl),
|
||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate,
|
||||||
changeLog: changeLog.isEmpty ? null : changeLog,
|
changeLog: changeLog.isEmpty ? null : changeLog,
|
||||||
allAssetUrls:
|
allAssetUrls:
|
||||||
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>);
|
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (onHttpErrorCode != null) {
|
if (onHttpErrorCode != null) {
|
||||||
onHttpErrorCode(res);
|
onHttpErrorCode(res);
|
||||||
@ -483,20 +566,27 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLatestAPKDetailsCommon2(
|
Future<APKDetails> getLatestAPKDetailsCommon2(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
Future<String> Function(bool) reqUrlGenerator,
|
Future<String> Function(bool) reqUrlGenerator,
|
||||||
dynamic Function(Response)? onHttpErrorCode) async {
|
dynamic Function(Response)? onHttpErrorCode,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
return await getLatestAPKDetailsCommon(
|
return await getLatestAPKDetailsCommon(
|
||||||
await reqUrlGenerator(false), standardUrl, additionalSettings,
|
await reqUrlGenerator(false),
|
||||||
onHttpErrorCode: onHttpErrorCode);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err is NoReleasesError && additionalSettings['trackOnly'] == true) {
|
if (err is NoReleasesError && additionalSettings['trackOnly'] == true) {
|
||||||
return await getLatestAPKDetailsCommon(
|
return await getLatestAPKDetailsCommon(
|
||||||
await reqUrlGenerator(true), standardUrl, additionalSettings,
|
await reqUrlGenerator(true),
|
||||||
onHttpErrorCode: onHttpErrorCode);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@ -508,12 +598,16 @@ class GitHub extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
return await getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
return await getLatestAPKDetailsCommon2(
|
||||||
(bool useTagUrl) async {
|
standardUrl,
|
||||||
return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
additionalSettings,
|
||||||
}, (Response res) {
|
(bool useTagUrl) async {
|
||||||
rateLimitErrorCheck(res);
|
return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
});
|
},
|
||||||
|
(Response res) {
|
||||||
|
rateLimitErrorCheck(res);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
@ -523,9 +617,12 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<String>>> searchCommon(
|
Future<Map<String, List<String>>> searchCommon(
|
||||||
String query, String requestUrl, String rootProp,
|
String query,
|
||||||
{Function(Response)? onHttpErrorCode,
|
String requestUrl,
|
||||||
Map<String, dynamic> querySettings = const {}}) async {
|
String rootProp, {
|
||||||
|
Function(Response)? onHttpErrorCode,
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
Response res = await sourceRequest(requestUrl, {});
|
Response res = await sourceRequest(requestUrl, {});
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
int minStarCount = querySettings['minStarCount'] != null
|
int minStarCount = querySettings['minStarCount'] != null
|
||||||
@ -540,8 +637,8 @@ class GitHub extends AppSource {
|
|||||||
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
||||||
(e['description'] != null
|
(e['description'] != null
|
||||||
? e['description'] as String
|
? e['description'] as String
|
||||||
: tr('noDescription')))
|
: tr('noDescription'))),
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -555,22 +652,27 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
return searchCommon(
|
return searchCommon(
|
||||||
query,
|
query,
|
||||||
'${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
'${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||||
'items', onHttpErrorCode: (Response res) {
|
'items',
|
||||||
rateLimitErrorCheck(res);
|
onHttpErrorCode: (Response res) {
|
||||||
}, querySettings: querySettings);
|
rateLimitErrorCheck(res);
|
||||||
|
},
|
||||||
|
querySettings: querySettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
rateLimitErrorCheck(Response res) {
|
void rateLimitErrorCheck(Response res) {
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
throw RateLimitError(
|
throw RateLimitError(
|
||||||
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000)
|
||||||
60000000)
|
.round(),
|
||||||
.round());
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,36 +18,41 @@ class GitLab extends AppSource {
|
|||||||
showReleaseDateAsVersionToggle = true;
|
showReleaseDateAsVersionToggle = true;
|
||||||
|
|
||||||
sourceConfigSettingFormItems = [
|
sourceConfigSettingFormItems = [
|
||||||
GeneratedFormTextField('gitlab-creds',
|
GeneratedFormTextField(
|
||||||
label: tr('gitlabPATLabel'),
|
'gitlab-creds',
|
||||||
password: true,
|
label: tr('gitlabPATLabel'),
|
||||||
required: false,
|
password: true,
|
||||||
belowWidgets: [
|
required: false,
|
||||||
const SizedBox(
|
belowWidgets: [
|
||||||
height: 4,
|
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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
),
|
||||||
onTap: () {
|
const SizedBox(height: 4),
|
||||||
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 = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
]
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,11 +60,13 @@ class GitLab extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
var urlSegments = url.split('/');
|
var urlSegments = url.split('/');
|
||||||
var cutOffIndex = urlSegments.indexWhere((s) => s == '-');
|
var cutOffIndex = urlSegments.indexWhere((s) => s == '-');
|
||||||
url =
|
url = urlSegments
|
||||||
urlSegments.sublist(0, cutOffIndex <= 0 ? null : cutOffIndex).join('/');
|
.sublist(0, cutOffIndex <= 0 ? null : cutOffIndex)
|
||||||
|
.join('/');
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^((\b/\b)|(\b/-/\b))]+){1,20}',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^((\b/\b)|(\b/-/\b))]+){1,20}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -70,15 +77,19 @@ class GitLab extends AppSource {
|
|||||||
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
||||||
SettingsProvider settingsProvider = SettingsProvider();
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
await settingsProvider.initializeSettings();
|
await settingsProvider.initializeSettings();
|
||||||
var sourceConfig =
|
var sourceConfig = await getSourceConfigValues(
|
||||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
additionalSettings,
|
||||||
|
settingsProvider,
|
||||||
|
);
|
||||||
String? creds = sourceConfig['gitlab-creds'];
|
String? creds = sourceConfig['gitlab-creds'];
|
||||||
return creds != null && creds.isNotEmpty ? creds : null;
|
return creds != null && creds.isNotEmpty ? creds : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
var url =
|
var url =
|
||||||
'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
|
'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
|
||||||
var res = await sourceRequest(url, {});
|
var res = await sourceRequest(url, {});
|
||||||
@ -90,7 +101,7 @@ class GitLab extends AppSource {
|
|||||||
for (var element in json) {
|
for (var element in json) {
|
||||||
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
|
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
|
||||||
element['name_with_namespace'],
|
element['name_with_namespace'],
|
||||||
element['description'] ?? tr('noDescription')
|
element['description'] ?? tr('noDescription'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
@ -102,8 +113,9 @@ class GitLab extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
// Change headers to pacify, e.g. cloudflare protection
|
// Change headers to pacify, e.g. cloudflare protection
|
||||||
// Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697)
|
// Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697)
|
||||||
var headers = <String, String>{};
|
var headers = <String, String>{};
|
||||||
@ -116,8 +128,11 @@ class GitLab extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Map<String, dynamic> additionalSettings) async {
|
String apkUrl,
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
||||||
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
|
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
|
||||||
return '$apkUrl${(Uri.parse(apkUrl).query.isEmpty ? '?' : '&')}$optionalAuth';
|
return '$apkUrl${(Uri.parse(apkUrl).query.isEmpty ? '?' : '&')}$optionalAuth';
|
||||||
@ -139,8 +154,9 @@ class GitLab extends AppSource {
|
|||||||
|
|
||||||
// Get project ID
|
// Get project ID
|
||||||
Response res0 = await sourceRequest(
|
Response res0 = await sourceRequest(
|
||||||
'https://${hosts[0]}/api/v4/projects/$projectUriComponent?$optionalAuth',
|
'https://${hosts[0]}/api/v4/projects/$projectUriComponent?$optionalAuth',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res0.statusCode != 200) {
|
if (res0.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res0);
|
throw getObtainiumHttpError(res0);
|
||||||
}
|
}
|
||||||
@ -151,8 +167,9 @@ class GitLab extends AppSource {
|
|||||||
|
|
||||||
// Request data from REST API
|
// Request data from REST API
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'https://${hosts[0]}/api/v4/projects/$projectUriComponent/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
|
'https://${hosts[0]}/api/v4/projects/$projectUriComponent/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@ -166,11 +183,13 @@ class GitLab extends AppSource {
|
|||||||
var url = (e['direct_asset_url'] ?? e['url'] ?? '') as String;
|
var url = (e['direct_asset_url'] ?? e['url'] ?? '') as String;
|
||||||
var parsedUrl = url.isNotEmpty ? Uri.parse(url) : null;
|
var parsedUrl = url.isNotEmpty ? Uri.parse(url) : null;
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
(e['name'] ??
|
(e['name'] ??
|
||||||
(parsedUrl != null && parsedUrl.pathSegments.isNotEmpty
|
(parsedUrl != null && parsedUrl.pathSegments.isNotEmpty
|
||||||
? parsedUrl.pathSegments.last
|
? parsedUrl.pathSegments.last
|
||||||
: 'unknown')) as String,
|
: 'unknown'))
|
||||||
(e['direct_asset_url'] ?? e['url'] ?? '') as String);
|
as String,
|
||||||
|
(e['direct_asset_url'] ?? e['url'] ?? '') as String,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.where((s) => s.key.isNotEmpty)
|
.where((s) => s.key.isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
@ -193,11 +212,15 @@ class GitLab extends AppSource {
|
|||||||
}
|
}
|
||||||
var releaseDateString =
|
var releaseDateString =
|
||||||
e['released_at'] ?? e['created_at'] ?? e['commit']?['created_at'];
|
e['released_at'] ?? e['created_at'] ?? e['commit']?['created_at'];
|
||||||
DateTime? releaseDate =
|
DateTime? releaseDate = releaseDateString != null
|
||||||
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
|
? DateTime.parse(releaseDateString)
|
||||||
return APKDetails(e['tag_name'] ?? e['name'], apkUrls.entries.toList(),
|
: null;
|
||||||
AppNames(names.author, names.name.split('/').last),
|
return APKDetails(
|
||||||
releaseDate: releaseDate);
|
e['tag_name'] ?? e['name'],
|
||||||
|
apkUrls.entries.toList(),
|
||||||
|
AppNames(names.author, names.name.split('/').last),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
if (apkDetailsList.isEmpty) {
|
if (apkDetailsList.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
@ -208,8 +231,9 @@ class GitLab extends AppSource {
|
|||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) {
|
if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) {
|
||||||
apkDetailsList =
|
apkDetailsList = apkDetailsList
|
||||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
.where((e) => e.apkUrls.isNotEmpty)
|
||||||
|
.toList();
|
||||||
finalResult = apkDetailsList.first;
|
finalResult = apkDetailsList.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,10 +242,13 @@ class GitLab extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
|
finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
|
||||||
if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+')
|
if (RegExp(
|
||||||
.hasMatch(apkUrl.value)) {
|
'^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+',
|
||||||
|
).hasMatch(apkUrl.value)) {
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/'));
|
apkUrl.key,
|
||||||
|
apkUrl.value.replaceFirst('/file/', '/raw/'),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return apkUrl;
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,13 @@ import 'package:obtainium/providers/apps_provider.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
||||||
|
try {
|
||||||
|
if (Uri.parse(ambiguousUrl).isAbsolute) {
|
||||||
|
return ambiguousUrl; // #2315
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
return referenceAbsoluteUrl.resolve(ambiguousUrl).toString();
|
return referenceAbsoluteUrl.resolve(ambiguousUrl).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,28 +100,37 @@ bool _isNumeric(String s) {
|
|||||||
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
|
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MapEntry<String, String>> getLinksInLines(String lines) => RegExp(
|
List<MapEntry<String, String>> getLinksInLines(String lines) =>
|
||||||
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
|
RegExp(
|
||||||
.allMatches(lines)
|
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?',
|
||||||
.map((match) =>
|
)
|
||||||
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
|
.allMatches(lines)
|
||||||
.toList();
|
.map(
|
||||||
|
(match) =>
|
||||||
|
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
// Given an HTTP response, grab some links according to the common additional settings
|
// Given an HTTP response, grab some links according to the common additional settings
|
||||||
// (those that apply to intermediate and final steps)
|
// (those that apply to intermediate and final steps)
|
||||||
Future<List<MapEntry<String, String>>> grabLinksCommon(
|
Future<List<MapEntry<String, String>>> grabLinksCommon(
|
||||||
Response res, Map<String, dynamic> additionalSettings) async {
|
Response res,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
var html = parse(res.body);
|
var html = parse(res.body);
|
||||||
List<MapEntry<String, String>> allLinks = html
|
List<MapEntry<String, String>> allLinks = html
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((element) => MapEntry(
|
.map(
|
||||||
|
(element) => MapEntry(
|
||||||
element.attributes['href'] ?? '',
|
element.attributes['href'] ?? '',
|
||||||
element.text.isNotEmpty
|
element.text.isNotEmpty
|
||||||
? element.text
|
? element.text
|
||||||
: (element.attributes['href'] ?? '').split('/').last))
|
: (element.attributes['href'] ?? '').split('/').last,
|
||||||
|
),
|
||||||
|
)
|
||||||
.where((element) => element.key.isNotEmpty)
|
.where((element) => element.key.isNotEmpty)
|
||||||
.map((e) => MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
|
.map((e) => MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
|
||||||
.toList();
|
.toList();
|
||||||
@ -127,9 +143,13 @@ Future<List<MapEntry<String, String>>> grabLinksCommon(
|
|||||||
var jsonStrings = collectAllStringsFromJSONObject(jsonDecode(res.body));
|
var jsonStrings = collectAllStringsFromJSONObject(jsonDecode(res.body));
|
||||||
allLinks = getLinksInLines(jsonStrings.join('\n'));
|
allLinks = getLinksInLines(jsonStrings.join('\n'));
|
||||||
if (allLinks.isEmpty) {
|
if (allLinks.isEmpty) {
|
||||||
allLinks = getLinksInLines(jsonStrings.map((l) {
|
allLinks = getLinksInLines(
|
||||||
return ensureAbsoluteUrl(l, res.request!.url);
|
jsonStrings
|
||||||
}).join('\n'));
|
.map((l) {
|
||||||
|
return ensureAbsoluteUrl(l, res.request!.url);
|
||||||
|
})
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
@ -158,17 +178,20 @@ Future<List<MapEntry<String, String>>> grabLinksCommon(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Some links may not have valid encoding
|
// Some links may not have valid encoding
|
||||||
}
|
}
|
||||||
return Uri.parse(filterLinkByText ? element.value : link)
|
return Uri.parse(
|
||||||
.path
|
filterLinkByText ? element.value : link,
|
||||||
.toLowerCase()
|
).path.toLowerCase().endsWith('.apk');
|
||||||
.endsWith('.apk');
|
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
if (!skipSort) {
|
if (!skipSort) {
|
||||||
links.sort((a, b) => additionalSettings['sortByLastLinkSegment'] == true
|
links.sort(
|
||||||
? compareAlphaNumeric(a.key.split('/').where((e) => e.isNotEmpty).last,
|
(a, b) => additionalSettings['sortByLastLinkSegment'] == true
|
||||||
b.key.split('/').where((e) => e.isNotEmpty).last)
|
? compareAlphaNumeric(
|
||||||
: compareAlphaNumeric(a.key, b.key));
|
a.key.split('/').where((e) => e.isNotEmpty).last,
|
||||||
|
b.key.split('/').where((e) => e.isNotEmpty).last,
|
||||||
|
)
|
||||||
|
: compareAlphaNumeric(a.key, b.key),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (additionalSettings['reverseSort'] == true) {
|
if (additionalSettings['reverseSort'] == true) {
|
||||||
links = links.reversed.toList();
|
links = links.reversed.toList();
|
||||||
@ -194,102 +217,119 @@ class HTML extends AppSource {
|
|||||||
|
|
||||||
var finalStepFormitems = [
|
var finalStepFormitems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('customLinkFilterRegex',
|
GeneratedFormTextField(
|
||||||
label: tr('customLinkFilterRegex'),
|
'customLinkFilterRegex',
|
||||||
hint: 'download/(.*/)?(android|apk|mobile)',
|
label: tr('customLinkFilterRegex'),
|
||||||
required: false,
|
hint: 'download/(.*/)?(android|apk|mobile)',
|
||||||
additionalValidators: [
|
required: false,
|
||||||
(value) {
|
additionalValidators: [
|
||||||
return regExValidator(value);
|
(value) {
|
||||||
}
|
return regExValidator(value);
|
||||||
])
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('versionExtractWholePage',
|
GeneratedFormSwitch(
|
||||||
label: tr('versionExtractWholePage'))
|
'versionExtractWholePage',
|
||||||
]
|
label: tr('versionExtractWholePage'),
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
var commonFormItems = [
|
var commonFormItems = [
|
||||||
[GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
|
[GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
|
||||||
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
|
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
|
||||||
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
|
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('sortByLastLinkSegment',
|
GeneratedFormSwitch(
|
||||||
label: tr('sortByLastLinkSegment'))
|
'sortByLastLinkSegment',
|
||||||
|
label: tr('sortByLastLinkSegment'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
var intermediateFormItems = [
|
var intermediateFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('customLinkFilterRegex',
|
GeneratedFormTextField(
|
||||||
label: tr('intermediateLinkRegex'),
|
'customLinkFilterRegex',
|
||||||
hint: '([0-9]+.)*[0-9]+/\$',
|
label: tr('intermediateLinkRegex'),
|
||||||
required: true,
|
hint: '([0-9]+.)*[0-9]+/\$',
|
||||||
additionalValidators: [(value) => regExValidator(value)])
|
required: true,
|
||||||
|
additionalValidators: [(value) => regExValidator(value)],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('autoLinkFilterByArch',
|
GeneratedFormSwitch(
|
||||||
label: tr('autoLinkFilterByArch'), defaultValue: false)
|
'autoLinkFilterByArch',
|
||||||
|
label: tr('autoLinkFilterByArch'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
HTML() {
|
HTML() {
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSubForm(
|
GeneratedFormSubForm('intermediateLink', [
|
||||||
'intermediateLink', [...intermediateFormItems, ...commonFormItems],
|
...intermediateFormItems,
|
||||||
label: tr('intermediateLink'))
|
...commonFormItems,
|
||||||
|
], label: tr('intermediateLink')),
|
||||||
],
|
],
|
||||||
finalStepFormitems[0],
|
finalStepFormitems[0],
|
||||||
...commonFormItems,
|
...commonFormItems,
|
||||||
...finalStepFormitems.sublist(1),
|
...finalStepFormitems.sublist(1),
|
||||||
[
|
[
|
||||||
GeneratedFormSubForm(
|
GeneratedFormSubForm(
|
||||||
'requestHeader',
|
'requestHeader',
|
||||||
|
[
|
||||||
[
|
[
|
||||||
[
|
GeneratedFormTextField(
|
||||||
GeneratedFormTextField('requestHeader',
|
'requestHeader',
|
||||||
label: tr('requestHeader'),
|
label: tr('requestHeader'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
if ((value ?? 'empty:valid')
|
if ((value ?? 'empty:valid')
|
||||||
.split(':')
|
.split(':')
|
||||||
.map((e) => e.trim())
|
.map((e) => e.trim())
|
||||||
.where((e) => e.isNotEmpty)
|
.where((e) => e.isNotEmpty)
|
||||||
.length <
|
.length <
|
||||||
2) {
|
2) {
|
||||||
return tr('invalidInput');
|
return tr('invalidInput');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
]
|
),
|
||||||
],
|
],
|
||||||
label: tr('requestHeader'),
|
],
|
||||||
defaultValue: [
|
label: tr('requestHeader'),
|
||||||
{
|
defaultValue: [
|
||||||
'requestHeader':
|
{
|
||||||
'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
|
'requestHeader':
|
||||||
}
|
'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36',
|
||||||
])
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormDropdown(
|
GeneratedFormDropdown(
|
||||||
'defaultPseudoVersioningMethod',
|
'defaultPseudoVersioningMethod',
|
||||||
[
|
[
|
||||||
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
||||||
MapEntry('APKLinkHash', tr('APKLinkHash')),
|
MapEntry('APKLinkHash', tr('APKLinkHash')),
|
||||||
MapEntry('ETag', 'ETag')
|
MapEntry('ETag', 'ETag'),
|
||||||
],
|
],
|
||||||
label: tr('defaultPseudoVersioningMethod'),
|
label: tr('defaultPseudoVersioningMethod'),
|
||||||
defaultValue: 'partialAPKHash')
|
defaultValue: 'partialAPKHash',
|
||||||
]
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
if (additionalSettings.isNotEmpty) {
|
if (additionalSettings.isNotEmpty) {
|
||||||
if (additionalSettings['requestHeader']?.isNotEmpty != true) {
|
if (additionalSettings['requestHeader']?.isNotEmpty != true) {
|
||||||
additionalSettings['requestHeader'] = [];
|
additionalSettings['requestHeader'] = [];
|
||||||
@ -329,8 +369,9 @@ class HTML extends AppSource {
|
|||||||
.toList();
|
.toList();
|
||||||
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
|
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
|
||||||
var intLinks = await grabLinksCommon(
|
var intLinks = await grabLinksCommon(
|
||||||
await sourceRequest(currentUrl, additionalSettings),
|
await sourceRequest(currentUrl, additionalSettings),
|
||||||
additionalSettings['intermediateLink'][i]);
|
additionalSettings['intermediateLink'][i],
|
||||||
|
);
|
||||||
if (intLinks.isEmpty) {
|
if (intLinks.isEmpty) {
|
||||||
throw NoReleasesError(note: currentUrl);
|
throw NoReleasesError(note: currentUrl);
|
||||||
} else {
|
} else {
|
||||||
@ -346,11 +387,17 @@ class HTML extends AppSource {
|
|||||||
String versionExtractionWholePageString = currentUrl;
|
String versionExtractionWholePageString = currentUrl;
|
||||||
if (additionalSettings['directAPKLink'] != true) {
|
if (additionalSettings['directAPKLink'] != true) {
|
||||||
Response res = await sourceRequest(currentUrl, additionalSettings);
|
Response res = await sourceRequest(currentUrl, additionalSettings);
|
||||||
versionExtractionWholePageString =
|
versionExtractionWholePageString = res.body
|
||||||
res.body.split('\r\n').join('\n').split('\n').join('\\n');
|
.split('\r\n')
|
||||||
|
.join('\n')
|
||||||
|
.split('\n')
|
||||||
|
.join('\\n');
|
||||||
links = await grabLinksCommon(res, additionalSettings);
|
links = await grabLinksCommon(res, additionalSettings);
|
||||||
links = filterApks(links, additionalSettings['apkFilterRegEx'],
|
links = filterApks(
|
||||||
additionalSettings['invertAPKFilter']);
|
links,
|
||||||
|
additionalSettings['apkFilterRegEx'],
|
||||||
|
additionalSettings['invertAPKFilter'],
|
||||||
|
);
|
||||||
if (links.isEmpty) {
|
if (links.isEmpty) {
|
||||||
throw NoReleasesError(note: currentUrl);
|
throw NoReleasesError(note: currentUrl);
|
||||||
}
|
}
|
||||||
@ -366,37 +413,45 @@ class HTML extends AppSource {
|
|||||||
}
|
}
|
||||||
String? version;
|
String? version;
|
||||||
version = extractVersion(
|
version = extractVersion(
|
||||||
additionalSettings['versionExtractionRegEx'] as String?,
|
additionalSettings['versionExtractionRegEx'] as String?,
|
||||||
additionalSettings['matchGroupToUse'] as String?,
|
additionalSettings['matchGroupToUse'] as String?,
|
||||||
additionalSettings['versionExtractWholePage'] == true
|
additionalSettings['versionExtractWholePage'] == true
|
||||||
? versionExtractionWholePageString
|
? versionExtractionWholePageString
|
||||||
: relDecoded);
|
: relDecoded,
|
||||||
var apkReqHeaders =
|
);
|
||||||
await getRequestHeaders(additionalSettings, forAPKDownload: true);
|
var apkReqHeaders = await getRequestHeaders(
|
||||||
|
additionalSettings,
|
||||||
|
forAPKDownload: true,
|
||||||
|
);
|
||||||
if (version == null &&
|
if (version == null &&
|
||||||
additionalSettings['defaultPseudoVersioningMethod'] == 'ETag') {
|
additionalSettings['defaultPseudoVersioningMethod'] == 'ETag') {
|
||||||
version = await checkETagHeader(rel,
|
version = await checkETagHeader(
|
||||||
headers: apkReqHeaders,
|
rel,
|
||||||
allowInsecure: additionalSettings['allowInsecure'] == true);
|
headers: apkReqHeaders,
|
||||||
|
allowInsecure: additionalSettings['allowInsecure'] == true,
|
||||||
|
);
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
version ??=
|
version ??=
|
||||||
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
|
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
|
||||||
? rel.hashCode.toString()
|
? rel.hashCode.toString()
|
||||||
: (await checkPartialDownloadHashDynamic(rel,
|
: (await checkPartialDownloadHashDynamic(
|
||||||
headers: apkReqHeaders,
|
rel,
|
||||||
allowInsecure: additionalSettings['allowInsecure'] == true))
|
headers: apkReqHeaders,
|
||||||
.toString();
|
allowInsecure: additionalSettings['allowInsecure'] == true,
|
||||||
|
)).toString();
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
[rel].map((e) {
|
[rel].map((e) {
|
||||||
var uri = Uri.parse(e);
|
var uri = Uri.parse(e);
|
||||||
var fileName =
|
var fileName = uri.pathSegments.isNotEmpty
|
||||||
uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.origin;
|
? uri.pathSegments.last
|
||||||
return MapEntry('${e.hashCode}-$fileName', e);
|
: uri.origin;
|
||||||
}).toList(),
|
return MapEntry('${e.hashCode}-$fileName', e);
|
||||||
AppNames(uri.host, tr('app')));
|
}).toList(),
|
||||||
|
AppNames(uri.host, tr('app')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,9 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}(/#)?/(app|appdl)/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}(/#)?/(app|appdl)/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -23,13 +24,18 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
return match.group(0)!;
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDlUrl(String standardUrl) =>
|
String getDlUrl(String standardUrl) =>
|
||||||
'https://${hosts[0].replaceAll('appgallery.huawei', 'appgallery.cloud.huawei')}/appdl/${standardUrl.split('/').last}';
|
'https://${hosts[0].replaceAll('appgallery.huawei', 'appgallery.cloud.huawei')}/appdl/${standardUrl.split('/').last}';
|
||||||
|
|
||||||
requestAppdlRedirect(
|
Future<Response> requestAppdlRedirect(
|
||||||
String dlUrl, Map<String, dynamic> additionalSettings) async {
|
String dlUrl,
|
||||||
Response res =
|
Map<String, dynamic> additionalSettings,
|
||||||
await sourceRequest(dlUrl, additionalSettings, followRedirects: false);
|
) async {
|
||||||
|
Response res = await sourceRequest(
|
||||||
|
dlUrl,
|
||||||
|
additionalSettings,
|
||||||
|
followRedirects: false,
|
||||||
|
);
|
||||||
if (res.statusCode == 200 ||
|
if (res.statusCode == 200 ||
|
||||||
res.statusCode == 302 ||
|
res.statusCode == 302 ||
|
||||||
res.statusCode == 304) {
|
res.statusCode == 304) {
|
||||||
@ -39,7 +45,7 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appIdFromRedirectDlUrl(String redirectDlUrl) {
|
String appIdFromRedirectDlUrl(String redirectDlUrl) {
|
||||||
var parts = redirectDlUrl
|
var parts = redirectDlUrl
|
||||||
.split('?')[0]
|
.split('?')[0]
|
||||||
.split('/')
|
.split('/')
|
||||||
@ -53,8 +59,10 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
String dlUrl = getDlUrl(standardUrl);
|
String dlUrl = getDlUrl(standardUrl);
|
||||||
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
||||||
return res.headers['location'] != null
|
return res.headers['location'] != null
|
||||||
@ -76,8 +84,11 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
if (appId.isEmpty) {
|
if (appId.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var relDateStr =
|
var relDateStr = res.headers['location']
|
||||||
res.headers['location']?.split('?')[0].split('.').reversed.toList()[1];
|
?.split('?')[0]
|
||||||
|
.split('.')
|
||||||
|
.reversed
|
||||||
|
.toList()[1];
|
||||||
if (relDateStr == null || relDateStr.length != 10) {
|
if (relDateStr == null || relDateStr.length != 10) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -88,10 +99,15 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
relDateStrAdj.insert((i + i ~/ 2 - 1), '-');
|
relDateStrAdj.insert((i + i ~/ 2 - 1), '-');
|
||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
var relDate =
|
var relDate = DateFormat(
|
||||||
DateFormat('yy-MM-dd-HH-mm', 'en_US').parse(relDateStrAdj.join(''));
|
'yy-MM-dd-HH-mm',
|
||||||
|
'en_US',
|
||||||
|
).parse(relDateStrAdj.join(''));
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
relDateStr, [MapEntry('$appId.apk', dlUrl)], AppNames(name, appId),
|
relDateStr,
|
||||||
releaseDate: relDate);
|
[MapEntry('$appId.apk', dlUrl)],
|
||||||
|
AppNames(name, appId),
|
||||||
|
releaseDate: relDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,15 @@ class IzzyOnDroid extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+',
|
'^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExA.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+',
|
'^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExB.firstMatch(url);
|
match = standardUrlRegExB.firstMatch(url);
|
||||||
}
|
}
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -32,8 +34,10 @@ class IzzyOnDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return fd.tryInferringAppId(standardUrl);
|
return fd.tryInferringAppId(standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,12 +48,14 @@ class IzzyOnDroid extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
String? appId = await tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest(
|
await sourceRequest(
|
||||||
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId',
|
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId',
|
||||||
additionalSettings),
|
additionalSettings,
|
||||||
'https://android.izzysoft.de/frepo/$appId',
|
),
|
||||||
standardUrl,
|
'https://android.izzysoft.de/frepo/$appId',
|
||||||
name,
|
standardUrl,
|
||||||
additionalSettings: additionalSettings);
|
name,
|
||||||
|
additionalSettings: additionalSettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,14 +31,17 @@ class Jenkins extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
standardUrl = trimJobUrl(standardUrl);
|
standardUrl = trimJobUrl(standardUrl);
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
|
'$standardUrl/lastSuccessfulBuild/api/json',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
var releaseDate = json['timestamp'] == null
|
var releaseDate = json['timestamp'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
||||||
var version =
|
var version = json['number'] == null
|
||||||
json['number'] == null ? null : (json['number'] as int).toString();
|
? null
|
||||||
|
: (json['number'] as int).toString();
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -51,16 +54,21 @@ class Jenkins extends AppSource {
|
|||||||
return path == null
|
return path == null
|
||||||
? const MapEntry<String, String>('', '')
|
? const MapEntry<String, String>('', '')
|
||||||
: MapEntry<String, String>(
|
: MapEntry<String, String>(
|
||||||
(e['fileName'] ?? e['relativePath']) as String, path);
|
(e['fileName'] ?? e['relativePath']) as String,
|
||||||
|
path,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.where((url) =>
|
.where(
|
||||||
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
|
(url) =>
|
||||||
|
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
apkUrls,
|
apkUrls,
|
||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate,
|
||||||
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last));
|
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,9 @@ class Mullvad extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -31,7 +32,9 @@ class Mullvad extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'$standardUrl/en/download/android', additionalSettings);
|
'$standardUrl/en/download/android',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var versions = parse(res.body)
|
var versions = parse(res.body)
|
||||||
.querySelectorAll('p')
|
.querySelectorAll('p')
|
||||||
@ -53,17 +56,18 @@ class Mullvad extends AppSource {
|
|||||||
String? changeLog;
|
String? changeLog;
|
||||||
try {
|
try {
|
||||||
changeLog = (await GitHub().getLatestAPKDetails(
|
changeLog = (await GitHub().getLatestAPKDetails(
|
||||||
'https://github.com/mullvad/mullvadvpn-app',
|
'https://github.com/mullvad/mullvadvpn-app',
|
||||||
{'fallbackToOlderReleases': true}))
|
{'fallbackToOlderReleases': true},
|
||||||
.changeLog;
|
)).changeLog;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
versions[0],
|
versions[0],
|
||||||
getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']),
|
getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']),
|
||||||
AppNames(name, 'Mullvad-VPN'),
|
AppNames(name, 'Mullvad-VPN'),
|
||||||
changeLog: changeLog);
|
changeLog: changeLog,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,9 @@ class NeutronCode extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -55,7 +56,7 @@ class NeutronCode extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customDateParse(String dateString) {
|
String? customDateParse(String dateString) {
|
||||||
List<String> parts = dateString.split(' ');
|
List<String> parts = dateString.split(' ');
|
||||||
if (parts.length != 3) {
|
if (parts.length != 3) {
|
||||||
return null;
|
return null;
|
||||||
@ -89,24 +90,31 @@ class NeutronCode extends AppSource {
|
|||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var version =
|
var version = http
|
||||||
http.querySelector('.pd-version-txt')?.nextElementSibling?.innerHtml;
|
.querySelector('.pd-version-txt')
|
||||||
|
?.nextElementSibling
|
||||||
|
?.innerHtml;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? apkUrl = 'https://${hosts[0]}/download/$filename';
|
String? apkUrl = 'https://${hosts[0]}/download/$filename';
|
||||||
var dateStringOriginal =
|
var dateStringOriginal = http
|
||||||
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
|
.querySelector('.pd-date-txt')
|
||||||
|
?.nextElementSibling
|
||||||
|
?.innerHtml;
|
||||||
var dateString = dateStringOriginal != null
|
var dateString = dateStringOriginal != null
|
||||||
? (customDateParse(dateStringOriginal))
|
? (customDateParse(dateStringOriginal))
|
||||||
: null;
|
: null;
|
||||||
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
|
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
|
||||||
return APKDetails(version, getApkUrlsFromUrls([apkUrl]),
|
return APKDetails(
|
||||||
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
|
version,
|
||||||
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
|
getApkUrlsFromUrls([apkUrl]),
|
||||||
changeLog: changeLogElements.isNotEmpty
|
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
|
||||||
? changeLogElements.last.innerHtml
|
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
|
||||||
: null);
|
changeLog: changeLogElements.isNotEmpty
|
||||||
|
? changeLogElements.last.innerHtml
|
||||||
|
: null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,9 @@ class RuStore extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/catalog/app/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/catalog/app/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -28,16 +29,18 @@ class RuStore extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> decodeString(String str) async {
|
Future<String> decodeString(String str) async {
|
||||||
try {
|
try {
|
||||||
return (await CharsetDetector.autoDecode(
|
return (await CharsetDetector.autoDecode(
|
||||||
Uint8List.fromList(str.codeUnits)))
|
Uint8List.fromList(str.codeUnits),
|
||||||
.string;
|
)).string;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
@ -50,8 +53,9 @@ class RuStore extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
String? appId = await tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
Response res0 = await sourceRequest(
|
Response res0 = await sourceRequest(
|
||||||
'https://backapi.rustore.ru/applicationData/overallInfo/$appId',
|
'https://backapi.rustore.ru/applicationData/overallInfo/$appId',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res0.statusCode != 200) {
|
if (res0.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res0);
|
throw getObtainiumHttpError(res0);
|
||||||
}
|
}
|
||||||
@ -74,10 +78,11 @@ class RuStore extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Response res1 = await sourceRequest(
|
Response res1 = await sourceRequest(
|
||||||
'https://backapi.rustore.ru/applicationData/download-link',
|
'https://backapi.rustore.ru/applicationData/download-link',
|
||||||
additionalSettings,
|
additionalSettings,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
postBody: {"appId": appDetails['appId'], "firstInstall": true});
|
postBody: {"appId": appDetails['appId'], "firstInstall": true},
|
||||||
|
);
|
||||||
var downloadDetails = jsonDecode(res1.body)['body'];
|
var downloadDetails = jsonDecode(res1.body)['body'];
|
||||||
if (res1.statusCode != 200 || downloadDetails['apkUrl'] == null) {
|
if (res1.statusCode != 200 || downloadDetails['apkUrl'] == null) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
@ -88,13 +93,16 @@ class RuStore extends AppSource {
|
|||||||
changeLog = changeLog != null ? await decodeString(changeLog) : null;
|
changeLog = changeLog != null ? await decodeString(changeLog) : null;
|
||||||
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
getApkUrlsFromUrls([
|
getApkUrlsFromUrls([
|
||||||
(downloadDetails['apkUrl'] as String)
|
(downloadDetails['apkUrl'] as String).replaceAll(
|
||||||
.replaceAll(RegExp('\\.zip\$'), '.apk')
|
RegExp('\\.zip\$'),
|
||||||
]),
|
'.apk',
|
||||||
AppNames(author, appName),
|
),
|
||||||
releaseDate: relDate,
|
]),
|
||||||
changeLog: changeLog);
|
AppNames(author, appName),
|
||||||
|
releaseDate: relDate,
|
||||||
|
changeLog: changeLog,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,23 +11,27 @@ class SourceForge extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
var sourceRegex = getSourceRegex(hosts);
|
var sourceRegex = getSourceRegex(hosts);
|
||||||
RegExp standardUrlRegExC =
|
RegExp standardUrlRegExC = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$sourceRegex/p/.+', caseSensitive: false);
|
'^https?://(www\\.)?$sourceRegex/p/.+',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExC.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExC.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url =
|
url =
|
||||||
'https://${Uri.parse(match.group(0)!).host}/projects/${url.substring(Uri.parse(match.group(0)!).host.length + '/projects/'.length + 1)}';
|
'https://${Uri.parse(match.group(0)!).host}/projects/${url.substring(Uri.parse(match.group(0)!).host.length + '/projects/'.length + 1)}';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://(www\\.)?$sourceRegex/projects/[^/]+',
|
'^https?://(www\\.)?$sourceRegex/projects/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExB.firstMatch(url);
|
match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null && match.group(0) == url) {
|
if (match != null && match.group(0) == url) {
|
||||||
url = '$url/files';
|
url = '$url/files';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://(www\\.)?$sourceRegex/projects/[^/]+/files(/.+)?',
|
'^https?://(www\\.)?$sourceRegex/projects/[^/]+/files(/.+)?',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExA.firstMatch(url);
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -46,8 +50,9 @@ class SourceForge extends AppSource {
|
|||||||
standardUri = Uri.parse(standardUrl);
|
standardUri = Uri.parse(standardUrl);
|
||||||
}
|
}
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
|
'${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
var allDownloadLinks = parsedHtml
|
var allDownloadLinks = parsedHtml
|
||||||
@ -74,9 +79,10 @@ class SourceForge extends AppSource {
|
|||||||
if (version != null) {
|
if (version != null) {
|
||||||
try {
|
try {
|
||||||
var extractedVersion = extractVersion(
|
var extractedVersion = extractVersion(
|
||||||
additionalSettings['versionExtractionRegEx'] as String?,
|
additionalSettings['versionExtractionRegEx'] as String?,
|
||||||
additionalSettings['matchGroupToUse'] as String?,
|
additionalSettings['matchGroupToUse'] as String?,
|
||||||
version);
|
version,
|
||||||
|
);
|
||||||
if (extractedVersion != null) {
|
if (extractedVersion != null) {
|
||||||
version = extractedVersion;
|
version = extractedVersion;
|
||||||
}
|
}
|
||||||
@ -111,8 +117,11 @@ class SourceForge extends AppSource {
|
|||||||
.where((element) => getVersion(element) == version)
|
.where((element) => getVersion(element) == version)
|
||||||
.toList();
|
.toList();
|
||||||
var segments = standardUrl.split('/');
|
var segments = standardUrl.split('/');
|
||||||
return APKDetails(version, getApkUrlsFromUrls(apkUrlList),
|
return APKDetails(
|
||||||
AppNames(name, segments[segments.indexOf('files') - 1]));
|
version,
|
||||||
|
getApkUrlsFromUrls(apkUrlList),
|
||||||
|
AppNames(name, segments[segments.indexOf('files') - 1]),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -13,17 +13,21 @@ class SourceHut extends AppSource {
|
|||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
]
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -52,8 +56,10 @@ class SourceHut extends AppSource {
|
|||||||
String appName = standardUri.pathSegments.last;
|
String appName = standardUri.pathSegments.last;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
|
'$standardUrl/refs/rss.xml',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
List<APKDetails> apkDetailsList = [];
|
List<APKDetails> apkDetailsList = [];
|
||||||
@ -63,10 +69,10 @@ class SourceHut extends AppSource {
|
|||||||
ind++;
|
ind++;
|
||||||
String releasePage = // querySelector('link') fails for some reason
|
String releasePage = // querySelector('link') fails for some reason
|
||||||
entry
|
entry
|
||||||
.querySelector('guid') // Luckily guid is identical
|
.querySelector('guid') // Luckily guid is identical
|
||||||
?.innerHtml
|
?.innerHtml
|
||||||
.trim() ??
|
.trim() ??
|
||||||
'';
|
'';
|
||||||
if (!releasePage.startsWith('$standardUrl/refs')) {
|
if (!releasePage.startsWith('$standardUrl/refs')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -84,8 +90,9 @@ class SourceHut extends AppSource {
|
|||||||
? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
||||||
: null;
|
: null;
|
||||||
releaseDate = releaseDateString != null
|
releaseDate = releaseDateString != null
|
||||||
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z')
|
? DateFormat(
|
||||||
.parse(releaseDateString)
|
'EEE, dd MMM yyyy HH:mm:ss Z',
|
||||||
|
).parse(releaseDateString)
|
||||||
: null;
|
: null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
@ -93,27 +100,35 @@ class SourceHut extends AppSource {
|
|||||||
var res2 = await sourceRequest(releasePage, additionalSettings);
|
var res2 = await sourceRequest(releasePage, additionalSettings);
|
||||||
List<MapEntry<String, String>> apkUrls = [];
|
List<MapEntry<String, String>> apkUrls = [];
|
||||||
if (res2.statusCode == 200) {
|
if (res2.statusCode == 200) {
|
||||||
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
apkUrls = getApkUrlsFromUrls(
|
||||||
.querySelectorAll('a')
|
parse(res2.body)
|
||||||
.map((e) => e.attributes['href'] ?? '')
|
.querySelectorAll('a')
|
||||||
.where((e) => e.toLowerCase().endsWith('.apk'))
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
.map((e) => ensureAbsoluteUrl(e, standardUri))
|
.where((e) => e.toLowerCase().endsWith('.apk'))
|
||||||
.toList());
|
.map((e) => ensureAbsoluteUrl(e, standardUri))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
apkDetailsList.add(APKDetails(
|
apkDetailsList.add(
|
||||||
|
APKDetails(
|
||||||
version,
|
version,
|
||||||
apkUrls,
|
apkUrls,
|
||||||
AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName,
|
AppNames(
|
||||||
appName),
|
entry.querySelector('author')?.innerHtml.trim() ?? appName,
|
||||||
releaseDate: releaseDate));
|
appName,
|
||||||
|
),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (apkDetailsList.isEmpty) {
|
if (apkDetailsList.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
if (fallbackToOlderReleases) {
|
if (fallbackToOlderReleases) {
|
||||||
if (additionalSettings['trackOnly'] != true) {
|
if (additionalSettings['trackOnly'] != true) {
|
||||||
apkDetailsList =
|
apkDetailsList = apkDetailsList
|
||||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
.where((e) => e.apkUrls.isNotEmpty)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
if (apkDetailsList.isEmpty) {
|
if (apkDetailsList.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
|
@ -20,12 +20,15 @@ class TelegramApp extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
|
'https://t.me/s/TAndroidAPK',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var messages =
|
var messages = http.querySelectorAll(
|
||||||
http.querySelectorAll('.tgme_widget_message_text.js-message_text');
|
'.tgme_widget_message_text.js-message_text',
|
||||||
|
);
|
||||||
var version = messages.isNotEmpty
|
var version = messages.isNotEmpty
|
||||||
? messages.last.innerHtml.split('\n').first.trim().split(' ').first
|
? messages.last.innerHtml.split('\n').first.trim().split(' ').first
|
||||||
: null;
|
: null;
|
||||||
@ -33,10 +36,9 @@ class TelegramApp extends AppSource {
|
|||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? apkUrl = 'https://telegram.org/dl/android/apk';
|
String? apkUrl = 'https://telegram.org/dl/android/apk';
|
||||||
return APKDetails(
|
return APKDetails(version, [
|
||||||
version,
|
MapEntry<String, String>('telegram-$version.apk', apkUrl),
|
||||||
[MapEntry<String, String>('telegram-$version.apk', apkUrl)],
|
], AppNames('Telegram', 'Telegram'));
|
||||||
AppNames('Telegram', 'Telegram'));
|
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,9 @@ class Tencent extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://${getSourceRegex(hosts)}/appdetail/[^/]+',
|
'^https?://${getSourceRegex(hosts)}/appdetail/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
var match = standardUrlRegEx.firstMatch(url);
|
var match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -25,8 +26,10 @@ class Tencent extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,18 +39,16 @@ class Tencent extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String appId = (await tryInferringAppId(standardUrl))!;
|
String appId = (await tryInferringAppId(standardUrl))!;
|
||||||
String baseHost = Uri.parse(standardUrl)
|
String baseHost = Uri.parse(
|
||||||
.host
|
standardUrl,
|
||||||
.split('.')
|
).host.split('.').reversed.toList().sublist(0, 2).reversed.join('.');
|
||||||
.reversed
|
|
||||||
.toList()
|
|
||||||
.sublist(0, 2)
|
|
||||||
.reversed
|
|
||||||
.join('.');
|
|
||||||
|
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'https://upage.html5.$baseHost/wechat-apkinfo', additionalSettings,
|
'https://upage.html5.$baseHost/wechat-apkinfo',
|
||||||
followRedirects: false, postBody: {"packagename": appId});
|
additionalSettings,
|
||||||
|
followRedirects: false,
|
||||||
|
postBody: {"packagename": appId},
|
||||||
|
);
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
@ -64,14 +65,18 @@ class Tencent extends AppSource {
|
|||||||
var author = json['app_detail_records'][appId]['app_info']['author'];
|
var author = json['app_detail_records'][appId]['app_info']['author'];
|
||||||
var releaseDate =
|
var releaseDate =
|
||||||
json['app_detail_records'][appId]['app_info']['update_time'];
|
json['app_detail_records'][appId]['app_info']['update_time'];
|
||||||
var apkName = Uri.parse(apkUrl).queryParameters['fsname'] ??
|
var apkName =
|
||||||
|
Uri.parse(apkUrl).queryParameters['fsname'] ??
|
||||||
'${appId}_$version.apk';
|
'${appId}_$version.apk';
|
||||||
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, [MapEntry(apkName, apkUrl)], AppNames(author, appName),
|
version,
|
||||||
releaseDate: releaseDate != null
|
[MapEntry(apkName, apkUrl)],
|
||||||
? DateTime.fromMillisecondsSinceEpoch(releaseDate * 1000)
|
AppNames(author, appName),
|
||||||
: null);
|
releaseDate: releaseDate != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(releaseDate * 1000)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import 'package:html/parser.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';
|
||||||
|
|
||||||
parseDateTimeMMMddCommayyyy(String? dateString) {
|
DateTime? parseDateTimeMMMddCommayyyy(String? dateString) {
|
||||||
DateTime? releaseDate;
|
DateTime? releaseDate;
|
||||||
try {
|
try {
|
||||||
releaseDate = dateString != null
|
releaseDate = dateString != null
|
||||||
@ -30,8 +30,9 @@ class Uptodown extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -40,14 +41,20 @@ class Uptodown extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return (await getAppDetailsFromPage(
|
return (await getAppDetailsFromPage(
|
||||||
standardUrl, additionalSettings))['appId'];
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
))['appId'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String?>> getAppDetailsFromPage(
|
Future<Map<String, String?>> getAppDetailsFromPage(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest(standardUrl, additionalSettings);
|
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -63,8 +70,9 @@ class Uptodown extends AppSource {
|
|||||||
.toList();
|
.toList();
|
||||||
String? appId = detailElements.elementAtOrNull(0);
|
String? appId = detailElements.elementAtOrNull(0);
|
||||||
String? dateStr = detailElements.elementAtOrNull(6);
|
String? dateStr = detailElements.elementAtOrNull(6);
|
||||||
String? fileId =
|
String? fileId = html
|
||||||
html.querySelector('#detail-app-name')?.attributes['data-file-id'];
|
.querySelector('#detail-app-name')
|
||||||
|
?.attributes['data-file-id'];
|
||||||
String? extension = detailElements.elementAtOrNull(7)?.toLowerCase();
|
String? extension = detailElements.elementAtOrNull(7)?.toLowerCase();
|
||||||
return Map.fromEntries([
|
return Map.fromEntries([
|
||||||
MapEntry('version', version),
|
MapEntry('version', version),
|
||||||
@ -73,7 +81,7 @@ class Uptodown extends AppSource {
|
|||||||
MapEntry('author', author),
|
MapEntry('author', author),
|
||||||
MapEntry('dateStr', dateStr),
|
MapEntry('dateStr', dateStr),
|
||||||
MapEntry('fileId', fileId),
|
MapEntry('fileId', fileId),
|
||||||
MapEntry('extension', extension)
|
MapEntry('extension', extension),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,8 +90,10 @@ class Uptodown extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var appDetails =
|
var appDetails = await getAppDetailsFromPage(
|
||||||
await getAppDetailsFromPage(standardUrl, additionalSettings);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
var version = appDetails['version'];
|
var version = appDetails['version'];
|
||||||
var appId = appDetails['appId'];
|
var appId = appDetails['appId'];
|
||||||
var fileId = appDetails['fileId'];
|
var fileId = appDetails['fileId'];
|
||||||
@ -105,21 +115,28 @@ class Uptodown extends AppSource {
|
|||||||
if (dateStr != null) {
|
if (dateStr != null) {
|
||||||
relDate = parseDateTimeMMMddCommayyyy(dateStr);
|
relDate = parseDateTimeMMMddCommayyyy(dateStr);
|
||||||
}
|
}
|
||||||
return APKDetails(version, [MapEntry('$appId.$extension', apkUrl)],
|
return APKDetails(
|
||||||
AppNames(author, appName),
|
version,
|
||||||
releaseDate: relDate);
|
[MapEntry('$appId.$extension', apkUrl)],
|
||||||
|
AppNames(author, appName),
|
||||||
|
releaseDate: relDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Map<String, dynamic> additionalSettings) async {
|
String apkUrl,
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest(apkUrl, additionalSettings);
|
var res = await sourceRequest(apkUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
var html = parse(res.body);
|
var html = parse(res.body);
|
||||||
var finalUrlKey =
|
var finalUrlKey = html
|
||||||
html.querySelector('#detail-download-button')?.attributes['data-url'];
|
.querySelector('#detail-download-button')
|
||||||
|
?.attributes['data-url'];
|
||||||
if (finalUrlKey == null) {
|
if (finalUrlKey == null) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,19 @@ class VivoAppStore extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
var json = await getDetailJson(standardUrl, additionalSettings);
|
var json = await getDetailJson(standardUrl, additionalSettings);
|
||||||
return json['package_name'];
|
return json['package_name'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var json = await getDetailJson(standardUrl, additionalSettings);
|
var json = await getDetailJson(standardUrl, additionalSettings);
|
||||||
var appName = json['title_zh'].toString();
|
var appName = json['title_zh'].toString();
|
||||||
var packageName = json['package_name'].toString();
|
var packageName = json['package_name'].toString();
|
||||||
@ -42,13 +46,18 @@ class VivoAppStore extends AppSource {
|
|||||||
var apkUrl = json['download_url'].toString();
|
var apkUrl = json['download_url'].toString();
|
||||||
var apkName = '${packageName}_$versionCode.apk';
|
var apkName = '${packageName}_$versionCode.apk';
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
versionName, [MapEntry(apkName, apkUrl)], AppNames(developer, appName),
|
versionName,
|
||||||
releaseDate: DateTime.parse(uploadTime));
|
[MapEntry(apkName, apkUrl)],
|
||||||
|
AppNames(developer, appName),
|
||||||
|
releaseDate: DateTime.parse(uploadTime),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
var apiBaseUrl =
|
var apiBaseUrl =
|
||||||
'https://h5-api.appstore.vivo.com.cn/h5appstore/search/result-list?app_version=2100&page_index=1&apps_per_page=20&target=local&cfrom=2&key=';
|
'https://h5-api.appstore.vivo.com.cn/h5appstore/search/result-list?app_version=2100&page_index=1&apps_per_page=20&target=local&cfrom=2&key=';
|
||||||
var searchUrl = '$apiBaseUrl${Uri.encodeQueryComponent(query)}';
|
var searchUrl = '$apiBaseUrl${Uri.encodeQueryComponent(query)}';
|
||||||
@ -65,14 +74,16 @@ class VivoAppStore extends AppSource {
|
|||||||
for (var item in (resultsJson as List<dynamic>)) {
|
for (var item in (resultsJson as List<dynamic>)) {
|
||||||
results['$appDetailUrl${item['id']}'] = [
|
results['$appDetailUrl${item['id']}'] = [
|
||||||
item['title_zh'].toString(),
|
item['title_zh'].toString(),
|
||||||
item['developer'].toString()
|
item['developer'].toString(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getDetailJson(
|
Future<Map<String, dynamic>> getDetailJson(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var vivoAppId = parseVivoAppId(standardUrl);
|
var vivoAppId = parseVivoAppId(standardUrl);
|
||||||
var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/detail/';
|
var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/detail/';
|
||||||
var params = '?frompage=messageh5&app_version=2100';
|
var params = '?frompage=messageh5&app_version=2100';
|
||||||
|
@ -20,8 +20,9 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
|||||||
titlePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
titlePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(color: Theme.of(context).textTheme.bodyMedium!.color),
|
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -16,11 +16,13 @@ abstract class GeneratedFormItem {
|
|||||||
dynamic ensureType(dynamic val);
|
dynamic ensureType(dynamic val);
|
||||||
GeneratedFormItem clone();
|
GeneratedFormItem clone();
|
||||||
|
|
||||||
GeneratedFormItem(this.key,
|
GeneratedFormItem(
|
||||||
{this.label = 'Input',
|
this.key, {
|
||||||
this.belowWidgets = const [],
|
this.label = 'Input',
|
||||||
this.defaultValue,
|
this.belowWidgets = const [],
|
||||||
this.additionalValidators = const []});
|
this.defaultValue,
|
||||||
|
this.additionalValidators = const [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedFormTextField extends GeneratedFormItem {
|
class GeneratedFormTextField extends GeneratedFormItem {
|
||||||
@ -31,18 +33,19 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
late TextInputType? textInputType;
|
late TextInputType? textInputType;
|
||||||
late List<String>? autoCompleteOptions;
|
late List<String>? autoCompleteOptions;
|
||||||
|
|
||||||
GeneratedFormTextField(super.key,
|
GeneratedFormTextField(
|
||||||
{super.label,
|
super.key, {
|
||||||
super.belowWidgets,
|
super.label,
|
||||||
String super.defaultValue = '',
|
super.belowWidgets,
|
||||||
List<String? Function(String? value)> super.additionalValidators =
|
String super.defaultValue = '',
|
||||||
const [],
|
List<String? Function(String? value)> super.additionalValidators = const [],
|
||||||
this.required = true,
|
this.required = true,
|
||||||
this.max = 1,
|
this.max = 1,
|
||||||
this.hint,
|
this.hint,
|
||||||
this.password = false,
|
this.password = false,
|
||||||
this.textInputType,
|
this.textInputType,
|
||||||
this.autoCompleteOptions});
|
this.autoCompleteOptions,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String ensureType(val) {
|
String ensureType(val) {
|
||||||
@ -51,16 +54,18 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormTextField clone() {
|
GeneratedFormTextField clone() {
|
||||||
return GeneratedFormTextField(key,
|
return GeneratedFormTextField(
|
||||||
label: label,
|
key,
|
||||||
belowWidgets: belowWidgets,
|
label: label,
|
||||||
defaultValue: defaultValue,
|
belowWidgets: belowWidgets,
|
||||||
additionalValidators: List.from(additionalValidators),
|
defaultValue: defaultValue,
|
||||||
required: required,
|
additionalValidators: List.from(additionalValidators),
|
||||||
max: max,
|
required: required,
|
||||||
hint: hint,
|
max: max,
|
||||||
password: password,
|
hint: hint,
|
||||||
textInputType: textInputType);
|
password: password,
|
||||||
|
textInputType: textInputType,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,8 +96,9 @@ class GeneratedFormDropdown extends GeneratedFormItem {
|
|||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
disabledOptKeys:
|
disabledOptKeys: disabledOptKeys != null
|
||||||
disabledOptKeys != null ? List.from(disabledOptKeys!) : null,
|
? List.from(disabledOptKeys!)
|
||||||
|
: null,
|
||||||
additionalValidators: List.from(additionalValidators),
|
additionalValidators: List.from(additionalValidators),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -117,12 +123,14 @@ class GeneratedFormSwitch extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormSwitch clone() {
|
GeneratedFormSwitch clone() {
|
||||||
return GeneratedFormSwitch(key,
|
return GeneratedFormSwitch(
|
||||||
label: label,
|
key,
|
||||||
belowWidgets: belowWidgets,
|
label: label,
|
||||||
defaultValue: defaultValue,
|
belowWidgets: belowWidgets,
|
||||||
disabled: false,
|
defaultValue: defaultValue,
|
||||||
additionalValidators: List.from(additionalValidators));
|
disabled: false,
|
||||||
|
additionalValidators: List.from(additionalValidators),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,17 +140,20 @@ class GeneratedFormTagInput extends GeneratedFormItem {
|
|||||||
late WrapAlignment alignment;
|
late WrapAlignment alignment;
|
||||||
late String emptyMessage;
|
late String emptyMessage;
|
||||||
late bool showLabelWhenNotEmpty;
|
late bool showLabelWhenNotEmpty;
|
||||||
GeneratedFormTagInput(super.key,
|
GeneratedFormTagInput(
|
||||||
{super.label,
|
super.key, {
|
||||||
super.belowWidgets,
|
super.label,
|
||||||
Map<String, MapEntry<int, bool>> super.defaultValue = const {},
|
super.belowWidgets,
|
||||||
List<String? Function(Map<String, MapEntry<int, bool>> value)>
|
Map<String, MapEntry<int, bool>> super.defaultValue = const {},
|
||||||
super.additionalValidators = const [],
|
List<String? Function(Map<String, MapEntry<int, bool>> value)>
|
||||||
this.deleteConfirmationMessage,
|
super.additionalValidators =
|
||||||
this.singleSelect = false,
|
const [],
|
||||||
this.alignment = WrapAlignment.start,
|
this.deleteConfirmationMessage,
|
||||||
this.emptyMessage = 'Input',
|
this.singleSelect = false,
|
||||||
this.showLabelWhenNotEmpty = true});
|
this.alignment = WrapAlignment.start,
|
||||||
|
this.emptyMessage = 'Input',
|
||||||
|
this.showLabelWhenNotEmpty = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, MapEntry<int, bool>> ensureType(val) {
|
Map<String, MapEntry<int, bool>> ensureType(val) {
|
||||||
@ -151,25 +162,30 @@ class GeneratedFormTagInput extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormTagInput clone() {
|
GeneratedFormTagInput clone() {
|
||||||
return GeneratedFormTagInput(key,
|
return GeneratedFormTagInput(
|
||||||
label: label,
|
key,
|
||||||
belowWidgets: belowWidgets,
|
label: label,
|
||||||
defaultValue: defaultValue,
|
belowWidgets: belowWidgets,
|
||||||
additionalValidators: List.from(additionalValidators),
|
defaultValue: defaultValue,
|
||||||
deleteConfirmationMessage: deleteConfirmationMessage,
|
additionalValidators: List.from(additionalValidators),
|
||||||
singleSelect: singleSelect,
|
deleteConfirmationMessage: deleteConfirmationMessage,
|
||||||
alignment: alignment,
|
singleSelect: singleSelect,
|
||||||
emptyMessage: emptyMessage,
|
alignment: alignment,
|
||||||
showLabelWhenNotEmpty: showLabelWhenNotEmpty);
|
emptyMessage: emptyMessage,
|
||||||
|
showLabelWhenNotEmpty: showLabelWhenNotEmpty,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef OnValueChanges = void Function(
|
typedef OnValueChanges =
|
||||||
Map<String, dynamic> values, bool valid, bool isBuilding);
|
void Function(Map<String, dynamic> values, bool valid, bool isBuilding);
|
||||||
|
|
||||||
class GeneratedForm extends StatefulWidget {
|
class GeneratedForm extends StatefulWidget {
|
||||||
const GeneratedForm(
|
const GeneratedForm({
|
||||||
{super.key, required this.items, required this.onValueChanges});
|
super.key,
|
||||||
|
required this.items,
|
||||||
|
required this.onValueChanges,
|
||||||
|
});
|
||||||
|
|
||||||
final List<List<GeneratedFormItem>> items;
|
final List<List<GeneratedFormItem>> items;
|
||||||
final OnValueChanges onValueChanges;
|
final OnValueChanges onValueChanges;
|
||||||
@ -179,7 +195,8 @@ class GeneratedForm extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<List<GeneratedFormItem>> cloneFormItems(
|
List<List<GeneratedFormItem>> cloneFormItems(
|
||||||
List<List<GeneratedFormItem>> items) {
|
List<List<GeneratedFormItem>> items,
|
||||||
|
) {
|
||||||
List<List<GeneratedFormItem>> clonedItems = [];
|
List<List<GeneratedFormItem>> clonedItems = [];
|
||||||
for (var row in items) {
|
for (var row in items) {
|
||||||
List<GeneratedFormItem> clonedRow = [];
|
List<GeneratedFormItem> clonedRow = [];
|
||||||
@ -194,8 +211,13 @@ List<List<GeneratedFormItem>> cloneFormItems(
|
|||||||
class GeneratedFormSubForm extends GeneratedFormItem {
|
class GeneratedFormSubForm extends GeneratedFormItem {
|
||||||
final List<List<GeneratedFormItem>> items;
|
final List<List<GeneratedFormItem>> items;
|
||||||
|
|
||||||
GeneratedFormSubForm(super.key, this.items,
|
GeneratedFormSubForm(
|
||||||
{super.label, super.belowWidgets, super.defaultValue = const []});
|
super.key,
|
||||||
|
this.items, {
|
||||||
|
super.label,
|
||||||
|
super.belowWidgets,
|
||||||
|
super.defaultValue = const [],
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ensureType(val) {
|
ensureType(val) {
|
||||||
@ -204,8 +226,13 @@ class GeneratedFormSubForm extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormSubForm clone() {
|
GeneratedFormSubForm clone() {
|
||||||
return GeneratedFormSubForm(key, cloneFormItems(items),
|
return GeneratedFormSubForm(
|
||||||
label: label, belowWidgets: belowWidgets, defaultValue: defaultValue);
|
key,
|
||||||
|
cloneFormItems(items),
|
||||||
|
label: label,
|
||||||
|
belowWidgets: belowWidgets,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,13 +247,18 @@ Color generateRandomLightColor() {
|
|||||||
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
|
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
|
||||||
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
|
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
|
||||||
// Map RBG values from 0-1 to 0-255:
|
// Map RBG values from 0-1 to 0-255:
|
||||||
final List<int> rgbValues =
|
final List<int> rgbValues = rgbValuesDbl
|
||||||
rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
|
.map((rgb) => (rgb * 255).toInt())
|
||||||
|
.toList();
|
||||||
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
|
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
int generateRandomNumber(int seed1,
|
int generateRandomNumber(
|
||||||
{int seed2 = 0, int seed3 = 0, max = 10000}) {
|
int seed1, {
|
||||||
|
int seed2 = 0,
|
||||||
|
int seed3 = 0,
|
||||||
|
max = 10000,
|
||||||
|
}) {
|
||||||
int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode;
|
int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode;
|
||||||
Random random = Random(combinedSeed);
|
Random random = Random(combinedSeed);
|
||||||
int randomNumber = random.nextInt(max);
|
int randomNumber = random.nextInt(max);
|
||||||
@ -261,7 +293,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
widget.onValueChanges(returnValues, valid, isBuilding);
|
widget.onValueChanges(returnValues, valid, isBuilding);
|
||||||
}
|
}
|
||||||
|
|
||||||
initForm() {
|
void initForm() {
|
||||||
initKey = widget.key.toString();
|
initKey = widget.key.toString();
|
||||||
// Initialize form values as all empty
|
// Initialize form values as all empty
|
||||||
values.clear();
|
values.clear();
|
||||||
@ -297,9 +329,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
helperText:
|
helperText: formItem.label + (formItem.required ? ' *' : ''),
|
||||||
formItem.label + (formItem.required ? ' *' : ''),
|
hintText: formItem.hint,
|
||||||
hintText: formItem.hint),
|
),
|
||||||
minLines: formItem.max <= 1 ? null : formItem.max,
|
minLines: formItem.max <= 1 ? null : formItem.max,
|
||||||
maxLines: formItem.max <= 1 ? 1 : formItem.max,
|
maxLines: formItem.max <= 1 ? 1 : formItem.max,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@ -339,23 +371,26 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
return Text(tr('dropdownNoOptsError'));
|
return Text(tr('dropdownNoOptsError'));
|
||||||
}
|
}
|
||||||
return DropdownButtonFormField(
|
return DropdownButtonFormField(
|
||||||
decoration: InputDecoration(labelText: formItem.label),
|
decoration: InputDecoration(labelText: formItem.label),
|
||||||
value: values[formItem.key],
|
value: values[formItem.key],
|
||||||
items: formItem.opts!.map((e2) {
|
items: formItem.opts!.map((e2) {
|
||||||
var enabled =
|
var enabled = formItem.disabledOptKeys?.contains(e2.key) != true;
|
||||||
formItem.disabledOptKeys?.contains(e2.key) != true;
|
return DropdownMenuItem(
|
||||||
return DropdownMenuItem(
|
value: e2.key,
|
||||||
value: e2.key,
|
enabled: enabled,
|
||||||
enabled: enabled,
|
child: Opacity(
|
||||||
child: Opacity(
|
opacity: enabled ? 1 : 0.5,
|
||||||
opacity: enabled ? 1 : 0.5, child: Text(e2.value)));
|
child: Text(e2.value),
|
||||||
}).toList(),
|
),
|
||||||
onChanged: (value) {
|
);
|
||||||
setState(() {
|
}).toList(),
|
||||||
values[formItem.key] = value ?? formItem.opts!.first.key;
|
onChanged: (value) {
|
||||||
someValueChanged();
|
setState(() {
|
||||||
});
|
values[formItem.key] = value ?? formItem.opts!.first.key;
|
||||||
|
someValueChanged();
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
} else if (formItem is GeneratedFormSubForm) {
|
} else if (formItem is GeneratedFormSubForm) {
|
||||||
values[formItem.key] = [];
|
values[formItem.key] = [];
|
||||||
for (Map<String, dynamic> v
|
for (Map<String, dynamic> v
|
||||||
@ -394,33 +429,33 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Flexible(child: Text(widget.items[r][e].label)),
|
Flexible(child: Text(widget.items[r][e].label)),
|
||||||
const SizedBox(
|
const SizedBox(width: 8),
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Switch(
|
Switch(
|
||||||
value: values[fieldKey],
|
value: values[fieldKey],
|
||||||
onChanged:
|
onChanged: (widget.items[r][e] as GeneratedFormSwitch).disabled
|
||||||
(widget.items[r][e] as GeneratedFormSwitch).disabled
|
? null
|
||||||
? null
|
: (value) {
|
||||||
: (value) {
|
setState(() {
|
||||||
setState(() {
|
values[fieldKey] = value;
|
||||||
values[fieldKey] = value;
|
someValueChanged();
|
||||||
someValueChanged();
|
});
|
||||||
});
|
},
|
||||||
})
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else if (widget.items[r][e] is GeneratedFormTagInput) {
|
} else if (widget.items[r][e] is GeneratedFormTagInput) {
|
||||||
onAddPressed() {
|
onAddPressed() {
|
||||||
showDialog<Map<String, dynamic>?>(
|
showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: widget.items[r][e].label,
|
title: widget.items[r][e].label,
|
||||||
items: [
|
items: [
|
||||||
[GeneratedFormTextField('label', label: tr('label'))]
|
[GeneratedFormTextField('label', label: tr('label'))],
|
||||||
]);
|
],
|
||||||
}).then((value) {
|
);
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
String? label = value?['label'];
|
String? label = value?['label'];
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -434,8 +469,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
var someSelected = temp.entries
|
var someSelected = temp.entries
|
||||||
.where((element) => element.value.value)
|
.where((element) => element.value.value)
|
||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
temp[label] = MapEntry(generateRandomLightColor().value,
|
temp[label] = MapEntry(
|
||||||
!(someSelected && singleSelect));
|
generateRandomLightColor().value,
|
||||||
|
!(someSelected && singleSelect),
|
||||||
|
);
|
||||||
values[fieldKey] = temp;
|
values[fieldKey] = temp;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
}
|
}
|
||||||
@ -444,236 +481,274 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formInputs[r][e] =
|
formInputs[r][e] = Column(
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
children: [
|
||||||
?.isNotEmpty ==
|
if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
true &&
|
?.isNotEmpty ==
|
||||||
(widget.items[r][e] as GeneratedFormTagInput)
|
true &&
|
||||||
.showLabelWhenNotEmpty)
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
Column(
|
.showLabelWhenNotEmpty)
|
||||||
crossAxisAlignment:
|
Column(
|
||||||
(widget.items[r][e] as GeneratedFormTagInput).alignment ==
|
crossAxisAlignment:
|
||||||
WrapAlignment.center
|
(widget.items[r][e] as GeneratedFormTagInput).alignment ==
|
||||||
? CrossAxisAlignment.center
|
WrapAlignment.center
|
||||||
: CrossAxisAlignment.stretch,
|
? CrossAxisAlignment.center
|
||||||
|
: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(widget.items[r][e].label),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
alignment:
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput).alignment,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(widget.items[r][e].label),
|
// (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
const SizedBox(
|
// ?.isEmpty ==
|
||||||
height: 8,
|
// true
|
||||||
),
|
// ? Text(
|
||||||
],
|
// (widget.items[r][e] as GeneratedFormTagInput)
|
||||||
),
|
// .emptyMessage,
|
||||||
Wrap(
|
// )
|
||||||
alignment:
|
// : const SizedBox.shrink(),
|
||||||
(widget.items[r][e] as GeneratedFormTagInput).alignment,
|
...(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
?.entries
|
||||||
children: [
|
.map((e2) {
|
||||||
// (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
return Padding(
|
||||||
// ?.isEmpty ==
|
padding: const EdgeInsets.symmetric(
|
||||||
// true
|
horizontal: 4,
|
||||||
// ? Text(
|
),
|
||||||
// (widget.items[r][e] as GeneratedFormTagInput)
|
child: ChoiceChip(
|
||||||
// .emptyMessage,
|
label: Text(e2.key),
|
||||||
// )
|
backgroundColor: Color(
|
||||||
// : const SizedBox.shrink(),
|
e2.value.key,
|
||||||
...(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
).withAlpha(50),
|
||||||
?.entries
|
selectedColor: Color(e2.value.key),
|
||||||
.map((e2) {
|
visualDensity: VisualDensity.compact,
|
||||||
return Padding(
|
selected: e2.value.value,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
onSelected: (value) {
|
||||||
child: ChoiceChip(
|
setState(() {
|
||||||
label: Text(e2.key),
|
(values[fieldKey]
|
||||||
backgroundColor: Color(e2.value.key).withAlpha(50),
|
as Map<String, MapEntry<int, bool>>)[e2
|
||||||
selectedColor: Color(e2.value.key),
|
.key] = MapEntry(
|
||||||
visualDensity: VisualDensity.compact,
|
(values[fieldKey]
|
||||||
selected: e2.value.value,
|
as Map<
|
||||||
onSelected: (value) {
|
String,
|
||||||
setState(() {
|
MapEntry<int, bool>
|
||||||
(values[fieldKey] as Map<String,
|
>)[e2.key]!
|
||||||
MapEntry<int, bool>>)[e2.key] =
|
.key,
|
||||||
MapEntry(
|
value,
|
||||||
(values[fieldKey] as Map<String,
|
);
|
||||||
MapEntry<int, bool>>)[e2.key]!
|
if ((widget.items[r][e]
|
||||||
.key,
|
as GeneratedFormTagInput)
|
||||||
value);
|
.singleSelect &&
|
||||||
if ((widget.items[r][e]
|
value == true) {
|
||||||
as GeneratedFormTagInput)
|
for (var key
|
||||||
.singleSelect &&
|
in (values[fieldKey]
|
||||||
value == true) {
|
as Map<
|
||||||
for (var key in (values[fieldKey]
|
String,
|
||||||
as Map<String, MapEntry<int, bool>>)
|
MapEntry<int, bool>
|
||||||
.keys) {
|
>)
|
||||||
if (key != e2.key) {
|
.keys) {
|
||||||
(values[fieldKey] as Map<
|
if (key != e2.key) {
|
||||||
String,
|
(values[fieldKey]
|
||||||
MapEntry<int,
|
as Map<
|
||||||
bool>>)[key] = MapEntry(
|
String,
|
||||||
(values[fieldKey] as Map<String,
|
MapEntry<int, bool>
|
||||||
MapEntry<int, bool>>)[key]!
|
>)[key] = MapEntry(
|
||||||
.key,
|
(values[fieldKey]
|
||||||
false);
|
as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>
|
||||||
|
>)[key]!
|
||||||
|
.key,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
someValueChanged();
|
||||||
}
|
});
|
||||||
someValueChanged();
|
},
|
||||||
});
|
),
|
||||||
},
|
);
|
||||||
));
|
}) ??
|
||||||
}) ??
|
[const SizedBox.shrink()],
|
||||||
[const SizedBox.shrink()],
|
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
?.values
|
||||||
?.values
|
.where((e) => e.value)
|
||||||
.where((e) => e.value)
|
.length ==
|
||||||
.length ==
|
1
|
||||||
1
|
? Padding(
|
||||||
? Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
child: IconButton(
|
||||||
child: IconButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
var temp = values[fieldKey]
|
|
||||||
as Map<String, MapEntry<int, bool>>;
|
|
||||||
// get selected category str where bool is true
|
|
||||||
final oldEntry = temp.entries
|
|
||||||
.firstWhere((entry) => entry.value.value);
|
|
||||||
// generate new color, ensure it is not the same
|
|
||||||
int newColor = oldEntry.value.key;
|
|
||||||
while (oldEntry.value.key == newColor) {
|
|
||||||
newColor = generateRandomLightColor().value;
|
|
||||||
}
|
|
||||||
// Update entry with new color, remain selected
|
|
||||||
temp.update(oldEntry.key,
|
|
||||||
(old) => MapEntry(newColor, old.value));
|
|
||||||
values[fieldKey] = temp;
|
|
||||||
someValueChanged();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.format_color_fill_rounded),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
tooltip: tr('colour'),
|
|
||||||
))
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
|
||||||
?.values
|
|
||||||
.where((e) => e.value)
|
|
||||||
.isNotEmpty ==
|
|
||||||
true
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
fn() {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
var temp = values[fieldKey]
|
var temp =
|
||||||
as Map<String, MapEntry<int, bool>>;
|
values[fieldKey]
|
||||||
temp.removeWhere((key, value) => value.value);
|
as Map<String, MapEntry<int, bool>>;
|
||||||
|
// get selected category str where bool is true
|
||||||
|
final oldEntry = temp.entries.firstWhere(
|
||||||
|
(entry) => entry.value.value,
|
||||||
|
);
|
||||||
|
// generate new color, ensure it is not the same
|
||||||
|
int newColor = oldEntry.value.key;
|
||||||
|
while (oldEntry.value.key == newColor) {
|
||||||
|
newColor = generateRandomLightColor().value;
|
||||||
|
}
|
||||||
|
// Update entry with new color, remain selected
|
||||||
|
temp.update(
|
||||||
|
oldEntry.key,
|
||||||
|
(old) => MapEntry(newColor, old.value),
|
||||||
|
);
|
||||||
values[fieldKey] = temp;
|
values[fieldKey] = temp;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
icon: const Icon(Icons.format_color_fill_rounded),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tooltip: tr('colour'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.values
|
||||||
|
.where((e) => e.value)
|
||||||
|
.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
fn() {
|
||||||
|
setState(() {
|
||||||
|
var temp =
|
||||||
|
values[fieldKey]
|
||||||
|
as Map<String, MapEntry<int, bool>>;
|
||||||
|
temp.removeWhere((key, value) => value.value);
|
||||||
|
values[fieldKey] = temp;
|
||||||
|
someValueChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ((widget.items[r][e] as GeneratedFormTagInput)
|
if ((widget.items[r][e] as GeneratedFormTagInput)
|
||||||
.deleteConfirmationMessage !=
|
.deleteConfirmationMessage !=
|
||||||
null) {
|
null) {
|
||||||
var message =
|
var message =
|
||||||
(widget.items[r][e] as GeneratedFormTagInput)
|
(widget.items[r][e]
|
||||||
.deleteConfirmationMessage!;
|
as GeneratedFormTagInput)
|
||||||
showDialog<Map<String, dynamic>?>(
|
.deleteConfirmationMessage!;
|
||||||
|
showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: message.key,
|
title: message.key,
|
||||||
message: message.value,
|
message: message.value,
|
||||||
items: const []);
|
items: const [],
|
||||||
}).then((value) {
|
);
|
||||||
if (value != null) {
|
},
|
||||||
fn();
|
).then((value) {
|
||||||
}
|
if (value != null) {
|
||||||
});
|
fn();
|
||||||
} else {
|
}
|
||||||
fn();
|
});
|
||||||
}
|
} else {
|
||||||
},
|
fn();
|
||||||
icon: const Icon(Icons.remove),
|
}
|
||||||
visualDensity: VisualDensity.compact,
|
},
|
||||||
tooltip: tr('remove'),
|
icon: const Icon(Icons.remove),
|
||||||
))
|
visualDensity: VisualDensity.compact,
|
||||||
: const SizedBox.shrink(),
|
tooltip: tr('remove'),
|
||||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
),
|
||||||
?.isEmpty ==
|
)
|
||||||
true
|
: const SizedBox.shrink(),
|
||||||
? Padding(
|
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
?.isEmpty ==
|
||||||
child: TextButton.icon(
|
true
|
||||||
onPressed: onAddPressed,
|
? Padding(
|
||||||
icon: const Icon(Icons.add),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
label: Text(
|
child: TextButton.icon(
|
||||||
|
onPressed: onAddPressed,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(
|
||||||
(widget.items[r][e] as GeneratedFormTagInput)
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
.label),
|
.label,
|
||||||
))
|
),
|
||||||
: Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
)
|
||||||
child: IconButton(
|
: Padding(
|
||||||
onPressed: onAddPressed,
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
icon: const Icon(Icons.add),
|
child: IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
onPressed: onAddPressed,
|
||||||
tooltip: tr('add'),
|
icon: const Icon(Icons.add),
|
||||||
)),
|
visualDensity: VisualDensity.compact,
|
||||||
],
|
tooltip: tr('add'),
|
||||||
)
|
),
|
||||||
]);
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
} else if (widget.items[r][e] is GeneratedFormSubForm) {
|
} else if (widget.items[r][e] is GeneratedFormSubForm) {
|
||||||
List<Widget> subformColumn = [];
|
List<Widget> subformColumn = [];
|
||||||
var compact = (widget.items[r][e] as GeneratedFormSubForm)
|
var compact =
|
||||||
.items
|
(widget.items[r][e] as GeneratedFormSubForm).items.length == 1 &&
|
||||||
.length ==
|
|
||||||
1 &&
|
|
||||||
(widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1;
|
(widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1;
|
||||||
for (int i = 0; i < values[fieldKey].length; i++) {
|
for (int i = 0; i < values[fieldKey].length; i++) {
|
||||||
var internalFormKey = ValueKey(generateRandomNumber(
|
var internalFormKey = ValueKey(
|
||||||
|
generateRandomNumber(
|
||||||
values[fieldKey].length,
|
values[fieldKey].length,
|
||||||
seed2: i,
|
seed2: i,
|
||||||
seed3: forceUpdateKeyCount));
|
seed3: forceUpdateKeyCount,
|
||||||
subformColumn.add(Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
);
|
||||||
children: [
|
subformColumn.add(
|
||||||
if (!compact)
|
Column(
|
||||||
const SizedBox(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
height: 16,
|
children: [
|
||||||
|
if (!compact) const SizedBox(height: 16),
|
||||||
|
if (!compact)
|
||||||
|
Text(
|
||||||
|
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
GeneratedForm(
|
||||||
|
key: internalFormKey,
|
||||||
|
items:
|
||||||
|
cloneFormItems(
|
||||||
|
(widget.items[r][e] as GeneratedFormSubForm)
|
||||||
|
.items,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(x) => x.map((y) {
|
||||||
|
y.defaultValue = values[fieldKey]?[i]?[y.key];
|
||||||
|
y.key = '${y.key.toString()},$internalFormKey';
|
||||||
|
return y;
|
||||||
|
}).toList(),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
values = values.map(
|
||||||
|
(key, value) => MapEntry(key.split(',')[0], value),
|
||||||
|
);
|
||||||
|
if (valid) {
|
||||||
|
this.values[fieldKey]?[i] = values;
|
||||||
|
}
|
||||||
|
someValueChanged(
|
||||||
|
isBuilding: isBuilding,
|
||||||
|
forceInvalid: !valid,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (!compact)
|
Row(
|
||||||
Text(
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
children: [
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
TextButton.icon(
|
||||||
),
|
|
||||||
GeneratedForm(
|
|
||||||
key: internalFormKey,
|
|
||||||
items: cloneFormItems(
|
|
||||||
(widget.items[r][e] as GeneratedFormSubForm).items)
|
|
||||||
.map((x) => x.map((y) {
|
|
||||||
y.defaultValue = values[fieldKey]?[i]?[y.key];
|
|
||||||
y.key = '${y.key.toString()},$internalFormKey';
|
|
||||||
return y;
|
|
||||||
}).toList())
|
|
||||||
.toList(),
|
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
|
||||||
values = values.map(
|
|
||||||
(key, value) => MapEntry(key.split(',')[0], value));
|
|
||||||
if (valid) {
|
|
||||||
this.values[fieldKey]?[i] = values;
|
|
||||||
}
|
|
||||||
someValueChanged(
|
|
||||||
isBuilding: isBuilding, forceInvalid: !valid);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton.icon(
|
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
Theme.of(context).colorScheme.error),
|
),
|
||||||
onPressed: (values[fieldKey].length > 0)
|
onPressed: (values[fieldKey].length > 0)
|
||||||
? () {
|
? () {
|
||||||
var temp = List.from(values[fieldKey]);
|
var temp = List.from(values[fieldKey]);
|
||||||
@ -686,33 +761,40 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
label: Text(
|
label: Text(
|
||||||
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
||||||
),
|
),
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.delete_outline_rounded),
|
||||||
Icons.delete_outline_rounded,
|
),
|
||||||
))
|
],
|
||||||
],
|
),
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
subformColumn.add(Padding(
|
subformColumn.add(
|
||||||
padding: const EdgeInsets.only(bottom: 0, top: 8),
|
Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.only(bottom: 0, top: 8),
|
||||||
children: [
|
child: Row(
|
||||||
Expanded(
|
children: [
|
||||||
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
values[fieldKey].add(getDefaultValuesFromFormItems(
|
values[fieldKey].add(
|
||||||
(widget.items[r][e] as GeneratedFormSubForm)
|
getDefaultValuesFromFormItems(
|
||||||
.items));
|
(widget.items[r][e] as GeneratedFormSubForm).items,
|
||||||
forceUpdateKeyCount++;
|
),
|
||||||
someValueChanged();
|
);
|
||||||
},
|
forceUpdateKeyCount++;
|
||||||
icon: const Icon(Icons.add),
|
someValueChanged();
|
||||||
label: Text((widget.items[r][e] as GeneratedFormSubForm)
|
},
|
||||||
.label))),
|
icon: const Icon(Icons.add),
|
||||||
],
|
label: Text(
|
||||||
|
(widget.items[r][e] as GeneratedFormSubForm).label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
formInputs[r][e] = Column(children: subformColumn);
|
formInputs[r][e] = Column(children: subformColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -726,38 +808,43 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
height: widget.items[rowInputs.key - 1][0] is GeneratedFormSwitch
|
height: widget.items[rowInputs.key - 1][0] is GeneratedFormSwitch
|
||||||
? 8
|
? 8
|
||||||
: 25,
|
: 25,
|
||||||
)
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
List<Widget> rowItems = [];
|
List<Widget> rowItems = [];
|
||||||
rowInputs.value.asMap().entries.forEach((rowInput) {
|
rowInputs.value.asMap().entries.forEach((rowInput) {
|
||||||
if (rowInput.key > 0) {
|
if (rowInput.key > 0) {
|
||||||
rowItems.add(const SizedBox(
|
rowItems.add(const SizedBox(width: 20));
|
||||||
width: 20,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
rowItems.add(Expanded(
|
rowItems.add(
|
||||||
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
rowInput.value,
|
rowInput.value,
|
||||||
...widget.items[rowInputs.key][rowInput.key].belowWidgets
|
...widget.items[rowInputs.key][rowInput.key].belowWidgets,
|
||||||
])));
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
rows.add(rowItems);
|
rows.add(rowItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
...rows.map((row) => Row(
|
...rows.map(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
(row) => Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [...row.map((e) => e)],
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
))
|
children: [...row.map((e) => e)],
|
||||||
],
|
),
|
||||||
));
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,16 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
|
||||||
class GeneratedFormModal extends StatefulWidget {
|
class GeneratedFormModal extends StatefulWidget {
|
||||||
const GeneratedFormModal(
|
const GeneratedFormModal({
|
||||||
{super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.items,
|
required this.items,
|
||||||
this.initValid = false,
|
this.initValid = false,
|
||||||
this.message = '',
|
this.message = '',
|
||||||
this.additionalWidgets = const [],
|
this.additionalWidgets = const [],
|
||||||
this.singleNullReturnButton,
|
this.singleNullReturnButton,
|
||||||
this.primaryActionColour});
|
this.primaryActionColour,
|
||||||
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String message;
|
final String message;
|
||||||
@ -41,14 +42,12 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
content:
|
content: Column(
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
if (widget.message.isNotEmpty) Text(widget.message),
|
children: [
|
||||||
if (widget.message.isNotEmpty)
|
if (widget.message.isNotEmpty) Text(widget.message),
|
||||||
const SizedBox(
|
if (widget.message.isNotEmpty) const SizedBox(height: 16),
|
||||||
height: 16,
|
GeneratedForm(
|
||||||
),
|
|
||||||
GeneratedForm(
|
|
||||||
items: widget.items,
|
items: widget.items,
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (isBuilding) {
|
if (isBuilding) {
|
||||||
@ -60,23 +59,29 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
this.valid = valid;
|
this.valid = valid;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets
|
),
|
||||||
]),
|
if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets,
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: Text(widget.singleNullReturnButton == null
|
child: Text(
|
||||||
|
widget.singleNullReturnButton == null
|
||||||
? tr('cancel')
|
? tr('cancel')
|
||||||
: widget.singleNullReturnButton!)),
|
: widget.singleNullReturnButton!,
|
||||||
|
),
|
||||||
|
),
|
||||||
widget.singleNullReturnButton == null
|
widget.singleNullReturnButton == null
|
||||||
? TextButton(
|
? TextButton(
|
||||||
style: widget.primaryActionColour == null
|
style: widget.primaryActionColour == null
|
||||||
? null
|
? null
|
||||||
: TextButton.styleFrom(
|
: TextButton.styleFrom(
|
||||||
foregroundColor: widget.primaryActionColour),
|
foregroundColor: widget.primaryActionColour,
|
||||||
|
),
|
||||||
onPressed: !valid
|
onPressed: !valid
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
@ -85,8 +90,9 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
Navigator.of(context).pop(values);
|
Navigator.of(context).pop(values);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(tr('continue')))
|
child: Text(tr('continue')),
|
||||||
: const SizedBox.shrink()
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,23 +20,24 @@ class ObtainiumError {
|
|||||||
class RateLimitError extends ObtainiumError {
|
class RateLimitError extends ObtainiumError {
|
||||||
late int remainingMinutes;
|
late int remainingMinutes;
|
||||||
RateLimitError(this.remainingMinutes)
|
RateLimitError(this.remainingMinutes)
|
||||||
: super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes));
|
: super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvalidURLError extends ObtainiumError {
|
class InvalidURLError extends ObtainiumError {
|
||||||
InvalidURLError(String sourceName)
|
InvalidURLError(String sourceName)
|
||||||
: super(tr('invalidURLForSource', args: [sourceName]));
|
: super(tr('invalidURLForSource', args: [sourceName]));
|
||||||
}
|
}
|
||||||
|
|
||||||
class CredsNeededError extends ObtainiumError {
|
class CredsNeededError extends ObtainiumError {
|
||||||
CredsNeededError(String sourceName)
|
CredsNeededError(String sourceName)
|
||||||
: super(tr('requiresCredentialsInSettings', args: [sourceName]));
|
: super(tr('requiresCredentialsInSettings', args: [sourceName]));
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoReleasesError extends ObtainiumError {
|
class NoReleasesError extends ObtainiumError {
|
||||||
NoReleasesError({String? note})
|
NoReleasesError({String? note})
|
||||||
: super(
|
: super(
|
||||||
'${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}');
|
'${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoAPKError extends ObtainiumError {
|
class NoAPKError extends ObtainiumError {
|
||||||
@ -52,12 +53,15 @@ class UnsupportedURLError extends ObtainiumError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DowngradeError extends ObtainiumError {
|
class DowngradeError extends ObtainiumError {
|
||||||
DowngradeError() : super(tr('cantInstallOlderVersion'));
|
DowngradeError(int currentVersionCode, int newVersionCode)
|
||||||
|
: super(
|
||||||
|
'${tr('cantInstallOlderVersion')} (versionCode $currentVersionCode ➔ $newVersionCode)',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class InstallError extends ObtainiumError {
|
class InstallError extends ObtainiumError {
|
||||||
InstallError(int code)
|
InstallError(int code)
|
||||||
: super(PackageInstallerStatus.byCode(code).name.substring(7));
|
: super(PackageInstallerStatus.byCode(code).name.substring(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
class IDChangedError extends ObtainiumError {
|
class IDChangedError extends ObtainiumError {
|
||||||
@ -75,7 +79,7 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
|
|
||||||
MultiAppMultiError() : super(tr('placeholder'), unexpected: true);
|
MultiAppMultiError() : super(tr('placeholder'), unexpected: true);
|
||||||
|
|
||||||
add(String appId, dynamic error, {String? appName}) {
|
void add(String appId, dynamic error, {String? appName}) {
|
||||||
if (error is SocketException) {
|
if (error is SocketException) {
|
||||||
error = error.message;
|
error = error.message;
|
||||||
}
|
}
|
||||||
@ -93,8 +97,11 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
String errorString(String appId, {bool includeIdsWithNames = false}) =>
|
String errorString(String appId, {bool includeIdsWithNames = false}) =>
|
||||||
'${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}';
|
'${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}';
|
||||||
|
|
||||||
String errorsAppsString(String errString, List<String> appIds,
|
String errorsAppsString(
|
||||||
{bool includeIdsWithNames = false}) =>
|
String errString,
|
||||||
|
List<String> appIds, {
|
||||||
|
bool includeIdsWithNames = false,
|
||||||
|
}) =>
|
||||||
'$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]';
|
'$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -103,43 +110,50 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage(dynamic e, BuildContext context, {bool isError = false}) {
|
void showMessage(dynamic e, BuildContext context, {bool isError = false}) {
|
||||||
Provider.of<LogsProvider>(context, listen: false)
|
Provider.of<LogsProvider>(
|
||||||
.add(e.toString(), level: isError ? LogLevels.error : LogLevels.info);
|
context,
|
||||||
|
listen: false,
|
||||||
|
).add(e.toString(), level: isError ? LogLevels.error : LogLevels.info);
|
||||||
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(e.toString())),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
} else {
|
} else {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(e is MultiAppMultiError
|
title: Text(
|
||||||
|
e is MultiAppMultiError
|
||||||
? tr(isError ? 'someErrors' : 'updates')
|
? tr(isError ? 'someErrors' : 'updates')
|
||||||
: tr(isError ? 'unexpectedError' : 'unknown')),
|
: tr(isError ? 'unexpectedError' : 'unknown'),
|
||||||
content: GestureDetector(
|
),
|
||||||
onLongPress: () {
|
content: GestureDetector(
|
||||||
Clipboard.setData(ClipboardData(text: e.toString()));
|
onLongPress: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
Clipboard.setData(ClipboardData(text: e.toString()));
|
||||||
content: Text(tr('copiedToClipboard')),
|
ScaffoldMessenger.of(
|
||||||
));
|
context,
|
||||||
},
|
).showSnackBar(SnackBar(content: Text(tr('copiedToClipboard'))));
|
||||||
child: Text(e.toString())),
|
},
|
||||||
actions: [
|
child: Text(e.toString()),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () {
|
actions: [
|
||||||
Navigator.of(context).pop(null);
|
TextButton(
|
||||||
},
|
onPressed: () {
|
||||||
child: Text(tr('ok'))),
|
Navigator.of(context).pop(null);
|
||||||
],
|
},
|
||||||
);
|
child: Text(tr('ok')),
|
||||||
});
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showError(dynamic e, BuildContext context) {
|
void showError(dynamic e, BuildContext context) {
|
||||||
showMessage(e, context, isError: true);
|
showMessage(e, context, isError: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,14 +161,16 @@ String list2FriendlyString(List<String> list) {
|
|||||||
return list.length == 2
|
return list.length == 2
|
||||||
? '${list[0]} ${tr('and')} ${list[1]}'
|
? '${list[0]} ${tr('and')} ${list[1]}'
|
||||||
: list
|
: list
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.map((e) =>
|
.map(
|
||||||
e.value +
|
(e) =>
|
||||||
(e.key == list.length - 1
|
e.value +
|
||||||
? ''
|
(e.key == list.length - 1
|
||||||
: e.key == list.length - 2
|
? ''
|
||||||
|
: e.key == list.length - 2
|
||||||
? ' and '
|
? ' and '
|
||||||
: ', '))
|
: ', '),
|
||||||
.join('');
|
)
|
||||||
|
.join('');
|
||||||
}
|
}
|
||||||
|
349
lib/main.dart
349
lib/main.dart
@ -9,7 +9,6 @@ import 'package:obtainium/providers/native_provider.dart';
|
|||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_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:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
@ -19,6 +18,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:easy_localization/src/easy_localization_controller.dart';
|
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';
|
||||||
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
|
|
||||||
List<MapEntry<Locale, String>> supportedLocales = const [
|
List<MapEntry<Locale, String>> supportedLocales = const [
|
||||||
MapEntry(Locale('en'), 'English'),
|
MapEntry(Locale('en'), 'English'),
|
||||||
@ -43,12 +43,15 @@ List<MapEntry<Locale, String>> supportedLocales = const [
|
|||||||
MapEntry(Locale('tr'), 'Türkçe'),
|
MapEntry(Locale('tr'), 'Türkçe'),
|
||||||
MapEntry(Locale('uk'), 'Українська'),
|
MapEntry(Locale('uk'), 'Українська'),
|
||||||
MapEntry(Locale('da'), 'Dansk'),
|
MapEntry(Locale('da'), 'Dansk'),
|
||||||
MapEntry(Locale('en', 'EO'),
|
MapEntry(
|
||||||
'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
|
Locale('en', 'EO'),
|
||||||
|
'Esperanto',
|
||||||
|
), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
|
||||||
MapEntry(Locale('in'), 'Bahasa Indonesia'),
|
MapEntry(Locale('in'), 'Bahasa Indonesia'),
|
||||||
MapEntry(Locale('ko'), '한국어'),
|
MapEntry(Locale('ko'), '한국어'),
|
||||||
MapEntry(Locale('ca'), 'Català'),
|
MapEntry(Locale('ca'), 'Català'),
|
||||||
MapEntry(Locale('ar'), 'العربية'),
|
MapEntry(Locale('ar'), 'العربية'),
|
||||||
|
MapEntry(Locale('ml'), 'മലയാളം'),
|
||||||
];
|
];
|
||||||
const fallbackLocale = Locale('en');
|
const fallbackLocale = Locale('en');
|
||||||
const localeDir = 'assets/translations';
|
const localeDir = 'assets/translations';
|
||||||
@ -76,9 +79,11 @@ Future<void> loadTranslations() async {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
await controller.loadTranslations();
|
await controller.loadTranslations();
|
||||||
Localization.load(controller.locale,
|
Localization.load(
|
||||||
translations: controller.translations,
|
controller.locale,
|
||||||
fallbackTranslations: controller.fallbackTranslations);
|
translations: controller.translations,
|
||||||
|
fallbackTranslations: controller.fallbackTranslations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
@ -94,13 +99,43 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async {
|
|||||||
BackgroundFetch.finish(taskId);
|
BackgroundFetch.finish(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
void startCallback() {
|
||||||
|
FlutterForegroundTask.setTaskHandler(MyTaskHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyTaskHandler extends TaskHandler {
|
||||||
|
static const String incrementCountCommand = 'incrementCount';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
|
||||||
|
print('onStart(starter: ${starter.name})');
|
||||||
|
bgUpdateCheck('bg_check', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRepeatEvent(DateTime timestamp) {
|
||||||
|
bgUpdateCheck('bg_check', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
|
||||||
|
print('Foreground service onDestroy(isTimeout: $isTimeout)');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onReceiveData(Object data) {}
|
||||||
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
try {
|
try {
|
||||||
ByteData data =
|
ByteData data = await PlatformAssetBundle().load(
|
||||||
await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
|
'assets/ca/lets-encrypt-r3.pem',
|
||||||
SecurityContext.defaultContext
|
);
|
||||||
.setTrustedCertificatesBytes(data.buffer.asUint8List());
|
SecurityContext.defaultContext.setTrustedCertificatesBytes(
|
||||||
|
data.buffer.asUint8List(),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Already added, do nothing (see #375)
|
// Already added, do nothing (see #375)
|
||||||
}
|
}
|
||||||
@ -113,20 +148,24 @@ void main() async {
|
|||||||
}
|
}
|
||||||
final np = NotificationsProvider();
|
final np = NotificationsProvider();
|
||||||
await np.initialize();
|
await np.initialize();
|
||||||
runApp(MultiProvider(
|
FlutterForegroundTask.initCommunicationPort();
|
||||||
providers: [
|
runApp(
|
||||||
ChangeNotifierProvider(create: (context) => AppsProvider()),
|
MultiProvider(
|
||||||
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
providers: [
|
||||||
Provider(create: (context) => np),
|
ChangeNotifierProvider(create: (context) => AppsProvider()),
|
||||||
Provider(create: (context) => LogsProvider())
|
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
||||||
],
|
Provider(create: (context) => np),
|
||||||
child: EasyLocalization(
|
Provider(create: (context) => LogsProvider()),
|
||||||
|
],
|
||||||
|
child: EasyLocalization(
|
||||||
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||||
path: localeDir,
|
path: localeDir,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
useOnlyLangCode: false,
|
useOnlyLangCode: false,
|
||||||
child: const Obtainium()),
|
child: const Obtainium(),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
|
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,26 +183,104 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initPlatformState();
|
initPlatformState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
requestNonOptionalPermissions();
|
||||||
|
initForegroundService();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> requestNonOptionalPermissions() async {
|
||||||
|
final NotificationPermission notificationPermission =
|
||||||
|
await FlutterForegroundTask.checkNotificationPermission();
|
||||||
|
if (notificationPermission != NotificationPermission.granted) {
|
||||||
|
await FlutterForegroundTask.requestNotificationPermission();
|
||||||
|
}
|
||||||
|
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
|
||||||
|
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initForegroundService() {
|
||||||
|
FlutterForegroundTask.init(
|
||||||
|
androidNotificationOptions: AndroidNotificationOptions(
|
||||||
|
channelId: 'bg_update',
|
||||||
|
channelName: tr('foregroundService'),
|
||||||
|
channelDescription: tr('foregroundService'),
|
||||||
|
onlyAlertOnce: true,
|
||||||
|
),
|
||||||
|
iosNotificationOptions: const IOSNotificationOptions(
|
||||||
|
showNotification: false,
|
||||||
|
playSound: false,
|
||||||
|
),
|
||||||
|
foregroundTaskOptions: ForegroundTaskOptions(
|
||||||
|
eventAction: ForegroundTaskEventAction.repeat(900000),
|
||||||
|
autoRunOnBoot: true,
|
||||||
|
autoRunOnMyPackageReplaced: true,
|
||||||
|
allowWakeLock: true,
|
||||||
|
allowWifiLock: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServiceRequestResult?> startForegroundService(bool restart) async {
|
||||||
|
if (await FlutterForegroundTask.isRunningService) {
|
||||||
|
if (restart) {
|
||||||
|
return FlutterForegroundTask.restartService();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return FlutterForegroundTask.startService(
|
||||||
|
serviceTypes: [ForegroundServiceTypes.specialUse],
|
||||||
|
serviceId: 666,
|
||||||
|
notificationTitle: tr('foregroundService'),
|
||||||
|
notificationText: tr('fgServiceNotice'),
|
||||||
|
notificationIcon: NotificationIcon(
|
||||||
|
metaDataName: 'dev.imranr.obtainium.service.NOTIFICATION_ICON',
|
||||||
|
),
|
||||||
|
callback: startCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopForegroundService() async {
|
||||||
|
if (await FlutterForegroundTask.isRunningService) {
|
||||||
|
return FlutterForegroundTask.stopService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// void onReceiveForegroundServiceData(Object data) {
|
||||||
|
// print('onReceiveTaskData: $data');
|
||||||
|
// }
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// Remove a callback to receive data sent from the TaskHandler.
|
||||||
|
// FlutterForegroundTask.removeTaskDataCallback(onReceiveForegroundServiceData);
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initPlatformState() async {
|
Future<void> initPlatformState() async {
|
||||||
await BackgroundFetch.configure(
|
await BackgroundFetch.configure(
|
||||||
BackgroundFetchConfig(
|
BackgroundFetchConfig(
|
||||||
minimumFetchInterval: 15,
|
minimumFetchInterval: 15,
|
||||||
stopOnTerminate: false,
|
stopOnTerminate: false,
|
||||||
startOnBoot: true,
|
startOnBoot: true,
|
||||||
enableHeadless: true,
|
enableHeadless: true,
|
||||||
requiresBatteryNotLow: false,
|
requiresBatteryNotLow: false,
|
||||||
requiresCharging: false,
|
requiresCharging: false,
|
||||||
requiresStorageNotLow: false,
|
requiresStorageNotLow: false,
|
||||||
requiresDeviceIdle: false,
|
requiresDeviceIdle: false,
|
||||||
requiredNetworkType: NetworkType.ANY), (String taskId) async {
|
requiredNetworkType: NetworkType.ANY,
|
||||||
await bgUpdateCheck(taskId, null);
|
),
|
||||||
BackgroundFetch.finish(taskId);
|
(String taskId) async {
|
||||||
}, (String taskId) async {
|
await bgUpdateCheck(taskId, null);
|
||||||
context.read<LogsProvider>().add('BG update task timed out.');
|
BackgroundFetch.finish(taskId);
|
||||||
BackgroundFetch.finish(taskId);
|
},
|
||||||
});
|
(String taskId) async {
|
||||||
|
context.read<LogsProvider>().add('BG update task timed out.');
|
||||||
|
BackgroundFetch.finish(taskId);
|
||||||
|
},
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,40 +290,53 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
LogsProvider logs = context.read<LogsProvider>();
|
LogsProvider logs = context.read<LogsProvider>();
|
||||||
NotificationsProvider notifs = context.read<NotificationsProvider>();
|
NotificationsProvider notifs = context.read<NotificationsProvider>();
|
||||||
|
if (settingsProvider.updateInterval == 0) {
|
||||||
|
stopForegroundService();
|
||||||
|
BackgroundFetch.stop();
|
||||||
|
} else {
|
||||||
|
if (settingsProvider.useFGService) {
|
||||||
|
BackgroundFetch.stop();
|
||||||
|
startForegroundService(false);
|
||||||
|
} else {
|
||||||
|
stopForegroundService();
|
||||||
|
BackgroundFetch.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (settingsProvider.prefs == null) {
|
if (settingsProvider.prefs == null) {
|
||||||
settingsProvider.initializeSettings();
|
settingsProvider.initializeSettings();
|
||||||
} else {
|
} else {
|
||||||
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
logs.add('This is the first ever run of Obtainium.');
|
logs.add('This is the first ever run of Obtainium.');
|
||||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
// If this is the first run, add Obtainium to the Apps list
|
||||||
Permission.notification.request();
|
|
||||||
if (!fdroid) {
|
if (!fdroid) {
|
||||||
getInstalledInfo(obtainiumId).then((value) {
|
getInstalledInfo(obtainiumId)
|
||||||
if (value?.versionName != null) {
|
.then((value) {
|
||||||
appsProvider.saveApps([
|
if (value?.versionName != null) {
|
||||||
App(
|
appsProvider.saveApps([
|
||||||
obtainiumId,
|
App(
|
||||||
obtainiumUrl,
|
obtainiumId,
|
||||||
'ImranR98',
|
obtainiumUrl,
|
||||||
'Obtainium',
|
'ImranR98',
|
||||||
value!.versionName,
|
'Obtainium',
|
||||||
value.versionName!,
|
value!.versionName,
|
||||||
[],
|
value.versionName!,
|
||||||
0,
|
[],
|
||||||
{
|
0,
|
||||||
'versionDetection': true,
|
{
|
||||||
'apkFilterRegEx': 'fdroid',
|
'versionDetection': true,
|
||||||
'invertAPKFilter': true
|
'apkFilterRegEx': 'fdroid',
|
||||||
},
|
'invertAPKFilter': true,
|
||||||
null,
|
},
|
||||||
false)
|
null,
|
||||||
], onlyIfExists: false);
|
false,
|
||||||
}
|
),
|
||||||
}).catchError((err) {
|
], onlyIfExists: false);
|
||||||
print(err);
|
}
|
||||||
});
|
})
|
||||||
|
.catchError((err) {
|
||||||
|
print(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!supportedLocales.map((e) => e.key).contains(context.locale) ||
|
if (!supportedLocales.map((e) => e.key).contains(context.locale) ||
|
||||||
@ -220,56 +350,71 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
notifs.checkLaunchByNotif();
|
notifs.checkLaunchByNotif();
|
||||||
});
|
});
|
||||||
|
|
||||||
return DynamicColorBuilder(
|
return WithForegroundTask(
|
||||||
|
child: DynamicColorBuilder(
|
||||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||||
// Decide on a colour/brightness scheme based on OS and user settings
|
// Decide on a colour/brightness scheme based on OS and user settings
|
||||||
ColorScheme lightColorScheme;
|
ColorScheme lightColorScheme;
|
||||||
ColorScheme darkColorScheme;
|
ColorScheme darkColorScheme;
|
||||||
if (lightDynamic != null &&
|
if (lightDynamic != null &&
|
||||||
darkDynamic != null &&
|
darkDynamic != null &&
|
||||||
settingsProvider.useMaterialYou) {
|
settingsProvider.useMaterialYou) {
|
||||||
lightColorScheme = lightDynamic.harmonized();
|
lightColorScheme = lightDynamic.harmonized();
|
||||||
darkColorScheme = darkDynamic.harmonized();
|
darkColorScheme = darkDynamic.harmonized();
|
||||||
} else {
|
} else {
|
||||||
lightColorScheme =
|
lightColorScheme = ColorScheme.fromSeed(
|
||||||
ColorScheme.fromSeed(seedColor: settingsProvider.themeColor);
|
seedColor: settingsProvider.themeColor,
|
||||||
darkColorScheme = ColorScheme.fromSeed(
|
);
|
||||||
seedColor: settingsProvider.themeColor,
|
darkColorScheme = ColorScheme.fromSeed(
|
||||||
brightness: Brightness.dark);
|
seedColor: settingsProvider.themeColor,
|
||||||
}
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// set the background and surface colors to pure black in the amoled theme
|
// set the background and surface colors to pure black in the amoled theme
|
||||||
if (settingsProvider.useBlackTheme) {
|
if (settingsProvider.useBlackTheme) {
|
||||||
darkColorScheme =
|
darkColorScheme = darkColorScheme
|
||||||
darkColorScheme.copyWith(surface: Colors.black).harmonized();
|
.copyWith(surface: Colors.black)
|
||||||
}
|
.harmonized();
|
||||||
|
}
|
||||||
|
|
||||||
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
|
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Obtainium',
|
title: 'Obtainium',
|
||||||
localizationsDelegates: context.localizationDelegates,
|
localizationsDelegates: context.localizationDelegates,
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
locale: context.locale,
|
locale: context.locale,
|
||||||
navigatorKey: globalNavigatorKey,
|
navigatorKey: globalNavigatorKey,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: settingsProvider.theme == ThemeSettings.dark
|
colorScheme: settingsProvider.theme == ThemeSettings.dark
|
||||||
? darkColorScheme
|
? darkColorScheme
|
||||||
: lightColorScheme,
|
: lightColorScheme,
|
||||||
fontFamily:
|
fontFamily: settingsProvider.useSystemFont
|
||||||
settingsProvider.useSystemFont ? 'SystemFont' : 'Montserrat'),
|
? 'SystemFont'
|
||||||
darkTheme: ThemeData(
|
: 'Montserrat',
|
||||||
|
),
|
||||||
|
darkTheme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: settingsProvider.theme == ThemeSettings.light
|
colorScheme: settingsProvider.theme == ThemeSettings.light
|
||||||
? lightColorScheme
|
? lightColorScheme
|
||||||
: darkColorScheme,
|
: darkColorScheme,
|
||||||
fontFamily:
|
fontFamily: settingsProvider.useSystemFont
|
||||||
settingsProvider.useSystemFont ? 'SystemFont' : 'Montserrat'),
|
? 'SystemFont'
|
||||||
home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{
|
: 'Montserrat',
|
||||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
),
|
||||||
}, child: const HomePage()));
|
home: Shortcuts(
|
||||||
});
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.select):
|
||||||
|
const ActivateIntent(),
|
||||||
|
},
|
||||||
|
child: const HomePage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,4 +3,4 @@ import 'main.dart' as m;
|
|||||||
void main() async {
|
void main() async {
|
||||||
m.fdroid = true;
|
m.fdroid = true;
|
||||||
m.main();
|
m.main();
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,15 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
late List<String> requiredArgs = [tr('uname')];
|
late List<String> requiredArgs = [tr('uname')];
|
||||||
|
|
||||||
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
String username, int page) async {
|
String username,
|
||||||
|
int page,
|
||||||
|
) async {
|
||||||
Response res = await get(
|
Response res = await get(
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
|
'https://api.github.com/users/$username/starred?per_page=100&page=$page',
|
||||||
headers: await GitHub().getRequestHeaders({}));
|
),
|
||||||
|
headers: await GitHub().getRequestHeaders({}),
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<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>)) {
|
||||||
@ -27,8 +31,8 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
e['full_name'] as String,
|
e['full_name'] as String,
|
||||||
e['description'] != null
|
e['description'] != null
|
||||||
? e['description'] as String
|
? e['description'] as String
|
||||||
: tr('noDescription')
|
: tr('noDescription'),
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return urlsWithDescriptions;
|
return urlsWithDescriptions;
|
||||||
@ -41,15 +45,18 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> getUrlsWithDescriptions(
|
Future<Map<String, List<String>>> getUrlsWithDescriptions(
|
||||||
List<String> args) async {
|
List<String> args,
|
||||||
|
) async {
|
||||||
if (args.length != requiredArgs.length) {
|
if (args.length != requiredArgs.length) {
|
||||||
throw ObtainiumError(tr('wrongArgNum'));
|
throw ObtainiumError(tr('wrongArgNum'));
|
||||||
}
|
}
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
var page = 1;
|
var page = 1;
|
||||||
while (true) {
|
while (true) {
|
||||||
var pageUrls =
|
var pageUrls = await getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
await getOnePageOfUserStarredUrlsWithDescriptions(args[0], page++);
|
args[0],
|
||||||
|
page++,
|
||||||
|
);
|
||||||
urlsWithDescriptions.addAll(pageUrls);
|
urlsWithDescriptions.addAll(pageUrls);
|
||||||
if (pageUrls.length < 100) {
|
if (pageUrls.length < 100) {
|
||||||
break;
|
break;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -40,13 +40,18 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onWebResourceError: (WebResourceError error) {
|
onWebResourceError: (WebResourceError error) {
|
||||||
if (error.isForMainFrame == true) {
|
if (error.isForMainFrame == true) {
|
||||||
showError(
|
showError(
|
||||||
ObtainiumError(error.description, unexpected: true), context);
|
ObtainiumError(error.description, unexpected: true),
|
||||||
|
context,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigationRequest: (NavigationRequest request) =>
|
onNavigationRequest: (NavigationRequest request) =>
|
||||||
request.url.startsWith("rustore://")
|
!(request.url.startsWith("http://") ||
|
||||||
? NavigationDecision.prevent
|
request.url.startsWith("https://") ||
|
||||||
: NavigationDecision.navigate,
|
request.url.startsWith("ftp://") ||
|
||||||
|
request.url.startsWith("ftps://"))
|
||||||
|
? NavigationDecision.prevent
|
||||||
|
: NavigationDecision.navigate,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -85,8 +90,10 @@ class _AppPageState extends State<AppPage> {
|
|||||||
var sourceProvider = SourceProvider();
|
var sourceProvider = SourceProvider();
|
||||||
AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy();
|
AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy();
|
||||||
var source = app != null
|
var source = app != null
|
||||||
? sourceProvider.getSource(app.app.url,
|
? sourceProvider.getSource(
|
||||||
overrideSource: app.app.overrideSource)
|
app.app.url,
|
||||||
|
overrideSource: app.app.overrideSource,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
if (!areDownloadsRunning &&
|
if (!areDownloadsRunning &&
|
||||||
prevApp == null &&
|
prevApp == null &&
|
||||||
@ -100,7 +107,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
bool isVersionDetectionStandard =
|
bool isVersionDetectionStandard =
|
||||||
app?.app.additionalSettings['versionDetection'] == true;
|
app?.app.additionalSettings['versionDetection'] == true;
|
||||||
|
|
||||||
bool installedVersionIsEstimate = app?.app != null ? isVersionPseudo(app!.app) : false;
|
bool installedVersionIsEstimate = app?.app != null
|
||||||
|
? isVersionPseudo(app!.app)
|
||||||
|
: false;
|
||||||
|
|
||||||
if (app != null && !_wasWebViewOpened) {
|
if (app != null && !_wasWebViewOpened) {
|
||||||
_wasWebViewOpened = true;
|
_wasWebViewOpened = true;
|
||||||
@ -122,11 +131,14 @@ class _AppPageState extends State<AppPage> {
|
|||||||
if (!upToDate) {
|
if (!upToDate) {
|
||||||
versionLines += '\n${app?.app.latestVersion} ${tr('latest')}';
|
versionLines += '\n${app?.app.latestVersion} ${tr('latest')}';
|
||||||
}
|
}
|
||||||
String infoLines = tr('lastUpdateCheckX', args: [
|
String infoLines = tr(
|
||||||
app?.app.lastUpdateCheck == null
|
'lastUpdateCheckX',
|
||||||
? tr('never')
|
args: [
|
||||||
: '${app?.app.lastUpdateCheck?.toLocal()}'
|
app?.app.lastUpdateCheck == null
|
||||||
]);
|
? tr('never')
|
||||||
|
: '${app?.app.lastUpdateCheck?.toLocal()}',
|
||||||
|
],
|
||||||
|
);
|
||||||
if (trackOnly) {
|
if (trackOnly) {
|
||||||
infoLines = '${tr('xIsTrackOnly', args: [tr('app')])}\n$infoLines';
|
infoLines = '${tr('xIsTrackOnly', args: [tr('app')])}\n$infoLines';
|
||||||
}
|
}
|
||||||
@ -146,15 +158,14 @@ class _AppPageState extends State<AppPage> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
Text(
|
||||||
|
versionLines,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(versionLines,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyLarge!
|
|
||||||
.copyWith(fontWeight: FontWeight.bold)),
|
|
||||||
changeLogFn != null || app?.app.releaseDate != null
|
changeLogFn != null || app?.app.releaseDate != null
|
||||||
? GestureDetector(
|
? GestureDetector(
|
||||||
onTap: changeLogFn,
|
onTap: changeLogFn,
|
||||||
@ -163,21 +174,19 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? tr('changes')
|
? tr('changes')
|
||||||
: app!.app.releaseDate!.toLocal().toString(),
|
: app!.app.releaseDate!.toLocal().toString(),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style: Theme.of(context).textTheme.labelSmall!
|
||||||
Theme.of(context).textTheme.labelSmall!.copyWith(
|
.copyWith(
|
||||||
decoration: changeLogFn != null
|
decoration: changeLogFn != null
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: null,
|
: null,
|
||||||
fontStyle: changeLogFn != null
|
fontStyle: changeLogFn != null
|
||||||
? FontStyle.italic
|
? FontStyle.italic
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -189,101 +198,108 @@ class _AppPageState extends State<AppPage> {
|
|||||||
if (app?.app.apkUrls.isNotEmpty == true ||
|
if (app?.app.apkUrls.isNotEmpty == true ||
|
||||||
app?.app.otherAssetUrls.isNotEmpty == true)
|
app?.app.otherAssetUrls.isNotEmpty == true)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: app?.app == null || updating
|
onTap: app?.app == null || updating
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
try {
|
try {
|
||||||
await appsProvider
|
await appsProvider.downloadAppAssets([
|
||||||
.downloadAppAssets([app!.app.id], context);
|
app!.app.id,
|
||||||
} catch (e) {
|
], context);
|
||||||
showError(e, context);
|
} catch (e) {
|
||||||
}
|
showError(e, context);
|
||||||
},
|
}
|
||||||
child: Row(
|
},
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Container(
|
children: [
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
borderRadius: BorderRadius.circular(12),
|
decoration: BoxDecoration(
|
||||||
color: settingsProvider.highlightTouchTargets
|
borderRadius: BorderRadius.circular(12),
|
||||||
? (Theme.of(context).brightness ==
|
color: settingsProvider.highlightTouchTargets
|
||||||
Brightness.light
|
? (Theme.of(context).brightness == Brightness.light
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: Theme.of(context).primaryColorLight)
|
: Theme.of(context).primaryColorLight)
|
||||||
.withAlpha(Theme.of(context).brightness ==
|
.withAlpha(
|
||||||
Brightness.light
|
Theme.of(context).brightness ==
|
||||||
? 20
|
Brightness.light
|
||||||
: 40)
|
? 20
|
||||||
: null),
|
: 40,
|
||||||
padding: settingsProvider.highlightTouchTargets
|
)
|
||||||
? const EdgeInsetsDirectional.fromSTEB(12, 6, 12, 6)
|
: null,
|
||||||
: const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 6),
|
),
|
||||||
margin:
|
padding: settingsProvider.highlightTouchTargets
|
||||||
const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 0),
|
? const EdgeInsetsDirectional.fromSTEB(12, 6, 12, 6)
|
||||||
child: Text(
|
: const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 6),
|
||||||
tr('downloadX',
|
margin: const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 0),
|
||||||
args: [tr('releaseAsset').toLowerCase()]),
|
child: Text(
|
||||||
textAlign: TextAlign.center,
|
tr('downloadX', args: [tr('releaseAsset').toLowerCase()]),
|
||||||
style:
|
textAlign: TextAlign.center,
|
||||||
Theme.of(context).textTheme.labelSmall!.copyWith(
|
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
))
|
),
|
||||||
],
|
),
|
||||||
)),
|
],
|
||||||
const SizedBox(
|
),
|
||||||
height: 48,
|
),
|
||||||
),
|
const SizedBox(height: 48),
|
||||||
CategoryEditorSelector(
|
CategoryEditorSelector(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
preselected: app?.app.categories != null
|
preselected: app?.app.categories != null
|
||||||
? app!.app.categories.toSet()
|
? app!.app.categories.toSet()
|
||||||
: {},
|
: {},
|
||||||
onSelected: (categories) {
|
onSelected: (categories) {
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
app.app.categories = categories;
|
app.app.categories = categories;
|
||||||
appsProvider.saveApps([app.app]);
|
appsProvider.saveApps([app.app]);
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
if (app?.app.additionalSettings['about'] is String &&
|
if (app?.app.additionalSettings['about'] is String &&
|
||||||
app?.app.additionalSettings['about'].isNotEmpty)
|
app?.app.additionalSettings['about'].isNotEmpty)
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 48),
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
Clipboard.setData(ClipboardData(
|
Clipboard.setData(
|
||||||
text: app?.app.additionalSettings['about'] ?? ''));
|
ClipboardData(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
text: app?.app.additionalSettings['about'] ?? '',
|
||||||
content: Text(tr('copiedToClipboard')),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: Markdown(
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
shrinkWrap: true,
|
|
||||||
styleSheet: MarkdownStyleSheet(
|
|
||||||
blockquoteDecoration:
|
|
||||||
BoxDecoration(color: Theme.of(context).cardColor),
|
|
||||||
textAlign: WrapAlignment.center),
|
|
||||||
data: app?.app.additionalSettings['about'],
|
|
||||||
onTapLink: (text, href, title) {
|
|
||||||
if (href != null) {
|
|
||||||
launchUrlString(href,
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extensionSet: md.ExtensionSet(
|
|
||||||
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
|
||||||
[
|
|
||||||
md.EmojiSyntax(),
|
|
||||||
...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
))
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(tr('copiedToClipboard'))),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Markdown(
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
styleSheet: MarkdownStyleSheet(
|
||||||
|
blockquoteDecoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
),
|
||||||
|
textAlign: WrapAlignment.center,
|
||||||
|
),
|
||||||
|
data: app?.app.additionalSettings['about'],
|
||||||
|
onTapLink: (text, href, title) {
|
||||||
|
if (href != null) {
|
||||||
|
launchUrlString(
|
||||||
|
href,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extensionSet: md.ExtensionSet(
|
||||||
|
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||||
|
[
|
||||||
|
md.EmojiSyntax(),
|
||||||
|
...md.ExtensionSet.gitHubFlavored.inlineSyntaxes,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -291,132 +307,143 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFullInfoColumn({bool small = false}) => Column(
|
getFullInfoColumn({bool small = false}) => Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: small ? 5 : 20),
|
SizedBox(height: small ? 5 : 20),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future:
|
future: appsProvider.updateAppIcon(app?.app.id, ignoreCache: true),
|
||||||
appsProvider.updateAppIcon(app?.app.id, ignoreCache: true),
|
builder: (ctx, val) {
|
||||||
builder: (ctx, val) {
|
return app?.icon != null
|
||||||
return app?.icon != null
|
? Row(
|
||||||
? Row(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
GestureDetector(
|
||||||
GestureDetector(
|
onTap: app == null
|
||||||
onTap: app == null
|
? null
|
||||||
? null
|
: () => pm.openApp(app.app.id),
|
||||||
: () => pm.openApp(app.app.id),
|
child: Image.memory(
|
||||||
child: Image.memory(
|
app!.icon!,
|
||||||
app!.icon!,
|
height: small ? 70 : 150,
|
||||||
height: small ? 70 : 150,
|
gaplessPlayback: true,
|
||||||
gaplessPlayback: true,
|
),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
])
|
)
|
||||||
: Container();
|
: Container();
|
||||||
}),
|
},
|
||||||
SizedBox(
|
),
|
||||||
height: small ? 10 : 25,
|
SizedBox(height: small ? 10 : 25),
|
||||||
|
Text(
|
||||||
|
app?.name ?? tr('app'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: small
|
||||||
|
? Theme.of(context).textTheme.displaySmall
|
||||||
|
: Theme.of(context).textTheme.displayLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tr('byX', args: [app?.author ?? tr('unknown')]),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: small
|
||||||
|
? Theme.of(context).textTheme.headlineSmall
|
||||||
|
: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (app?.app.url != null) {
|
||||||
|
launchUrlString(
|
||||||
|
app?.app.url ?? '',
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(tr('copiedToClipboard'))));
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
app?.app.url ?? '',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
app?.name ?? tr('app'),
|
),
|
||||||
textAlign: TextAlign.center,
|
Text(
|
||||||
style: small
|
app?.app.id ?? '',
|
||||||
? Theme.of(context).textTheme.displaySmall
|
textAlign: TextAlign.center,
|
||||||
: Theme.of(context).textTheme.displayLarge,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
Text(tr('byX', args: [app?.author ?? tr('unknown')]),
|
getInfoColumn(),
|
||||||
textAlign: TextAlign.center,
|
const SizedBox(height: 150),
|
||||||
style: small
|
],
|
||||||
? Theme.of(context).textTheme.headlineSmall
|
);
|
||||||
: Theme.of(context).textTheme.headlineMedium),
|
|
||||||
const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
if (app?.app.url != null) {
|
|
||||||
launchUrlString(app?.app.url ?? '',
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text(tr('copiedToClipboard')),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
app?.app.url ?? '',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
fontStyle: FontStyle.italic),
|
|
||||||
)),
|
|
||||||
Text(
|
|
||||||
app?.app.id ?? '',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
|
||||||
),
|
|
||||||
getInfoColumn(),
|
|
||||||
const SizedBox(height: 150)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
getAppWebView() => app != null
|
getAppWebView() => app != null
|
||||||
? WebViewWidget(
|
? WebViewWidget(
|
||||||
key: ObjectKey(_webViewController),
|
key: ObjectKey(_webViewController),
|
||||||
controller: _webViewController
|
controller: _webViewController
|
||||||
..setBackgroundColor(Theme.of(context).colorScheme.surface))
|
..setBackgroundColor(Theme.of(context).colorScheme.surface),
|
||||||
|
)
|
||||||
: Container();
|
: Container();
|
||||||
|
|
||||||
showMarkUpdatedDialog() {
|
showMarkUpdatedDialog() {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(tr('alreadyUpToDateQuestion')),
|
title: Text(tr('alreadyUpToDateQuestion')),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(tr('no'))),
|
child: Text(tr('no')),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () {
|
TextButton(
|
||||||
HapticFeedback.selectionClick();
|
onPressed: () {
|
||||||
var updatedApp = app?.app;
|
HapticFeedback.selectionClick();
|
||||||
if (updatedApp != null) {
|
var updatedApp = app?.app;
|
||||||
updatedApp.installedVersion = updatedApp.latestVersion;
|
if (updatedApp != null) {
|
||||||
appsProvider.saveApps([updatedApp]);
|
updatedApp.installedVersion = updatedApp.latestVersion;
|
||||||
}
|
appsProvider.saveApps([updatedApp]);
|
||||||
Navigator.of(context).pop();
|
}
|
||||||
},
|
Navigator.of(context).pop();
|
||||||
child: Text(tr('yesMarkUpdated')))
|
},
|
||||||
],
|
child: Text(tr('yesMarkUpdated')),
|
||||||
);
|
),
|
||||||
});
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAdditionalOptionsDialog() async {
|
showAdditionalOptionsDialog() async {
|
||||||
return await showDialog<Map<String, dynamic>?>(
|
return await showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
var items =
|
var items = (source?.combinedAppSpecificSettingFormItems ?? []).map((
|
||||||
(source?.combinedAppSpecificSettingFormItems ?? []).map((row) {
|
row,
|
||||||
row = row.map((e) {
|
) {
|
||||||
if (app?.app.additionalSettings[e.key] != null) {
|
row = row.map((e) {
|
||||||
e.defaultValue = app?.app.additionalSettings[e.key];
|
if (app?.app.additionalSettings[e.key] != null) {
|
||||||
}
|
e.defaultValue = app?.app.additionalSettings[e.key];
|
||||||
return e;
|
}
|
||||||
}).toList();
|
return e;
|
||||||
return row;
|
|
||||||
}).toList();
|
}).toList();
|
||||||
|
return row;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('additionalOptions'), items: items);
|
title: tr('additionalOptions'),
|
||||||
});
|
items: items,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAdditionalOptionChanges(Map<String, dynamic>? values) {
|
handleAdditionalOptionChanges(Map<String, dynamic>? values) {
|
||||||
@ -430,18 +457,18 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
var versionDetectionEnabled =
|
var versionDetectionEnabled =
|
||||||
app.app.additionalSettings['versionDetection'] == true &&
|
app.app.additionalSettings['versionDetection'] == true &&
|
||||||
originalSettings['versionDetection'] != true;
|
originalSettings['versionDetection'] != true;
|
||||||
var releaseDateVersionEnabled =
|
var releaseDateVersionEnabled =
|
||||||
app.app.additionalSettings['releaseDateAsVersion'] == true &&
|
app.app.additionalSettings['releaseDateAsVersion'] == true &&
|
||||||
originalSettings['releaseDateAsVersion'] != true;
|
originalSettings['releaseDateAsVersion'] != true;
|
||||||
var releaseDateVersionDisabled =
|
var releaseDateVersionDisabled =
|
||||||
app.app.additionalSettings['releaseDateAsVersion'] != true &&
|
app.app.additionalSettings['releaseDateAsVersion'] != true &&
|
||||||
originalSettings['releaseDateAsVersion'] == true;
|
originalSettings['releaseDateAsVersion'] == true;
|
||||||
if (releaseDateVersionEnabled) {
|
if (releaseDateVersionEnabled) {
|
||||||
if (app.app.releaseDate != null) {
|
if (app.app.releaseDate != null) {
|
||||||
bool isUpdated = app.app.installedVersion == app.app.latestVersion;
|
bool isUpdated = app.app.installedVersion == app.app.latestVersion;
|
||||||
app.app.latestVersion =
|
app.app.latestVersion = app.app.releaseDate!.microsecondsSinceEpoch
|
||||||
app.app.releaseDate!.microsecondsSinceEpoch.toString();
|
.toString();
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
app.app.installedVersion = app.app.latestVersion;
|
app.app.installedVersion = app.app.latestVersion;
|
||||||
}
|
}
|
||||||
@ -461,172 +488,195 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getInstallOrUpdateButton() => TextButton(
|
getInstallOrUpdateButton() => TextButton(
|
||||||
onPressed: !updating &&
|
onPressed:
|
||||||
(app?.app.installedVersion == null ||
|
!updating &&
|
||||||
app?.app.installedVersion != app?.app.latestVersion) &&
|
(app?.app.installedVersion == null ||
|
||||||
!areDownloadsRunning
|
app?.app.installedVersion != app?.app.latestVersion) &&
|
||||||
? () async {
|
!areDownloadsRunning
|
||||||
try {
|
? () async {
|
||||||
var successMessage = app?.app.installedVersion == null
|
try {
|
||||||
? tr('installed')
|
var successMessage = app?.app.installedVersion == null
|
||||||
: tr('appsUpdated');
|
? tr('installed')
|
||||||
HapticFeedback.heavyImpact();
|
: tr('appsUpdated');
|
||||||
var res = await appsProvider.downloadAndInstallLatestApps(
|
HapticFeedback.heavyImpact();
|
||||||
app?.app.id != null ? [app!.app.id] : [],
|
var res = await appsProvider.downloadAndInstallLatestApps(
|
||||||
globalNavigatorKey.currentContext,
|
app?.app.id != null ? [app!.app.id] : [],
|
||||||
);
|
globalNavigatorKey.currentContext,
|
||||||
if (res.isNotEmpty && !trackOnly) {
|
);
|
||||||
// ignore: use_build_context_synchronously
|
if (res.isNotEmpty && !trackOnly) {
|
||||||
showMessage(successMessage, context);
|
|
||||||
}
|
|
||||||
if (res.isNotEmpty && mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showError(e, context);
|
showMessage(successMessage, context);
|
||||||
}
|
}
|
||||||
|
if (res.isNotEmpty && mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
showError(e, context);
|
||||||
}
|
}
|
||||||
: null,
|
}
|
||||||
child: Text(app?.app.installedVersion == null
|
: null,
|
||||||
|
child: Text(
|
||||||
|
app?.app.installedVersion == null
|
||||||
? !trackOnly
|
? !trackOnly
|
||||||
? tr('install')
|
? tr('install')
|
||||||
: tr('markInstalled')
|
: tr('markInstalled')
|
||||||
: !trackOnly
|
: !trackOnly
|
||||||
? tr('update')
|
? tr('update')
|
||||||
: tr('markUpdated')));
|
: tr('markUpdated'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
getBottomSheetMenu() => Padding(
|
getBottomSheetMenu() => Padding(
|
||||||
padding:
|
padding: EdgeInsets.fromLTRB(
|
||||||
EdgeInsets.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
|
0,
|
||||||
child: Column(
|
0,
|
||||||
mainAxisSize: MainAxisSize.min,
|
0,
|
||||||
children: [
|
MediaQuery.of(context).padding.bottom,
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
child: Column(
|
||||||
child: Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
if (source != null &&
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||||
source.combinedAppSpecificSettingFormItems.isNotEmpty)
|
child: Row(
|
||||||
IconButton(
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
onPressed: app?.downloadProgress != null || updating
|
children: [
|
||||||
? null
|
if (source != null &&
|
||||||
: () async {
|
source.combinedAppSpecificSettingFormItems.isNotEmpty)
|
||||||
var values =
|
IconButton(
|
||||||
await showAdditionalOptionsDialog();
|
onPressed: app?.downloadProgress != null || updating
|
||||||
handleAdditionalOptionChanges(values);
|
? null
|
||||||
},
|
: () async {
|
||||||
tooltip: tr('additionalOptions'),
|
var values = await showAdditionalOptionsDialog();
|
||||||
icon: const Icon(Icons.edit)),
|
handleAdditionalOptionChanges(values);
|
||||||
if (app != null && app.installedInfo != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
appsProvider.openAppSettings(app.app.id);
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.settings),
|
tooltip: tr('additionalOptions'),
|
||||||
tooltip: tr('settings'),
|
icon: const Icon(Icons.edit),
|
||||||
),
|
),
|
||||||
if (app != null && settingsProvider.showAppWebpage)
|
if (app != null && app.installedInfo != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
appsProvider.openAppSettings(app.app.id);
|
||||||
context: context,
|
},
|
||||||
builder: (BuildContext ctx) {
|
icon: const Icon(Icons.settings),
|
||||||
return AlertDialog(
|
tooltip: tr('settings'),
|
||||||
scrollable: true,
|
),
|
||||||
content: getFullInfoColumn(small: true),
|
if (app != null && settingsProvider.showAppWebpage)
|
||||||
title: Text(app.name),
|
IconButton(
|
||||||
actions: [
|
onPressed: () {
|
||||||
TextButton(
|
showDialog(
|
||||||
onPressed: () {
|
context: context,
|
||||||
Navigator.of(context).pop();
|
builder: (BuildContext ctx) {
|
||||||
},
|
return AlertDialog(
|
||||||
child: Text(tr('continue')))
|
scrollable: true,
|
||||||
],
|
content: getFullInfoColumn(small: true),
|
||||||
);
|
title: Text(app.name),
|
||||||
});
|
actions: [
|
||||||
},
|
TextButton(
|
||||||
icon: const Icon(Icons.more_horiz),
|
onPressed: () {
|
||||||
tooltip: tr('more')),
|
Navigator.of(context).pop();
|
||||||
if (app?.app.installedVersion != null &&
|
},
|
||||||
app?.app.installedVersion != app?.app.latestVersion &&
|
child: Text(tr('continue')),
|
||||||
!isVersionDetectionStandard &&
|
),
|
||||||
!trackOnly)
|
],
|
||||||
IconButton(
|
);
|
||||||
onPressed: app?.downloadProgress != null || updating
|
},
|
||||||
? null
|
);
|
||||||
: showMarkUpdatedDialog,
|
},
|
||||||
tooltip: tr('markUpdated'),
|
icon: const Icon(Icons.more_horiz),
|
||||||
icon: const Icon(Icons.done)),
|
tooltip: tr('more'),
|
||||||
if ((!isVersionDetectionStandard || trackOnly) &&
|
),
|
||||||
app?.app.installedVersion != null &&
|
if (app?.app.installedVersion != null &&
|
||||||
app?.app.installedVersion == app?.app.latestVersion)
|
app?.app.installedVersion != app?.app.latestVersion &&
|
||||||
IconButton(
|
!isVersionDetectionStandard &&
|
||||||
onPressed: app?.app == null || updating
|
!trackOnly)
|
||||||
? null
|
IconButton(
|
||||||
: () {
|
onPressed: app?.downloadProgress != null || updating
|
||||||
app!.app.installedVersion = null;
|
? null
|
||||||
appsProvider.saveApps([app.app]);
|
: showMarkUpdatedDialog,
|
||||||
},
|
tooltip: tr('markUpdated'),
|
||||||
icon: const Icon(Icons.restore_rounded),
|
icon: const Icon(Icons.done),
|
||||||
tooltip: tr('resetInstallStatus')),
|
),
|
||||||
const SizedBox(width: 16.0),
|
if ((!isVersionDetectionStandard || trackOnly) &&
|
||||||
Expanded(child: getInstallOrUpdateButton()),
|
app?.app.installedVersion != null &&
|
||||||
const SizedBox(width: 16.0),
|
app?.app.installedVersion == app?.app.latestVersion)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: app?.downloadProgress != null || updating
|
onPressed: app?.app == null || updating
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
appsProvider
|
app!.app.installedVersion = null;
|
||||||
.removeAppsWithModal(
|
appsProvider.saveApps([app.app]);
|
||||||
context, app != null ? [app.app] : [])
|
},
|
||||||
.then((value) {
|
icon: const Icon(Icons.restore_rounded),
|
||||||
if (value == true) {
|
tooltip: tr('resetInstallStatus'),
|
||||||
Navigator.of(context).pop();
|
),
|
||||||
}
|
const SizedBox(width: 16.0),
|
||||||
});
|
Expanded(child: getInstallOrUpdateButton()),
|
||||||
},
|
const SizedBox(width: 16.0),
|
||||||
tooltip: tr('remove'),
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline),
|
onPressed: app?.downloadProgress != null || updating
|
||||||
),
|
? null
|
||||||
])),
|
: () {
|
||||||
if (app?.downloadProgress != null)
|
appsProvider
|
||||||
Padding(
|
.removeAppsWithModal(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
context,
|
||||||
child: LinearProgressIndicator(
|
app != null ? [app.app] : [],
|
||||||
value: app!.downloadProgress! >= 0
|
)
|
||||||
? app.downloadProgress! / 100
|
.then((value) {
|
||||||
: null))
|
if (value == true) {
|
||||||
],
|
Navigator.of(context).pop();
|
||||||
));
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: tr('remove'),
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (app?.downloadProgress != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: app!.downloadProgress! >= 0
|
||||||
|
? app.downloadProgress! / 100
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
appScreenAppBar() => AppBar(
|
appScreenAppBar() => AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(),
|
appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(),
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
child: settingsProvider.showAppWebpage
|
child: settingsProvider.showAppWebpage
|
||||||
? getAppWebView()
|
? getAppWebView()
|
||||||
: CustomScrollView(
|
: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(children: [getFullInfoColumn()])),
|
child: Column(children: [getFullInfoColumn()]),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
onRefresh: () async {
|
],
|
||||||
if (app != null) {
|
),
|
||||||
getUpdate(app.app.id);
|
onRefresh: () async {
|
||||||
}
|
if (app != null) {
|
||||||
}),
|
getUpdate(app.app.id);
|
||||||
bottomSheet: getBottomSheetMenu());
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
bottomSheet: getBottomSheetMenu(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1397
lib/pages/apps.dart
1397
lib/pages/apps.dart
File diff suppressed because it is too large
Load Diff
@ -43,13 +43,22 @@ class _HomePageState extends State<HomePage> {
|
|||||||
bool isLinkActivity = false;
|
bool isLinkActivity = false;
|
||||||
|
|
||||||
List<NavigationPageItem> pages = [
|
List<NavigationPageItem> pages = [
|
||||||
NavigationPageItem(tr('appsString'), Icons.apps,
|
|
||||||
AppsPage(key: GlobalKey<AppsPageState>())),
|
|
||||||
NavigationPageItem(
|
NavigationPageItem(
|
||||||
tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
|
tr('appsString'),
|
||||||
|
Icons.apps,
|
||||||
|
AppsPage(key: GlobalKey<AppsPageState>()),
|
||||||
|
),
|
||||||
NavigationPageItem(
|
NavigationPageItem(
|
||||||
tr('importExport'), Icons.import_export, const ImportExportPage()),
|
tr('addApp'),
|
||||||
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
|
Icons.add,
|
||||||
|
AddAppPage(key: GlobalKey<AddAppPageState>()),
|
||||||
|
),
|
||||||
|
NavigationPageItem(
|
||||||
|
tr('importExport'),
|
||||||
|
Icons.import_export,
|
||||||
|
const ImportExportPage(),
|
||||||
|
),
|
||||||
|
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -60,63 +69,69 @@ class _HomePageState extends State<HomePage> {
|
|||||||
var sp = context.read<SettingsProvider>();
|
var sp = context.read<SettingsProvider>();
|
||||||
if (!sp.welcomeShown) {
|
if (!sp.welcomeShown) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(tr('welcome')),
|
title: Text(tr('welcome')),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
spacing: 20,
|
spacing: 20,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('documentationLinksNote')),
|
Text(tr('documentationLinksNote')),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(
|
||||||
|
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(tr('batteryOptimizationNote')),
|
||||||
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
final intent = AndroidIntent(
|
||||||
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
action:
|
||||||
mode: LaunchMode.externalApplication);
|
'android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS',
|
||||||
|
package:
|
||||||
|
obtainiumId, // Replace with your app's package name
|
||||||
|
);
|
||||||
|
|
||||||
|
intent.launch();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
tr('settings'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
)),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(tr('batteryOptimizationNote')),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
final intent = AndroidIntent(
|
|
||||||
action:
|
|
||||||
'android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS',
|
|
||||||
package:
|
|
||||||
obtainiumId, // Replace with your app's package name
|
|
||||||
);
|
|
||||||
|
|
||||||
intent.launch();
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
tr('settings'),
|
|
||||||
style: const TextStyle(
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
),
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
sp.welcomeShown = true;
|
|
||||||
Navigator.of(context).pop(null);
|
|
||||||
},
|
|
||||||
child: Text(tr('ok'))),
|
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
});
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
sp.welcomeShown = true;
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
},
|
||||||
|
child: Text(tr('ok')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -126,13 +141,12 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
goToAddApp(String data) async {
|
goToAddApp(String data) async {
|
||||||
switchToPage(1);
|
switchToPage(1);
|
||||||
while (
|
while ((pages[1].widget.key as GlobalKey<AddAppPageState>?)
|
||||||
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
|
?.currentState ==
|
||||||
null) {
|
null) {
|
||||||
await Future.delayed(const Duration(microseconds: 1));
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
}
|
}
|
||||||
(pages[1].widget.key as GlobalKey<AddAppPageState>?)
|
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState
|
||||||
?.currentState
|
|
||||||
?.linkFn(data);
|
?.linkFn(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,44 +160,55 @@ class _HomePageState extends State<HomePage> {
|
|||||||
} else if (action == 'app' || action == 'apps') {
|
} else if (action == 'app' || action == 'apps') {
|
||||||
var dataStr = Uri.decodeComponent(data);
|
var dataStr = Uri.decodeComponent(data);
|
||||||
if (await showDialog(
|
if (await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('importX', args: [
|
title: tr(
|
||||||
action == 'app' ? tr('app') : tr('appsString')
|
'importX',
|
||||||
]),
|
args: [
|
||||||
items: const [],
|
(action == 'app' ? tr('app') : tr('appsString'))
|
||||||
additionalWidgets: [
|
.toLowerCase(),
|
||||||
ExpansionTile(
|
|
||||||
title: const Text('Raw JSON'),
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
dataStr,
|
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}) !=
|
items: const [],
|
||||||
|
additionalWidgets: [
|
||||||
|
ExpansionTile(
|
||||||
|
title: const Text('Raw JSON'),
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
dataStr,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
) !=
|
||||||
null) {
|
null) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var appsProvider = context.read<AppsProvider>();
|
var appsProvider = context.read<AppsProvider>();
|
||||||
var result = await appsProvider.import(action == 'app'
|
var result = await appsProvider.import(
|
||||||
? '{ "apps": [$dataStr] }'
|
action == 'app'
|
||||||
: '{ "apps": $dataStr }');
|
? '{ "apps": [$dataStr] }'
|
||||||
|
: '{ "apps": $dataStr }',
|
||||||
|
);
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showMessage(
|
showMessage(
|
||||||
tr('importedX', args: [plural('apps', result.key.length)]),
|
tr(
|
||||||
context);
|
'importedX',
|
||||||
|
args: [plural('apps', result.key.length).toLowerCase()],
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
);
|
||||||
await appsProvider
|
await appsProvider
|
||||||
.checkUpdates(specificIds: result.key.map((e) => e.id).toList())
|
.checkUpdates(specificIds: result.key.map((e) => e.id).toList())
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
if (e is Map && e['errors'] is MultiAppMultiError) {
|
if (e is Map && e['errors'] is MultiAppMultiError) {
|
||||||
showError(e['errors'].toString(), context);
|
showError(e['errors'].toString(), context);
|
||||||
}
|
}
|
||||||
return <App>[];
|
return <App>[];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw ObtainiumError(tr('unknown'));
|
throw ObtainiumError(tr('unknown'));
|
||||||
@ -210,15 +235,16 @@ class _HomePageState extends State<HomePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsReversing(int targetIndex) {
|
void setIsReversing(int targetIndex) {
|
||||||
bool reversing = selectedIndexHistory.isNotEmpty &&
|
bool reversing =
|
||||||
|
selectedIndexHistory.isNotEmpty &&
|
||||||
selectedIndexHistory.last > targetIndex;
|
selectedIndexHistory.last > targetIndex;
|
||||||
setState(() {
|
setState(() {
|
||||||
isReversing = reversing;
|
isReversing = reversing;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToPage(int index) async {
|
Future<void> switchToPage(int index) async {
|
||||||
setIsReversing(index);
|
setIsReversing(index);
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
|
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
|
||||||
@ -259,65 +285,71 @@ class _HomePageState extends State<HomePage> {
|
|||||||
prevIsLoading = appsProvider.loadingApps;
|
prevIsLoading = appsProvider.loadingApps;
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: PageTransitionSwitcher(
|
body: PageTransitionSwitcher(
|
||||||
duration: Duration(
|
duration: Duration(
|
||||||
milliseconds:
|
milliseconds: settingsProvider.disablePageTransitions ? 0 : 300,
|
||||||
settingsProvider.disablePageTransitions ? 0 : 300),
|
|
||||||
reverse: settingsProvider.reversePageTransitions
|
|
||||||
? !isReversing
|
|
||||||
: isReversing,
|
|
||||||
transitionBuilder: (
|
|
||||||
Widget child,
|
|
||||||
Animation<double> animation,
|
|
||||||
Animation<double> secondaryAnimation,
|
|
||||||
) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: pages
|
|
||||||
.elementAt(selectedIndexHistory.isEmpty
|
|
||||||
? 0
|
|
||||||
: selectedIndexHistory.last)
|
|
||||||
.widget,
|
|
||||||
),
|
),
|
||||||
bottomNavigationBar: NavigationBar(
|
reverse: settingsProvider.reversePageTransitions
|
||||||
destinations: pages
|
? !isReversing
|
||||||
.map((e) =>
|
: isReversing,
|
||||||
NavigationDestination(icon: Icon(e.icon), label: e.title))
|
transitionBuilder:
|
||||||
.toList(),
|
(
|
||||||
onDestinationSelected: (int index) async {
|
Widget child,
|
||||||
HapticFeedback.selectionClick();
|
Animation<double> animation,
|
||||||
switchToPage(index);
|
Animation<double> secondaryAnimation,
|
||||||
},
|
) {
|
||||||
selectedIndex:
|
return SharedAxisTransition(
|
||||||
|
animation: animation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: pages
|
||||||
|
.elementAt(
|
||||||
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||||
),
|
)
|
||||||
|
.widget,
|
||||||
),
|
),
|
||||||
onWillPop: () async {
|
bottomNavigationBar: NavigationBar(
|
||||||
if (isLinkActivity &&
|
destinations: pages
|
||||||
selectedIndexHistory.length == 1 &&
|
.map(
|
||||||
selectedIndexHistory.last == 1) {
|
(e) =>
|
||||||
return true;
|
NavigationDestination(icon: Icon(e.icon), label: e.title),
|
||||||
}
|
)
|
||||||
setIsReversing(selectedIndexHistory.length >= 2
|
.toList(),
|
||||||
|
onDestinationSelected: (int index) async {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
switchToPage(index);
|
||||||
|
},
|
||||||
|
selectedIndex: selectedIndexHistory.isEmpty
|
||||||
|
? 0
|
||||||
|
: selectedIndexHistory.last,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onWillPop: () async {
|
||||||
|
if (isLinkActivity &&
|
||||||
|
selectedIndexHistory.length == 1 &&
|
||||||
|
selectedIndexHistory.last == 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
setIsReversing(
|
||||||
|
selectedIndexHistory.length >= 2
|
||||||
? selectedIndexHistory.reversed.toList()[1]
|
? selectedIndexHistory.reversed.toList()[1]
|
||||||
: 0);
|
: 0,
|
||||||
if (selectedIndexHistory.isNotEmpty) {
|
);
|
||||||
setState(() {
|
if (selectedIndexHistory.isNotEmpty) {
|
||||||
selectedIndexHistory.removeLast();
|
setState(() {
|
||||||
});
|
selectedIndexHistory.removeLast();
|
||||||
return false;
|
});
|
||||||
}
|
return false;
|
||||||
return !(pages[0].widget.key as GlobalKey<AppsPageState>)
|
}
|
||||||
.currentState
|
return !(pages[0].widget.key as GlobalKey<AppsPageState>).currentState!
|
||||||
?.clearSelected();
|
.clearSelected();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@ class Log {
|
|||||||
idColumn: id,
|
idColumn: id,
|
||||||
levelColumn: level.index,
|
levelColumn: level.index,
|
||||||
messageColumn: message,
|
messageColumn: message,
|
||||||
timestampColumn: timestamp.millisecondsSinceEpoch
|
timestampColumn: timestamp.millisecondsSinceEpoch,
|
||||||
};
|
};
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@ -33,8 +33,9 @@ class Log {
|
|||||||
id = map[idColumn] as int;
|
id = map[idColumn] as int;
|
||||||
level = LogLevels.values.elementAt(map[levelColumn] as int);
|
level = LogLevels.values.elementAt(map[levelColumn] as int);
|
||||||
message = map[messageColumn] as String;
|
message = map[messageColumn] as String;
|
||||||
timestamp =
|
timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||||
DateTime.fromMillisecondsSinceEpoch(map[timestampColumn] as int);
|
map[timestampColumn] as int,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -51,16 +52,19 @@ class LogsProvider {
|
|||||||
Database? db;
|
Database? db;
|
||||||
|
|
||||||
Future<Database> getDB() async {
|
Future<Database> getDB() async {
|
||||||
db ??= await openDatabase(dbPath, version: 1,
|
db ??= await openDatabase(
|
||||||
onCreate: (Database db, int version) async {
|
dbPath,
|
||||||
await db.execute('''
|
version: 1,
|
||||||
|
onCreate: (Database db, int version) async {
|
||||||
|
await db.execute('''
|
||||||
create table if not exists $logTable (
|
create table if not exists $logTable (
|
||||||
$idColumn integer primary key autoincrement,
|
$idColumn integer primary key autoincrement,
|
||||||
$levelColumn integer not null,
|
$levelColumn integer not null,
|
||||||
$messageColumn text not null,
|
$messageColumn text not null,
|
||||||
$timestampColumn integer not null)
|
$timestampColumn integer not null)
|
||||||
''');
|
''');
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return db!;
|
return db!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,27 +79,38 @@ create table if not exists $logTable (
|
|||||||
|
|
||||||
Future<List<Log>> get({DateTime? before, DateTime? after}) async {
|
Future<List<Log>> get({DateTime? before, DateTime? after}) async {
|
||||||
var where = getWhereDates(before: before, after: after);
|
var where = getWhereDates(before: before, after: after);
|
||||||
return (await (await getDB())
|
return (await (await getDB()).query(
|
||||||
.query(logTable, where: where.key, whereArgs: where.value))
|
logTable,
|
||||||
.map((e) => Log.fromMap(e))
|
where: where.key,
|
||||||
.toList();
|
whereArgs: where.value,
|
||||||
|
)).map((e) => Log.fromMap(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> clear({DateTime? before, DateTime? after}) async {
|
Future<int> clear({DateTime? before, DateTime? after}) async {
|
||||||
var where = getWhereDates(before: before, after: after);
|
var where = getWhereDates(before: before, after: after);
|
||||||
var res = await (await getDB())
|
var res = await (await getDB()).delete(
|
||||||
.delete(logTable, where: where.key, whereArgs: where.value);
|
logTable,
|
||||||
|
where: where.key,
|
||||||
|
whereArgs: where.value,
|
||||||
|
);
|
||||||
if (res > 0) {
|
if (res > 0) {
|
||||||
add(plural('clearedNLogsBeforeXAfterY', res,
|
add(
|
||||||
|
plural(
|
||||||
|
'clearedNLogsBeforeXAfterY',
|
||||||
|
res,
|
||||||
namedArgs: {'before': before.toString(), 'after': after.toString()},
|
namedArgs: {'before': before.toString(), 'after': after.toString()},
|
||||||
name: 'n'));
|
name: 'n',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MapEntry<String?, List<int>?> getWhereDates(
|
MapEntry<String?, List<int>?> getWhereDates({
|
||||||
{DateTime? before, DateTime? after}) {
|
DateTime? before,
|
||||||
|
DateTime? after,
|
||||||
|
}) {
|
||||||
List<String> where = [];
|
List<String> where = [];
|
||||||
List<int> whereArgs = [];
|
List<int> whereArgs = [];
|
||||||
if (before != null) {
|
if (before != null) {
|
||||||
|
@ -20,91 +20,116 @@ class ObtainiumNotification {
|
|||||||
bool onlyAlertOnce;
|
bool onlyAlertOnce;
|
||||||
String? payload;
|
String? payload;
|
||||||
|
|
||||||
ObtainiumNotification(this.id, this.title, this.message, this.channelCode,
|
ObtainiumNotification(
|
||||||
this.channelName, this.channelDescription, this.importance,
|
this.id,
|
||||||
{this.onlyAlertOnce = false, this.progPercent, this.payload});
|
this.title,
|
||||||
|
this.message,
|
||||||
|
this.channelCode,
|
||||||
|
this.channelName,
|
||||||
|
this.channelDescription,
|
||||||
|
this.importance, {
|
||||||
|
this.onlyAlertOnce = false,
|
||||||
|
this.progPercent,
|
||||||
|
this.payload,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdateNotification extends ObtainiumNotification {
|
class UpdateNotification extends ObtainiumNotification {
|
||||||
UpdateNotification(List<App> updates, {int? id})
|
UpdateNotification(List<App> updates, {int? id})
|
||||||
: super(
|
: super(
|
||||||
id ?? 2,
|
id ?? 2,
|
||||||
tr('updatesAvailable'),
|
tr('updatesAvailable'),
|
||||||
'',
|
'',
|
||||||
'UPDATES_AVAILABLE',
|
'UPDATES_AVAILABLE',
|
||||||
tr('updatesAvailableNotifChannel'),
|
tr('updatesAvailableNotifChannel'),
|
||||||
tr('updatesAvailableNotifDescription'),
|
tr('updatesAvailableNotifDescription'),
|
||||||
Importance.max) {
|
Importance.max,
|
||||||
|
) {
|
||||||
message = updates.isEmpty
|
message = updates.isEmpty
|
||||||
? tr('noNewUpdates')
|
? tr('noNewUpdates')
|
||||||
: updates.length == 1
|
: updates.length == 1
|
||||||
? tr('xHasAnUpdate', args: [updates[0].finalName])
|
? tr('xHasAnUpdate', args: [updates[0].finalName])
|
||||||
: plural('xAndNMoreUpdatesAvailable', updates.length - 1,
|
: plural(
|
||||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
'xAndNMoreUpdatesAvailable',
|
||||||
|
updates.length - 1,
|
||||||
|
args: [updates[0].finalName, (updates.length - 1).toString()],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SilentUpdateNotification extends ObtainiumNotification {
|
class SilentUpdateNotification extends ObtainiumNotification {
|
||||||
SilentUpdateNotification(List<App> updates, bool succeeded, {int? id})
|
SilentUpdateNotification(List<App> updates, bool succeeded, {int? id})
|
||||||
: super(
|
: super(
|
||||||
id ?? 3,
|
id ?? 3,
|
||||||
succeeded ? tr('appsUpdated') : tr('appsNotUpdated'),
|
succeeded ? tr('appsUpdated') : tr('appsNotUpdated'),
|
||||||
'',
|
'',
|
||||||
'APPS_UPDATED',
|
'APPS_UPDATED',
|
||||||
tr('appsUpdatedNotifChannel'),
|
tr('appsUpdatedNotifChannel'),
|
||||||
tr('appsUpdatedNotifDescription'),
|
tr('appsUpdatedNotifDescription'),
|
||||||
Importance.defaultImportance) {
|
Importance.defaultImportance,
|
||||||
|
) {
|
||||||
message = updates.length == 1
|
message = updates.length == 1
|
||||||
? tr(succeeded ? 'xWasUpdatedToY' : 'xWasNotUpdatedToY',
|
? tr(
|
||||||
args: [updates[0].finalName, updates[0].latestVersion])
|
succeeded ? 'xWasUpdatedToY' : 'xWasNotUpdatedToY',
|
||||||
|
args: [updates[0].finalName, updates[0].latestVersion],
|
||||||
|
)
|
||||||
: plural(
|
: plural(
|
||||||
succeeded ? 'xAndNMoreUpdatesInstalled' : "xAndNMoreUpdatesFailed",
|
succeeded ? 'xAndNMoreUpdatesInstalled' : "xAndNMoreUpdatesFailed",
|
||||||
updates.length - 1,
|
updates.length - 1,
|
||||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
args: [updates[0].finalName, (updates.length - 1).toString()],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SilentUpdateAttemptNotification extends ObtainiumNotification {
|
class SilentUpdateAttemptNotification extends ObtainiumNotification {
|
||||||
SilentUpdateAttemptNotification(List<App> updates, {int? id})
|
SilentUpdateAttemptNotification(List<App> updates, {int? id})
|
||||||
: super(
|
: super(
|
||||||
id ?? 3,
|
id ?? 3,
|
||||||
tr('appsPossiblyUpdated'),
|
tr('appsPossiblyUpdated'),
|
||||||
'',
|
'',
|
||||||
'APPS_POSSIBLY_UPDATED',
|
'APPS_POSSIBLY_UPDATED',
|
||||||
tr('appsPossiblyUpdatedNotifChannel'),
|
tr('appsPossiblyUpdatedNotifChannel'),
|
||||||
tr('appsPossiblyUpdatedNotifDescription'),
|
tr('appsPossiblyUpdatedNotifDescription'),
|
||||||
Importance.defaultImportance) {
|
Importance.defaultImportance,
|
||||||
|
) {
|
||||||
message = updates.length == 1
|
message = updates.length == 1
|
||||||
? tr('xWasPossiblyUpdatedToY',
|
? tr(
|
||||||
args: [updates[0].finalName, updates[0].latestVersion])
|
'xWasPossiblyUpdatedToY',
|
||||||
: plural('xAndNMoreUpdatesPossiblyInstalled', updates.length - 1,
|
args: [updates[0].finalName, updates[0].latestVersion],
|
||||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
)
|
||||||
|
: plural(
|
||||||
|
'xAndNMoreUpdatesPossiblyInstalled',
|
||||||
|
updates.length - 1,
|
||||||
|
args: [updates[0].finalName, (updates.length - 1).toString()],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
||||||
ErrorCheckingUpdatesNotification(String error, {int? id})
|
ErrorCheckingUpdatesNotification(String error, {int? id})
|
||||||
: super(
|
: super(
|
||||||
id ?? 5,
|
id ?? 5,
|
||||||
tr('errorCheckingUpdates'),
|
tr('errorCheckingUpdates'),
|
||||||
error,
|
error,
|
||||||
'BG_UPDATE_CHECK_ERROR',
|
'BG_UPDATE_CHECK_ERROR',
|
||||||
tr('errorCheckingUpdatesNotifChannel'),
|
tr('errorCheckingUpdatesNotifChannel'),
|
||||||
tr('errorCheckingUpdatesNotifDescription'),
|
tr('errorCheckingUpdatesNotifDescription'),
|
||||||
Importance.high,
|
Importance.high,
|
||||||
payload: "${tr('errorCheckingUpdates')}\n$error");
|
payload: "${tr('errorCheckingUpdates')}\n$error",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppsRemovedNotification extends ObtainiumNotification {
|
class AppsRemovedNotification extends ObtainiumNotification {
|
||||||
AppsRemovedNotification(List<List<String>> namedReasons)
|
AppsRemovedNotification(List<List<String>> namedReasons)
|
||||||
: super(
|
: super(
|
||||||
6,
|
6,
|
||||||
tr('appsRemoved'),
|
tr('appsRemoved'),
|
||||||
'',
|
'',
|
||||||
'APPS_REMOVED',
|
'APPS_REMOVED',
|
||||||
tr('appsRemovedNotifChannel'),
|
tr('appsRemovedNotifChannel'),
|
||||||
tr('appsRemovedNotifDescription'),
|
tr('appsRemovedNotifDescription'),
|
||||||
Importance.max) {
|
Importance.max,
|
||||||
|
) {
|
||||||
message = '';
|
message = '';
|
||||||
for (var r in namedReasons) {
|
for (var r in namedReasons) {
|
||||||
message += '${tr('xWasRemovedDueToErrorY', args: [r[0], r[1]])} \n';
|
message += '${tr('xWasRemovedDueToErrorY', args: [r[0], r[1]])} \n';
|
||||||
@ -115,49 +140,53 @@ class AppsRemovedNotification extends ObtainiumNotification {
|
|||||||
|
|
||||||
class DownloadNotification extends ObtainiumNotification {
|
class DownloadNotification extends ObtainiumNotification {
|
||||||
DownloadNotification(String appName, int progPercent)
|
DownloadNotification(String appName, int progPercent)
|
||||||
: super(
|
: super(
|
||||||
appName.hashCode,
|
appName.hashCode,
|
||||||
tr('downloadingX', args: [appName]),
|
tr('downloadingX', args: [appName]),
|
||||||
'',
|
'',
|
||||||
'APP_DOWNLOADING',
|
'APP_DOWNLOADING',
|
||||||
tr('downloadingXNotifChannel', args: [tr('app')]),
|
tr('downloadingXNotifChannel', args: [tr('app')]),
|
||||||
tr('downloadNotifDescription'),
|
tr('downloadNotifDescription'),
|
||||||
Importance.low,
|
Importance.low,
|
||||||
onlyAlertOnce: true,
|
onlyAlertOnce: true,
|
||||||
progPercent: progPercent);
|
progPercent: progPercent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadedNotification extends ObtainiumNotification {
|
class DownloadedNotification extends ObtainiumNotification {
|
||||||
DownloadedNotification(String fileName, String downloadUrl)
|
DownloadedNotification(String fileName, String downloadUrl)
|
||||||
: super(
|
: super(
|
||||||
downloadUrl.hashCode,
|
downloadUrl.hashCode,
|
||||||
tr('downloadedX', args: [fileName]),
|
tr('downloadedX', args: [fileName]),
|
||||||
'',
|
'',
|
||||||
'FILE_DOWNLOADED',
|
'FILE_DOWNLOADED',
|
||||||
tr('downloadedXNotifChannel', args: [tr('app')]),
|
tr('downloadedXNotifChannel', args: [tr('app')]),
|
||||||
tr('downloadedX', args: [tr('app')]),
|
tr('downloadedX', args: [tr('app')]),
|
||||||
Importance.defaultImportance);
|
Importance.defaultImportance,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final completeInstallationNotification = ObtainiumNotification(
|
final completeInstallationNotification = ObtainiumNotification(
|
||||||
1,
|
1,
|
||||||
tr('completeAppInstallation'),
|
tr('completeAppInstallation'),
|
||||||
tr('obtainiumMustBeOpenToInstallApps'),
|
tr('obtainiumMustBeOpenToInstallApps'),
|
||||||
'COMPLETE_INSTALL',
|
'COMPLETE_INSTALL',
|
||||||
tr('completeAppInstallationNotifChannel'),
|
tr('completeAppInstallationNotifChannel'),
|
||||||
tr('completeAppInstallationNotifDescription'),
|
tr('completeAppInstallationNotifDescription'),
|
||||||
Importance.max);
|
Importance.max,
|
||||||
|
);
|
||||||
|
|
||||||
class CheckingUpdatesNotification extends ObtainiumNotification {
|
class CheckingUpdatesNotification extends ObtainiumNotification {
|
||||||
CheckingUpdatesNotification(String appName)
|
CheckingUpdatesNotification(String appName)
|
||||||
: super(
|
: super(
|
||||||
4,
|
4,
|
||||||
tr('checkingForUpdates'),
|
tr('checkingForUpdates'),
|
||||||
appName,
|
appName,
|
||||||
'BG_UPDATE_CHECK',
|
'BG_UPDATE_CHECK',
|
||||||
tr('checkingForUpdatesNotifChannel'),
|
tr('checkingForUpdatesNotifChannel'),
|
||||||
tr('checkingForUpdatesNotifDescription'),
|
tr('checkingForUpdatesNotifDescription'),
|
||||||
Importance.min);
|
Importance.min,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationsProvider {
|
class NotificationsProvider {
|
||||||
@ -173,13 +202,15 @@ class NotificationsProvider {
|
|||||||
Importance.max: Priority.max,
|
Importance.max: Priority.max,
|
||||||
Importance.min: Priority.min,
|
Importance.min: Priority.min,
|
||||||
Importance.none: Priority.min,
|
Importance.none: Priority.min,
|
||||||
Importance.unspecified: Priority.defaultPriority
|
Importance.unspecified: Priority.defaultPriority,
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
isInitialized = await notifications.initialize(
|
isInitialized =
|
||||||
|
await notifications.initialize(
|
||||||
const InitializationSettings(
|
const InitializationSettings(
|
||||||
android: AndroidInitializationSettings('ic_notification')),
|
android: AndroidInitializationSettings('ic_notification'),
|
||||||
|
),
|
||||||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||||
_showNotificationPayload(response.payload);
|
_showNotificationPayload(response.payload);
|
||||||
},
|
},
|
||||||
@ -187,16 +218,18 @@ class NotificationsProvider {
|
|||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkLaunchByNotif() async {
|
Future<void> checkLaunchByNotif() async {
|
||||||
final NotificationAppLaunchDetails? launchDetails =
|
final NotificationAppLaunchDetails? launchDetails = await notifications
|
||||||
await notifications.getNotificationAppLaunchDetails();
|
.getNotificationAppLaunchDetails();
|
||||||
if (launchDetails?.didNotificationLaunchApp ?? false) {
|
if (launchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
_showNotificationPayload(launchDetails!.notificationResponse?.payload,
|
_showNotificationPayload(
|
||||||
doublePop: true);
|
launchDetails!.notificationResponse?.payload,
|
||||||
|
doublePop: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showNotificationPayload(String? payload, {bool doublePop = false}) {
|
void _showNotificationPayload(String? payload, {bool doublePop = false}) {
|
||||||
if (payload?.isNotEmpty == true) {
|
if (payload?.isNotEmpty == true) {
|
||||||
var title = (payload ?? '\n\n').split('\n').first;
|
var title = (payload ?? '\n\n').split('\n').first;
|
||||||
var content = (payload ?? '\n\n').split('\n').sublist(1).join('\n');
|
var content = (payload ?? '\n\n').split('\n').sublist(1).join('\n');
|
||||||
@ -207,13 +240,14 @@ class NotificationsProvider {
|
|||||||
content: Text(content),
|
content: Text(content),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
if (doublePop) {
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
if (doublePop) {
|
}
|
||||||
Navigator.of(context).pop(null);
|
},
|
||||||
}
|
child: Text(tr('ok')),
|
||||||
},
|
),
|
||||||
child: Text(tr('ok'))),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -229,17 +263,18 @@ class NotificationsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notifyRaw(
|
Future<void> notifyRaw(
|
||||||
int id,
|
int id,
|
||||||
String title,
|
String title,
|
||||||
String message,
|
String message,
|
||||||
String channelCode,
|
String channelCode,
|
||||||
String channelName,
|
String channelName,
|
||||||
String channelDescription,
|
String channelDescription,
|
||||||
Importance importance,
|
Importance importance, {
|
||||||
{bool cancelExisting = false,
|
bool cancelExisting = false,
|
||||||
int? progPercent,
|
int? progPercent,
|
||||||
bool onlyAlertOnce = false,
|
bool onlyAlertOnce = false,
|
||||||
String? payload}) async {
|
String? payload,
|
||||||
|
}) async {
|
||||||
if (cancelExisting) {
|
if (cancelExisting) {
|
||||||
await cancel(id);
|
await cancel(id);
|
||||||
}
|
}
|
||||||
@ -247,29 +282,42 @@ class NotificationsProvider {
|
|||||||
await initialize();
|
await initialize();
|
||||||
}
|
}
|
||||||
await notifications.show(
|
await notifications.show(
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
NotificationDetails(
|
NotificationDetails(
|
||||||
android: AndroidNotificationDetails(channelCode, channelName,
|
android: AndroidNotificationDetails(
|
||||||
channelDescription: channelDescription,
|
channelCode,
|
||||||
importance: importance,
|
channelName,
|
||||||
priority: importanceToPriority[importance]!,
|
channelDescription: channelDescription,
|
||||||
groupKey: '$obtainiumId.$channelCode',
|
importance: importance,
|
||||||
progress: progPercent ?? 0,
|
priority: importanceToPriority[importance]!,
|
||||||
maxProgress: 100,
|
groupKey: '$obtainiumId.$channelCode',
|
||||||
showProgress: progPercent != null,
|
progress: progPercent ?? 0,
|
||||||
onlyAlertOnce: onlyAlertOnce,
|
maxProgress: 100,
|
||||||
indeterminate: progPercent != null && progPercent < 0)),
|
showProgress: progPercent != null,
|
||||||
payload: payload);
|
onlyAlertOnce: onlyAlertOnce,
|
||||||
|
indeterminate: progPercent != null && progPercent < 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notify(ObtainiumNotification notif,
|
Future<void> notify(
|
||||||
{bool cancelExisting = false}) =>
|
ObtainiumNotification notif, {
|
||||||
notifyRaw(notif.id, notif.title, notif.message, notif.channelCode,
|
bool cancelExisting = false,
|
||||||
notif.channelName, notif.channelDescription, notif.importance,
|
}) => notifyRaw(
|
||||||
cancelExisting: cancelExisting,
|
notif.id,
|
||||||
onlyAlertOnce: notif.onlyAlertOnce,
|
notif.title,
|
||||||
progPercent: notif.progPercent,
|
notif.message,
|
||||||
payload: notif.payload);
|
notif.channelCode,
|
||||||
|
notif.channelName,
|
||||||
|
notif.channelDescription,
|
||||||
|
notif.importance,
|
||||||
|
cancelExisting: cancelExisting,
|
||||||
|
onlyAlertOnce: notif.onlyAlertOnce,
|
||||||
|
progPercent: notif.progPercent,
|
||||||
|
payload: notif.payload,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,8 +58,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeSettings get theme {
|
ThemeSettings get theme {
|
||||||
return ThemeSettings
|
return ThemeSettings.values[prefs?.getInt('theme') ??
|
||||||
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
|
ThemeSettings.system.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
set theme(ThemeSettings t) {
|
set theme(ThemeSettings t) {
|
||||||
@ -123,8 +123,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SortColumnSettings get sortColumn {
|
SortColumnSettings get sortColumn {
|
||||||
return SortColumnSettings.values[
|
return SortColumnSettings.values[prefs?.getInt('sortColumn') ??
|
||||||
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
|
SortColumnSettings.nameAuthor.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
set sortColumn(SortColumnSettings s) {
|
set sortColumn(SortColumnSettings s) {
|
||||||
@ -133,8 +133,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SortOrderSettings get sortOrder {
|
SortOrderSettings get sortOrder {
|
||||||
return SortOrderSettings.values[
|
return SortOrderSettings.values[prefs?.getInt('sortOrder') ??
|
||||||
prefs?.getInt('sortOrder') ?? SortOrderSettings.ascending.index];
|
SortOrderSettings.ascending.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
set sortOrder(SortOrderSettings s) {
|
set sortOrder(SortOrderSettings s) {
|
||||||
@ -171,7 +171,9 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
while (!(await Permission.requestInstallPackages.isGranted)) {
|
while (!(await Permission.requestInstallPackages.isGranted)) {
|
||||||
// Explicit request as InstallPlugin request sometimes bugged
|
// Explicit request as InstallPlugin request sometimes bugged
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG);
|
msg: tr('pleaseAllowInstallPerm'),
|
||||||
|
toastLength: Toast.LENGTH_LONG,
|
||||||
|
);
|
||||||
if ((await Permission.requestInstallPackages.request()) ==
|
if ((await Permission.requestInstallPackages.request()) ==
|
||||||
PermissionStatus.granted) {
|
PermissionStatus.granted) {
|
||||||
return true;
|
return true;
|
||||||
@ -470,7 +472,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> get searchDeselected {
|
List<String> get searchDeselected {
|
||||||
return prefs?.getStringList('searchDeselected') ?? SourceProvider().sources.map((s) => s.name).toList();
|
return prefs?.getStringList('searchDeselected') ??
|
||||||
|
SourceProvider().sources.map((s) => s.name).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
set searchDeselected(List<String> list) {
|
set searchDeselected(List<String> list) {
|
||||||
@ -495,4 +498,13 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
prefs?.setBool('shizukuPretendToBeGooglePlay', val);
|
prefs?.setBool('shizukuPretendToBeGooglePlay', val);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get useFGService {
|
||||||
|
return prefs?.getBool('useFGService') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set useFGService(bool val) {
|
||||||
|
prefs?.setBool('useFGService', val);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
44
pubspec.lock
44
pubspec.lock
@ -104,10 +104,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: battery_plus
|
name: battery_plus
|
||||||
sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551
|
sha256: fb794c34cee2e4ea31005fb17ff15e1d904951ec7f15eedead741021870ee834
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.1"
|
version: "6.2.2"
|
||||||
battery_plus_platform_interface:
|
battery_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -216,18 +216,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "0c6396126421b590089447154c5f98a5de423b70cfb15b1578fd018843ee6f53"
|
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.4.0"
|
version: "11.5.0"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: device_info_plus_platform_interface
|
name: device_info_plus_platform_interface
|
||||||
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
|
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.2"
|
version: "7.0.3"
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -288,10 +288,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964"
|
sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.9"
|
version: "10.2.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -377,6 +377,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.1"
|
version: "0.7.1"
|
||||||
|
flutter_foreground_task:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_foreground_task
|
||||||
|
sha256: "9f1b25a81db95d7119d2c5cffc654048cbdd49d4056183e1beadc1a6a38f3e29"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.0"
|
||||||
flutter_keyboard_visibility:
|
flutter_keyboard_visibility:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -804,10 +812,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pointer_interceptor_web
|
name: pointer_interceptor_web
|
||||||
sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044"
|
sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.2+1"
|
version: "0.10.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -893,7 +901,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: master
|
ref: master
|
||||||
resolved-ref: "89cdb5434a7ac7510f6bcdb60e1d51a27ee2f40b"
|
resolved-ref: "012e22791138958e089f6c1a8d6c4c6943a9f253"
|
||||||
url: "https://github.com/AlexBacich/shared-storage"
|
url: "https://github.com/AlexBacich/shared-storage"
|
||||||
source: git
|
source: git
|
||||||
version: "0.7.0"
|
version: "0.7.0"
|
||||||
@ -995,10 +1003,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: synchronized
|
name: synchronized
|
||||||
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1155,10 +1163,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_platform_interface
|
name: webview_flutter_platform_interface
|
||||||
sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d"
|
sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.1"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1171,10 +1179,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.13.0"
|
version: "5.14.0"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1201,4 +1209,4 @@ packages:
|
|||||||
version: "6.3.0"
|
version: "6.3.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.29.0"
|
||||||
|
@ -16,7 +16,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: 1.1.56+2313
|
version: 1.2.0+2316
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
@ -94,6 +94,7 @@ dependencies:
|
|||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
flutter_foreground_task: ^9.1.0
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: "ic_launcher"
|
android: "ic_launcher"
|
||||||
@ -149,4 +150,4 @@ flutter:
|
|||||||
fonts:
|
fonts:
|
||||||
- family: Montserrat
|
- family: Montserrat
|
||||||
fonts:
|
fonts:
|
||||||
- asset: assets/fonts/Montserrat-Regular.ttf
|
- asset: assets/fonts/Montserrat-Regular.ttf
|
||||||
|
Reference in New Issue
Block a user