mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
41 Commits
v1.0.3
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
ab43856b90 | |||
9dae24ace6 | |||
b6e6568500 | |||
a8eae7f04b | |||
4902e0ef06 | |||
e6926a714f | |||
c9eee4331d | |||
9a8cc2e5c3 | |||
a7c9cd0f27 | |||
efc6846c1c | |||
89edddd38c | |||
e7c2112f41 | |||
d8cd3b6c92 | |||
bb1dd4ecfd | |||
3824b386d7 | |||
a9159fc8a0 | |||
7f4cf6e681 | |||
215f05fbc2 | |||
6d6afe9e69 | |||
66122f1608 | |||
0ad9bbdd8e | |||
59cc08f28a | |||
afc0c3a2fa | |||
a827046acc | |||
fd8f967036 | |||
26be524c6d | |||
8ec3360575 | |||
f66753498b | |||
90e6e5a3a3 | |||
2c4713ff25 | |||
ef3b01ac56 | |||
1cfb258dcc | |||
d13464a392 | |||
f1dd50faee | |||
6f5315db27 | |||
02922c3c8e | |||
03ef649c0f | |||
9083e28637 | |||
cfa4c680cf | |||
8dd8f471a2 | |||
657d1cd042 |
2
.flutter
2
.flutter
Submodule .flutter updated: bae5e49bc2...ba39319843
@ -42,10 +42,13 @@ Currently supported App sources:
|
||||
[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png"
|
||||
alt="Get it on GitHub"
|
||||
height="80">](https://github.com/ImranR98/Obtainium/releases)
|
||||
[<img src="https://gitlab.com/fdroid/artwork/-/raw/master/badge/get-it-on.png"
|
||||
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
|
||||
alt="Get it on IzzyOnDroid"
|
||||
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/dev.imranr.obtainium.fdroid/)
|
||||
|
||||
|
||||
[PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index)
|
||||
|
||||
## Limitations
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Uklonite zastarjeli filter aplikacija",
|
||||
"showOutdatedOnly": "Prikaži samo zastarjele aplikacije",
|
||||
"filter": "Filtriranje",
|
||||
"filterActive": "Filtriranje",
|
||||
"filterApps": "Filtriraj aplikacije",
|
||||
"appName": "Naziv aplikacije",
|
||||
"author": "Autor",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Ne prikazuj upozorenja „Samo za praćenje”",
|
||||
"dontShowAPKOriginWarnings": "Ne prikazuj upozorenja o porijeklu APK-a",
|
||||
"moveNonInstalledAppsToBottom": "Premjesti neinstalirane aplikacije na dno prikaza aplikacija",
|
||||
"gitlabPATLabel": "GitLab token za lični pristup\n(Omogućava pretraživanje i bolje otkrivanje APK-a)",
|
||||
"gitlabPATLabel": "GitLab token za lični pristup",
|
||||
"about": "O nama",
|
||||
"requiresCredentialsInSettings": "{}: Za ovo su potrebni dodatni akreditivi (u Postavkama)",
|
||||
"checkOnStart": "Provjerite ima li novosti pri pokretanju",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Dodajte ove informacije ispod.",
|
||||
"addInfoInSettings": "Dodajte ove informacije u Postavkama.",
|
||||
"githubSourceNote": "GitHub ograničavanje se može izbjeći korišćenjem tokena za lični pristup.",
|
||||
"gitlabSourceNote": "GitLab APK preuzimanje možda neće raditi bez tokena za lični pristup.",
|
||||
"sortByLastLinkSegment": "Sortiraj samo po zadnjem segmentu veze",
|
||||
"filterReleaseNotesByRegEx": "Filtirajte promjene u izdanju po regularnom izrazu",
|
||||
"customLinkFilterRegex": "Prilagođeni APK link filtrira se po regularnom izrazu (Zadano '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} i još jedna aplikacija je vjerovatno ažurirana.",
|
||||
"other": "{} i još {} aplikacija su vjerovatno ažurirane."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Odstranit filtr Neaktuální",
|
||||
"showOutdatedOnly": "Zobrazovat pouze zastaralé aplikace",
|
||||
"filter": "Filtr",
|
||||
"filterActive": "Filtr *",
|
||||
"filterApps": "Filtrovat aplikace",
|
||||
"appName": "Název aplikace",
|
||||
"author": "Autor",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Nezobrazovat varování pro 'Jen sledované'",
|
||||
"dontShowAPKOriginWarnings": "Nezobrazovat varování pro původ APK",
|
||||
"moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "O",
|
||||
"requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)",
|
||||
"checkOnStart": "Zkontrolovat jednou při spuštění",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Přidat tuto informaci na konec stránky.",
|
||||
"addInfoInSettings": "Přidat tuto informaci do nastavení.",
|
||||
"githubSourceNote": "Omezení rychlosti GitHub lze obejít pomocí klíče API.",
|
||||
"gitlabSourceNote": "Extrakce GitLab APK nemusí fungovat bez klíče API",
|
||||
"sortByLastLinkSegment": "Seřadit pouze podle poslední části odkazu",
|
||||
"filterReleaseNotesByRegEx": "Filtrovat poznámky k vydání podle regulárního výrazu",
|
||||
"customLinkFilterRegex": "Vlastní filtr odkazů APK podle regulárního výrazu (výchozí '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} a 1 další aplikace možno aktualizovat",
|
||||
"other": "{} a {} další aplikace mohou být aktualizovány."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "App-Filter ‚Nicht aktuell‘ entfernen",
|
||||
"showOutdatedOnly": "Nur nicht aktuelle Apps anzeigen",
|
||||
"filter": "Filter",
|
||||
"filterActive": "Filter *",
|
||||
"filterApps": "Apps filtern",
|
||||
"appName": "App Name",
|
||||
"author": "Autor",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
||||
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
||||
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "Über",
|
||||
"requiresCredentialsInSettings": "{}: Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
||||
"checkOnStart": "Überprüfe einmalig beim Start",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Fügen Sie diese Informationen unten hinzu.",
|
||||
"addInfoInSettings": "Fügen Sie diese Info in den Einstellungen hinzu.",
|
||||
"githubSourceNote": "Die GitHub-Ratenbegrenzung kann mit einem API-Schlüssel umgangen werden.",
|
||||
"gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel",
|
||||
"sortByLastLinkSegment": "Sortiere nur nach dem letzten Teil des Links",
|
||||
"filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern",
|
||||
"customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.",
|
||||
"other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -218,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Don't show 'Track-Only' warnings",
|
||||
"dontShowAPKOriginWarnings": "Don't show APK origin warnings",
|
||||
"moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "{} needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check for updates on startup",
|
||||
@ -232,7 +232,6 @@
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByLastLinkSegment": "Sort by only the last segment of the link",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
@ -351,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Elimiar filtro de aplicaciones desactualizado",
|
||||
"showOutdatedOnly": "Mostrar solo aplicaciones desactualizadas",
|
||||
"filter": "Filtrar",
|
||||
"filterActive": "Filtrar *",
|
||||
"filterApps": "Filtrar Actualizaciones",
|
||||
"appName": "Nombre de la aplicación",
|
||||
"author": "Autor",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "No mostrar avisos sobre apps en 'solo seguimiento'",
|
||||
"dontShowAPKOriginWarnings": "No mostrar avisos sobre las fuentes de las APKs",
|
||||
"moveNonInstalledAppsToBottom": "Mover Apps no instaladas al final",
|
||||
"gitlabPATLabel": "Token de acceso personal a GitLab\n(habilita la búsqueda y mejor detección de APKs)",
|
||||
"gitlabPATLabel": "Token de acceso personal a GitLab",
|
||||
"about": "Acerca",
|
||||
"requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en ajustes)",
|
||||
"checkOnStart": "Comprobar actualizaciones al inicio",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Añadir esta información debajo.",
|
||||
"addInfoInSettings": "Puede añadir esta información en Ajustes.",
|
||||
"githubSourceNote": "La limitación de velocidad de GitHub puede evitarse con un 'token de acceso personal'.",
|
||||
"gitlabSourceNote": "La extracción de APK de GitLab podría no funcionar sin un 'token de acceso personal'.",
|
||||
"sortByLastLinkSegment": "Ordenar sólo por el último segmento del enlace",
|
||||
"filterReleaseNotesByRegEx": "Filtrar por notas de versión (release notes)",
|
||||
"customLinkFilterRegex": "Filtro personalizado de enlace APK (por defecto '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} y 1 aplicación más podría haber sido actualizada.",
|
||||
"other": "{} y {} aplicaciones más podrían haber sido actualizadas."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "فیلتر برنامه قدیمی را حذف کنید",
|
||||
"showOutdatedOnly": "فقط برنامه های قدیمی را نشان دهید",
|
||||
"filter": "فیلتر",
|
||||
"filterActive": "فیلتر *",
|
||||
"filterApps": "فیلتر کردن برنامه ها",
|
||||
"appName": "نام برنامه",
|
||||
"author": "سازنده",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
|
||||
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
|
||||
"moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
|
||||
"gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)",
|
||||
"gitlabPATLabel": "رمز دسترسی شخصی GitLab",
|
||||
"about": "درباره",
|
||||
"requiresCredentialsInSettings": "{}: این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
|
||||
"checkOnStart": "بررسی در شروع",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "این اطلاعات را در زیر اضافه کنید",
|
||||
"addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
|
||||
"githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
|
||||
"gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
|
||||
"sortByLastLinkSegment": "فقط بر اساس آخرین بخش پیوند مرتب کنید",
|
||||
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
|
||||
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} و 1 برنامه دیگر ممکن است به روز شده باشند.",
|
||||
"other": "ممکن است {} و {} برنامه های دیگر به روز شده باشند."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Supprimer le filtre d'application obsolète",
|
||||
"showOutdatedOnly": "Afficher uniquement les applications obsolètes",
|
||||
"filter": "Filtre",
|
||||
"filterActive": "Filtre *",
|
||||
"filterApps": "Filtrer les applications",
|
||||
"appName": "Nom de l'application",
|
||||
"author": "Auteur",
|
||||
@ -168,7 +167,7 @@
|
||||
"lastUpdateCheckX": "Vérification de la dernière mise à jour : {}",
|
||||
"remove": "Retirer",
|
||||
"yesMarkUpdated": "Oui, marquer comme mis à jour",
|
||||
"fdroid": "F-Droïde Officiel",
|
||||
"fdroid": "F-Droid Officiel",
|
||||
"appIdOrName": "ID ou nom de l'application",
|
||||
"appId": "ID de l'application",
|
||||
"appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
||||
"dontShowAPKOriginWarnings": "Ne pas afficher les avertissements sur l'origine de l'APK",
|
||||
"moveNonInstalledAppsToBottom": "Déplacer les applications non installées vers le bas de la vue Applications",
|
||||
"gitlabPATLabel": "Jeton d'accès personnel GitLab\\n (permet la recherche et une meilleure découverte d'APK)",
|
||||
"gitlabPATLabel": "Jeton d'accès personnel GitLab",
|
||||
"about": "À propos de",
|
||||
"requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Vérifier les mises à jour au démarrage",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Ajoutez ces informations ci-dessous.",
|
||||
"addInfoInSettings": "Ajoutez ces informations dans les paramètres.",
|
||||
"githubSourceNote": "La limitation du débit GitHub peut être évitée à l'aide d'une clé API.",
|
||||
"gitlabSourceNote": "L'extraction d'APK GitLab peut ne pas fonctionner sans clé API.",
|
||||
"sortByLastLinkSegment": "Trier uniquement sur le dernier segment du lien",
|
||||
"filterReleaseNotesByRegEx": "Filtrer les notes de version par expression régulière",
|
||||
"customLinkFilterRegex": "Filtre de lien APK personnalisé par expression régulière (par défaut '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"une": "{} et 1 application supplémentaire ont peut-être été mises à jour.",
|
||||
"other": "{} et {} autres applications peuvent avoir été mises à jour."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Távolítsa el az elavult app szűrőt",
|
||||
"showOutdatedOnly": "Csak az elavult appok megjelenítése",
|
||||
"filter": "Szűrő",
|
||||
"filterActive": "Szűrő *",
|
||||
"filterApps": "Appok szűrése",
|
||||
"appName": "App név",
|
||||
"author": "Szerző",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
||||
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
|
||||
"moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "Rólunk",
|
||||
"requiresCredentialsInSettings": "{}: Ehhez további hitelesítő adatokra van szükség (a Beállításokban)",
|
||||
"checkOnStart": "Egyszer az alkalmazás indításakor is",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Adja hozzá ezt az infót alább.",
|
||||
"addInfoInSettings": "Adja hozzá ezt az infót a Beállításokban.",
|
||||
"githubSourceNote": "A GitHub sebességkorlátozás elkerülhető API-kulcs használatával.",
|
||||
"gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.",
|
||||
"sortByLastLinkSegment": "Rendezés csak a link utolsó szegmense szerint",
|
||||
"filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel",
|
||||
"customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')",
|
||||
@ -283,14 +281,14 @@
|
||||
"parallelDownloads": "Párhuzamos letöltéseket enged",
|
||||
"installMethod": "Telepítési mód",
|
||||
"normal": "Normál",
|
||||
"root": "Gyökér",
|
||||
"root": "Root",
|
||||
"shizukuBinderNotFound": "A Shizuku nem fut",
|
||||
"useSystemFont": "Használja a rendszer betűtípusát",
|
||||
"systemFontError": "Hiba a rendszer betűtípusának betöltésekor: {}",
|
||||
"useVersionCodeAsOSVersion": "Az app verziókód használata a rendszer által észlelt verzióként",
|
||||
"requestHeader": "Kérelem fejléc",
|
||||
"useLatestAssetDateAsReleaseDate": "Használja a legújabb tartalomfeltöltést megjelenési dátumként",
|
||||
"defaultPseudoVersioningMethod": "Alapértelmezett álversziós módszer",
|
||||
"defaultPseudoVersioningMethod": "Alapértelmezett álverziós módszer",
|
||||
"partialAPKHash": "Részleges APK Hash",
|
||||
"APKLinkHash": "APK Link Hash",
|
||||
"directAPKLink": "Közvetlen APK Link",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} és 1 további alkalmazás is frissült.",
|
||||
"other": "{} és {} további alkalmazás is frissült."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK-k"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Rimuovi il filtro per le app non aggiornate",
|
||||
"showOutdatedOnly": "Mostra solo le app non aggiornate",
|
||||
"filter": "Filtri",
|
||||
"filterActive": "Filtri *",
|
||||
"filterApps": "Filtra app",
|
||||
"appName": "Nome dell'app",
|
||||
"author": "Autore",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Non mostrare gli avvisi 'Solo-Monitoraggio'",
|
||||
"dontShowAPKOriginWarnings": "Non mostrare gli avvisi di origine dell'APK",
|
||||
"moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(attiva la ricerca e migliora la rilevazione di apk)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "Informazioni",
|
||||
"requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)",
|
||||
"checkOnStart": "Controlla una volta all'avvio",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Aggiungi questa info sotto.",
|
||||
"addInfoInSettings": "Aggiungi questa info nelle impostazioni.",
|
||||
"githubSourceNote": "Il limite di ricerca GitHub può essere evitato usando una chiave API.",
|
||||
"gitlabSourceNote": "L'estrazione di APK da GitLab potrebbe non funzionare senza chiave API.",
|
||||
"sortByLastLinkSegment": "Ordina solo in base all'ultimo segmento del collegamento",
|
||||
"filterReleaseNotesByRegEx": "Filtra le note di rilascio con espressione regolare",
|
||||
"customLinkFilterRegex": "Filtra link APK personalizzato con espressione regolare (predefinito '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} e un'altra app potrebbero essere state aggiornate.",
|
||||
"other": "{} e altre {} app potrebbero essere state aggiornate."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "アップデートが存在するアプリのフィルターを解除",
|
||||
"showOutdatedOnly": "アップデートが存在するアプリのみ表示する",
|
||||
"filter": "フィルター",
|
||||
"filterActive": "フィルター *",
|
||||
"filterApps": "アプリをフィルタリングする",
|
||||
"appName": "アプリ名",
|
||||
"author": "作者",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
|
||||
"dontShowAPKOriginWarnings": "APKのダウンロード元の警告を表示しない",
|
||||
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
|
||||
"gitlabPATLabel": "GitLab パーソナルアクセストークン\n(検索とより良いAPK検出の有効化)",
|
||||
"gitlabPATLabel": "GitLab パーソナルアクセストークン",
|
||||
"about": "概要",
|
||||
"requiresCredentialsInSettings": "{}: これには追加の認証が必要です (設定にて)",
|
||||
"checkOnStart": "起動時にアップデートを確認する",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "下部でこの情報を追加してください。",
|
||||
"addInfoInSettings": "設定でこの情報を追加してください。",
|
||||
"githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。",
|
||||
"gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。",
|
||||
"sortByLastLinkSegment": "リンクの最後のセグメントのみでソートする",
|
||||
"filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする",
|
||||
"customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります。",
|
||||
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります。"
|
||||
},
|
||||
"apk": {
|
||||
"one": "{}APK",
|
||||
"other": "{}APK"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Verwijder out-of-date app filter",
|
||||
"showOutdatedOnly": "Toon alleen out-of-date apps",
|
||||
"filter": "Filter",
|
||||
"filterActive": "Filteren *",
|
||||
"filterApps": "Filter apps",
|
||||
"appName": "App naam",
|
||||
"author": "Auteur",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Geen waarschuwingen voor 'Track-Only' weergeven",
|
||||
"dontShowAPKOriginWarnings": "APK-herkomstwaarschuwingen niet weergeven",
|
||||
"moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Maakt het mogelijk beter te zoeken naar APK's)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "Over",
|
||||
"requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)",
|
||||
"checkOnStart": "Controleren op updates bij opstarten",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Voeg deze informatie hieronder toe.",
|
||||
"addInfoInSettings": "Voeg deze informatie toe in de instellingen.",
|
||||
"githubSourceNote": "Beperkingen van GitHub kunnen worden vermeden door het gebruik van een API-sleutel.",
|
||||
"gitlabSourceNote": "GitLab APK-extractie werkt mogelijk niet zonder een API-sleutel.",
|
||||
"sortByLastLinkSegment": "Sorteren op alleen het laatste segment van de link",
|
||||
"filterReleaseNotesByRegEx": "Filter release-opmerkingen met een reguliere expressie.",
|
||||
"customLinkFilterRegex": "Aangepaste APK-linkfilter met een reguliere expressie (Standaard '.apk$').",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} en nog 1 app zijn mogelijk bijgewerkt.",
|
||||
"other": "{} en {} meer apps zijn mogelijk bijgwerkt."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK's"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Usuń filtr nieaktualnych aplikacji",
|
||||
"showOutdatedOnly": "Pokaż tylko nieaktualne aplikacje",
|
||||
"filter": "FIltr",
|
||||
"filterActive": "Filtruj *",
|
||||
"filterApps": "Filtruj aplikacje",
|
||||
"appName": "Nazwa aplikacji",
|
||||
"author": "Autor",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Nie pokazuj ostrzeżeń \"Tylko obserwowana\"",
|
||||
"dontShowAPKOriginWarnings": "Nie pokazuj ostrzeżeń o pochodzeniu APK",
|
||||
"moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji",
|
||||
"gitlabPATLabel": "Osobisty token dostępu GitLab\n(Umożliwia wyszukiwanie i lepsze wykrywanie APK)",
|
||||
"gitlabPATLabel": "Osobisty token dostępu GitLab",
|
||||
"about": "Więcej informacji",
|
||||
"requiresCredentialsInSettings": "{}: Wymaga to dodatkowych poświadczeń (w Ustawieniach)",
|
||||
"checkOnStart": "Sprawdź aktualizacje przy uruchomieniu",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Dodaj tę informację poniżej.",
|
||||
"addInfoInSettings": "Dodaj tę informację w Ustawieniach.",
|
||||
"githubSourceNote": "Limit żądań GitHub można ominąć za pomocą klucza API.",
|
||||
"gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.",
|
||||
"sortByLastLinkSegment": "Sortuj tylko według ostatniego segmentu łącza",
|
||||
"filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego",
|
||||
"customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
|
||||
@ -378,5 +376,9 @@
|
||||
"few": "{} i {} inne apki mogły zostać zaktualizowane.",
|
||||
"many": "{} i {} innych apek mogło zostać zaktualizowanych.",
|
||||
"other": "{} i {} inne apki mogły zostać zaktualizowane."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Remover filtro de aplicativos desatualizados",
|
||||
"showOutdatedOnly": "Mostrar apenas aplicativos desatualizados",
|
||||
"filter": "Filtro",
|
||||
"filterActive": "Filtro *",
|
||||
"filterApps": "Filtrar aplicativos",
|
||||
"appName": "Nome do aplicativo",
|
||||
"author": "Autor",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas monitorar'",
|
||||
"dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK",
|
||||
"moveNonInstalledAppsToBottom": "Mover aplicativos não instalados para o fundo da lista de aplicativos",
|
||||
"gitlabPATLabel": "Token de acesso pessoal do Gitlab\n(Ativa pesquisa e melhora a descoberta de APKs)",
|
||||
"gitlabPATLabel": "Token de acesso pessoal do Gitlab",
|
||||
"about": "Sobre",
|
||||
"requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)",
|
||||
"checkOnStart": "Verificar se há atualizações ao iniciar",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Adicionar essa informação abaixo.",
|
||||
"addInfoInSettings": "Adicionar essa informação nas configurações.",
|
||||
"githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.",
|
||||
"gitlabSourceNote": "A extração de endereço de download do APK no GitLab provavelmente não funcione sem que seja fornecido uma chave de API.",
|
||||
"sortByLastLinkSegment": "Ordenar apenas usando o último segmento do link",
|
||||
"filterReleaseNotesByRegEx": "Filtrar notas de versão usando Regex",
|
||||
"customLinkFilterRegex": "Filtro de link personalizado usando expressão regular (Padrão '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} e um outro aplicativo podem ter sido atualizados.",
|
||||
"other": "{} e {} outros aplicativos podem ter sido atualizados."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Удалить фильтр для устаревших приложений",
|
||||
"showOutdatedOnly": "Показывать только устаревшие приложения",
|
||||
"filter": "Фильтр",
|
||||
"filterActive": "Фильтр *",
|
||||
"filterApps": "Фильтровать приложения",
|
||||
"appName": "Название приложения",
|
||||
"author": "Автор",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Не показывать предупреждения о только отслеживаемых приложениях",
|
||||
"dontShowAPKOriginWarnings": "Не показывать предупреждения об отличающемся источнике APK-файлов",
|
||||
"moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка",
|
||||
"gitlabPATLabel": "Персональный токен доступа GitLab\n(включает поиск и улучшает обнаружение APK)",
|
||||
"gitlabPATLabel": "Персональный токен доступа GitLab",
|
||||
"about": "Описание",
|
||||
"requiresCredentialsInSettings": "{}: Для этого требуются дополнительные учетные данные (в настройках)",
|
||||
"checkOnStart": "Проверять наличие обновлений при запуске",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Добавьте эту информацию ниже",
|
||||
"addInfoInSettings": "Добавьте эту информацию в Настройки",
|
||||
"githubSourceNote": "Используя ключ API можно обойти лимит запросов GitHub",
|
||||
"gitlabSourceNote": "Без ключа API может не работать извлечение APK с GitLab",
|
||||
"sortByLastLinkSegment": "Сортировать только по последнему сегменту ссылки",
|
||||
"filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
|
||||
"customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} и ещё 1 приложение могли быть обновлены",
|
||||
"other": "{} и ещё {} приложений могли быть обновлены"
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -58,11 +58,10 @@ const main = async () => {
|
||||
return `${translationsDir}/${f}`
|
||||
}).filter(f => f.endsWith('.json') && f != templateFile)
|
||||
|
||||
const templateTranslation = require(templateFile)
|
||||
|
||||
const templateTranslation = JSON.parse(fs.readFileSync(templateFile).toString())
|
||||
|
||||
otherFiles.forEach(file => {
|
||||
const thisTranslationOriginal = require(file)
|
||||
const thisTranslationOriginal = JSON.parse(fs.readFileSync((file).toString()))
|
||||
const thisTranslationNew = {}
|
||||
Object.keys(templateTranslation).forEach(k => {
|
||||
thisTranslationNew[k] = thisTranslationOriginal[k] || templateTranslation[k]
|
||||
@ -72,7 +71,7 @@ const main = async () => {
|
||||
|
||||
for (let i in otherFiles) {
|
||||
const file = otherFiles[i]
|
||||
const thisTranslation = require(file)
|
||||
const thisTranslation = JSON.parse(fs.readFileSync((file).toString()))
|
||||
const translationKeys = Object.keys(templateTranslation)
|
||||
for (let j in translationKeys) {
|
||||
const k = translationKeys[j]
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Ta bort Utgånga App-filtret",
|
||||
"showOutdatedOnly": "Visa Endast Utgånga Appar",
|
||||
"filter": "Filtrera",
|
||||
"filterActive": "Filter *",
|
||||
"filterApps": "Filtrera Appar",
|
||||
"appName": "Appnamn",
|
||||
"author": "Utvecklare",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Visa inte 'Följ-Endast' varningar",
|
||||
"dontShowAPKOriginWarnings": "Visa inte APK-ursprung varningar",
|
||||
"moveNonInstalledAppsToBottom": "Flytta icke-installerade appar till botten av appvyn",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\\n(Möjliggör sökning och bättre APK-upptäckt)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "Om",
|
||||
"requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Kolla efter uppdateringar vid start",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Lägg till denna information nedanför.",
|
||||
"addInfoInSettings": "Lägg till denna information i Inställningar.",
|
||||
"githubSourceNote": "GitHub-hastighetsbegränsning kan undvikas med en API-nyckel.",
|
||||
"gitlabSourceNote": "GitLab APK-extraktion kanske inte fungerar utan en API-nyckel.",
|
||||
"sortByLastLinkSegment": "Sortera endast efter det sista segmentet av länken",
|
||||
"filterReleaseNotesByRegEx": "Filtrera versionskommentarer efter reguljärt uttryck",
|
||||
"customLinkFilterRegex": "Anpassad APK-länkfiltrera efter reguljärt uttryck (standard '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} och 1 till app kan ha uppdaterats.",
|
||||
"other": "{} och {} appar till kan ha uppdaterats."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK:er"
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "Güncel Olmayan Uygulama Filtresini Kaldır",
|
||||
"showOutdatedOnly": "Yalnızca Güncel Olmayan Uygulamaları Göster",
|
||||
"filter": "Filtre",
|
||||
"filterActive": "Filtre *",
|
||||
"filterApps": "Uygulamaları Filtrele",
|
||||
"appName": "Uygulama Adı",
|
||||
"author": "Yazar",
|
||||
@ -219,7 +218,7 @@
|
||||
"dontShowTrackOnlyWarnings": "'Yalnızca Takip Edilen' uyarılarını gösterme",
|
||||
"dontShowAPKOriginWarnings": "APK kaynağı uyarılarını gösterme",
|
||||
"moveNonInstalledAppsToBottom": "Yüklenmemiş Uygulamaları Uygulamalar Görünümünün Altına Taşı",
|
||||
"gitlabPATLabel": "GitLab Kişisel Erişim Belirteci\n(Arama ve Daha İyi APK Keşfi İçin)",
|
||||
"gitlabPATLabel": "GitLab Kişisel Erişim Belirteci",
|
||||
"about": "Hakkında",
|
||||
"requiresCredentialsInSettings": "{}: Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)",
|
||||
"checkOnStart": "Başlangıçta güncellemeleri kontrol et",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "Bu bilgiyi aşağıya ekle.",
|
||||
"addInfoInSettings": "Bu bilgiyi Ayarlar'da ekleyin.",
|
||||
"githubSourceNote": "GitHub hız sınırlaması bir API anahtarı kullanılarak atlanabilir.",
|
||||
"gitlabSourceNote": "GitLab APK çıkarma işlemi bir API anahtarı olmadan çalışmayabilir.",
|
||||
"sortByLastLinkSegment": "Bağlantının yalnızca son bölümüne göre sırala",
|
||||
"filterReleaseNotesByRegEx": "Sürüm Notlarını Düzenli İfade ile Filtrele",
|
||||
"customLinkFilterRegex": "Özel APK Bağlantı Filtresi Düzenli İfade ile (Varsayılan '.apk$')",
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} ve 1 diğer uygulama muhtemelen güncellendi.",
|
||||
"other": "{} ve {} daha fazla uygulama muhtemelen güncellendi."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK'lar"
|
||||
}
|
||||
}
|
||||
|
359
assets/translations/uk.json
Normal file
359
assets/translations/uk.json
Normal file
@ -0,0 +1,359 @@
|
||||
{
|
||||
"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": "Колір",
|
||||
"githubStarredRepos": "Відзначені репозиторії GitHub",
|
||||
"uname": "Ім'я користувача",
|
||||
"wrongArgNum": "Надано неправильну кількість аргументів",
|
||||
"xIsTrackOnly": "{} - тільки відстежування",
|
||||
"source": "Джерело",
|
||||
"app": "застосунок",
|
||||
"appsFromSourceAreTrackOnly": "Додатки з цього джерела є лише для відстежування.",
|
||||
"youPickedTrackOnly": "Ви вибрали опцію лише для відстежування.",
|
||||
"trackOnlyAppDescription": "Застосунок буде відстежуватися для оновлень, але Obtainium не зможе його завантажити або встановити.",
|
||||
"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": "{} буде видалено з Obtainium, але залишиться встановленим на пристрої.",
|
||||
"removeSelectedAppsQuestion": "Видалити вибрані додатки?",
|
||||
"removeSelectedApps": "Видалити вибрані додатки",
|
||||
"updateX": "Оновити {}",
|
||||
"installX": "Встановити {}",
|
||||
"markXTrackOnlyAsUpdated": "Позначити {}\n(Тільки відстежування)\nяк оновлено",
|
||||
"changeX": "Змінити {}",
|
||||
"installUpdateApps": "Встановити/Оновити додатки",
|
||||
"installUpdateSelectedApps": "Встановити/Оновити вибрані додатки",
|
||||
"markXSelectedAppsAsUpdated": "Позначити {} вибрані додатки як оновлені?",
|
||||
"no": "Ні",
|
||||
"yes": "Так",
|
||||
"markSelectedAppsUpdated": "Позначити вибрані додатки як оновлені",
|
||||
"pinToTop": "Закріпити угорі",
|
||||
"unpinFromTop": "Відкріпити зверху",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Скинути статус встановлення для вибраних додатків?",
|
||||
"installStatusOfXWillBeResetExplanation": "Статус встановлення будь-яких вибраних додатків буде скинутий.\n\nЦе може допомогти, коли версія застосунку, відображена в Obtainium, є неправильною через невдалі оновлення або інші проблеми.",
|
||||
"customLinkMessage": "Ці посилання працюють на пристроях з встановленим Obtainium",
|
||||
"shareAppConfigLinks": "Поділитися посиланнями на конфігурацію Застосунку як HTML",
|
||||
"shareSelectedAppURLs": "Поділитися вибраними URL-адресами додатків",
|
||||
"resetInstallStatus": "Скинути статус встановлення",
|
||||
"more": "Більше",
|
||||
"removeOutdatedFilter": "Видалити фільтр застарілих додатків",
|
||||
"showOutdatedOnly": "Показати лише застарілі додатки",
|
||||
"filter": "Фільтр",
|
||||
"filterApps": "Фільтрувати додатки",
|
||||
"appName": "Назва застосунку",
|
||||
"author": "Автор",
|
||||
"upToDateApps": "Актуальні додатки",
|
||||
"nonInstalledApps": "Невстановлені додатки",
|
||||
"importExport": "Імпорт/Експорт",
|
||||
"settings": "Налаштування",
|
||||
"exportedTo": "Експортовано в {}",
|
||||
"obtainiumExport": "Експорт з Obtainium",
|
||||
"invalidInput": "Недійсний ввід",
|
||||
"importedX": "Імпортовано {}",
|
||||
"obtainiumImport": "Імпорт в Obtainium",
|
||||
"importFromURLList": "Імпорт зі списку URL-адрес",
|
||||
"searchQuery": "Пошуковий запит",
|
||||
"appURLList": "Список URL-адрес додатків",
|
||||
"line": "Лінія",
|
||||
"searchX": "Пошук {}",
|
||||
"noResults": "Результати відсутні",
|
||||
"importX": "Імпорт {}",
|
||||
"importedAppsIdDisclaimer": "Імпортовані додатки можуть неправильно відображатися як \"Не встановлені\".\nДля виправлення цього перевстановіть їх через Obtainium.\nЦе не повинно вплинути на дані додатків.\n\nПов'язано лише з URL-адресами та імпортом від третіх сторін.",
|
||||
"importErrors": "Помилки імпорту",
|
||||
"importedXOfYApps": "Імпортовано {} з {} додатків.",
|
||||
"followingURLsHadErrors": "Помилки в наступних URL-адресах:",
|
||||
"selectURL": "Вибрати URL",
|
||||
"selectURLs": "Вибрати URL-адреси",
|
||||
"pick": "Вибрати",
|
||||
"theme": "Тема",
|
||||
"dark": "Темна",
|
||||
"light": "Світла",
|
||||
"followSystem": "Дотримуватися системи",
|
||||
"useBlackTheme": "Використовувати чисто чорну темну тему",
|
||||
"appSortBy": "Сортувати додатки за",
|
||||
"authorName": "Автор/Назва",
|
||||
"nameAuthor": "Назва/Автор",
|
||||
"asAdded": "За додаванням",
|
||||
"appSortOrder": "Порядок сортування додатків",
|
||||
"ascending": "За зростанням",
|
||||
"descending": "За спаданням",
|
||||
"bgUpdateCheckInterval": "Інтервал перевірки оновлень у фоновому режимі",
|
||||
"neverManualOnly": "Ніколи - Тільки вручну",
|
||||
"appearance": "Вигляд",
|
||||
"showWebInAppView": "Показати джерело застосунку у вигляді веб-сторінки",
|
||||
"pinUpdates": "Закріпити оновлення у верхній частині вигляду додатків",
|
||||
"updates": "Оновлення",
|
||||
"sourceSpecific": "Певне джерело",
|
||||
"appSource": "Джерело застосунку",
|
||||
"noLogs": "Немає логів",
|
||||
"appLogs": "Лог застосунку",
|
||||
"close": "Закрити",
|
||||
"share": "Поділитися",
|
||||
"appNotFound": "Застосунок не знайдено",
|
||||
"obtainiumExportHyphenatedLowercase": "експорт з Obtainium",
|
||||
"pickAnAPK": "Вибрати APK",
|
||||
"appHasMoreThanOnePackage": "{} має більше одного пакету:",
|
||||
"deviceSupportsXArch": "Ваш пристрій підтримує архітектуру процесора {}.",
|
||||
"deviceSupportsFollowingArchs": "Ваш пристрій підтримує наступні архітектури процесора:",
|
||||
"warning": "Попередження",
|
||||
"sourceIsXButPackageFromYPrompt": "Джерело застосунку - '{}' але пакет випуску походить з '{}'. Продовжити?",
|
||||
"updatesAvailable": "Доступні оновлення",
|
||||
"updatesAvailableNotifDescription": "Повідомляє користувача, що доступні оновлення для одного чи декількох додатків, які відстежує Obtainium",
|
||||
"noNewUpdates": "Немає нових оновлень.",
|
||||
"xHasAnUpdate": "{} має оновлення.",
|
||||
"appsUpdated": "Додатки оновлено",
|
||||
"appsUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного чи декількох додатків було застосовано в фоновому режимі",
|
||||
"xWasUpdatedToY": "{} було оновлено до {}.",
|
||||
"errorCheckingUpdates": "Помилка перевірки оновлень",
|
||||
"errorCheckingUpdatesNotifDescription": "Повідомлення, яке з'являється, коли перевірка оновлень в фоновому режимі завершується невдачею",
|
||||
"appsRemoved": "Додатки видалено",
|
||||
"appsRemovedNotifDescription": "Повідомляє користувача, що один чи декілька додатків були видалені через помилки при завантаженні",
|
||||
"xWasRemovedDueToErrorY": "{} було видалено через цю помилку: {}",
|
||||
"completeAppInstallation": "Завершення установки застосунку",
|
||||
"obtainiumMustBeOpenToInstallApps": "Для встановлення додатків Obtainium має бути відкритий",
|
||||
"completeAppInstallationNotifDescription": "Прохання користувача повернутися до Obtainium для завершення установки застосунку",
|
||||
"checkingForUpdates": "Перевірка оновлень",
|
||||
"checkingForUpdatesNotifDescription": "Тимчасове повідомлення, яке з'являється при перевірці оновлень",
|
||||
"pleaseAllowInstallPerm": "Будь ласка, дозвольте Obtainium встановлювати додатки",
|
||||
"trackOnly": "Тільки відстеження",
|
||||
"errorWithHttpStatusCode": "Помилка {} HTTP-коду",
|
||||
"versionCorrectionDisabled": "Виправлення версії вимкнено (здається, плагін не працює)",
|
||||
"unknown": "Невідомо",
|
||||
"none": "Нічого",
|
||||
"never": "Ніколи",
|
||||
"latestVersionX": "Остання версія: {}",
|
||||
"installedVersionX": "Встановлено: {}",
|
||||
"lastUpdateCheckX": "Остання перевірка оновлень: {}",
|
||||
"remove": "Видалити",
|
||||
"yesMarkUpdated": "Так, позначити як оновлене",
|
||||
"fdroid": "F-Droid Офіційний",
|
||||
"appIdOrName": "Ідентифікатор або назва застосунку",
|
||||
"appId": "Ідентифікатор застосунку",
|
||||
"appWithIdOrNameNotFound": "Застосунок з таким ідентифікатором або назвою не знайдено",
|
||||
"reposHaveMultipleApps": "Сховища можуть містити кілька додатків",
|
||||
"fdroidThirdPartyRepo": "F-Droid Стороннє сховище",
|
||||
"steamMobile": "Мобільний Steam",
|
||||
"steamChat": "Чат Steam",
|
||||
"install": "Встановити",
|
||||
"markInstalled": "Позначити як встановлене",
|
||||
"update": "Оновити",
|
||||
"markUpdated": "Позначити як оновлене",
|
||||
"additionalOptions": "Додаткові опції",
|
||||
"disableVersionDetection": "Вимкнути визначення версії",
|
||||
"noVersionDetectionExplanation": "Цю опцію слід використовувати лише для додатків, де визначення версії працює неправильно.",
|
||||
"downloadingX": "Завантаження {}",
|
||||
"downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку",
|
||||
"noAPKFound": "APK не знайдено",
|
||||
"noVersionDetection": "Визначення версії відключено",
|
||||
"categorize": "Категоризувати",
|
||||
"categories": "Категорії",
|
||||
"category": "Категорія",
|
||||
"noCategory": "Без категорії",
|
||||
"noCategories": "Немає категорій",
|
||||
"deleteCategoriesQuestion": "Видалити категорії?",
|
||||
"categoryDeleteWarning": "Усі додатки у видалених категоріях будуть переведені у некатегоризовані.",
|
||||
"addCategory": "Додати категорію",
|
||||
"label": "Мітка",
|
||||
"language": "Мова",
|
||||
"copiedToClipboard": "Скопійовано в буфер обміну",
|
||||
"storagePermissionDenied": "Відмовлено у дозволі на доступ до сховища",
|
||||
"selectedCategorizeWarning": "Це замінить будь-які існуючі налаштування категорій для вибраних додатків.",
|
||||
"filterAPKsByRegEx": "Фільтрувати APK за регулярним виразом",
|
||||
"removeFromObtainium": "Видалити з Obtainium",
|
||||
"uninstallFromDevice": "Видалити з пристрою",
|
||||
"onlyWorksWithNonVersionDetectApps": "Працює лише з застосунками з вимкненим визначенням версії.",
|
||||
"releaseDateAsVersion": "Використовувати дату випуску як рядок версії",
|
||||
"releaseDateAsVersionExplanation": "Цю опцію слід використовувати лише для додатків, де визначення версії працює неправильно, але є дата випуску.",
|
||||
"changes": "Зміни",
|
||||
"releaseDate": "Дата випуску",
|
||||
"importFromURLsInFile": "Імпорт з URL-адрес у файлі (наприклад, OPML)",
|
||||
"versionDetectionExplanation": "Порівняти рядок версії з версією, визначеною операційною системою",
|
||||
"versionDetection": "Визначення версії",
|
||||
"standardVersionDetection": "Стандартне визначення версії",
|
||||
"groupByCategory": "Групувати за категоріями",
|
||||
"autoApkFilterByArch": "Спробувати фільтрувати APK за архітектурою ЦП, якщо можливо",
|
||||
"overrideSource": "Перевизначити джерело",
|
||||
"dontShowAgain": "Не показувати це знову",
|
||||
"dontShowTrackOnlyWarnings": "Не показувати попередження про 'Тільки відстеження'",
|
||||
"dontShowAPKOriginWarnings": "Не показувати попередження про походження APK",
|
||||
"moveNonInstalledAppsToBottom": "Перемістити невстановлені додатки вниз у перегляді додатків",
|
||||
"gitlabPATLabel": "Особистий токен GitLab (Увімкнення пошуку та краще виявлення APK)",
|
||||
"about": "Про програму",
|
||||
"requiresCredentialsInSettings": "{} потребує додаткових облікових даних (у налаштуваннях)",
|
||||
"checkOnStart": "Перевірити наявність оновлень при запуску",
|
||||
"tryInferAppIdFromCode": "Спробувати вивести ідентифікатор застосунку з вихідного коду",
|
||||
"removeOnExternalUninstall": "Автоматично видаляти додатки, які було видалено зовнішнім чином",
|
||||
"pickHighestVersionCode": "Автоматично вибрати APK з найвищим кодом версії",
|
||||
"checkUpdateOnDetailPage": "Перевіряти наявність оновлень при відкритті сторінки деталей застосунку",
|
||||
"disablePageTransitions": "Вимкнути анімації переходів між сторінками",
|
||||
"reversePageTransitions": "Зворотні анімації переходів між сторінками",
|
||||
"minStarCount": "Мінімальна кількість зірок",
|
||||
"addInfoBelow": "Додати цю інформацію нижче.",
|
||||
"addInfoInSettings": "Додати цю інформацію у налаштуваннях.",
|
||||
"githubSourceNote": "Лімітування швидкості GitHub можна уникнути, використовуючи ключ API.",
|
||||
"gitlabSourceNote": "Вилучення APK з GitLab може не працювати без ключа API.",
|
||||
"sortByLastLinkSegment": "Сортувати лише за останнім сегментом посилання",
|
||||
"filterReleaseNotesByRegEx": "Фільтрувати примітки до релізу за регулярним виразом",
|
||||
"customLinkFilterRegex": "Фільтр кастомного посилання на APK за регулярним виразом (за замовчуванням '.apk$')",
|
||||
"appsPossiblyUpdated": "Оновлення додатків спробовано",
|
||||
"appsPossiblyUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного або декількох додатків можливо були застосовані в фоновому режимі",
|
||||
"xWasPossiblyUpdatedToY": "{} можливо було оновлено до {}.",
|
||||
"enableBackgroundUpdates": "Увімкнути оновлення в фоновому режимі",
|
||||
"backgroundUpdateReqsExplanation": "Оновлення в фоновому режимі може бути неможливим для всіх додатків.",
|
||||
"backgroundUpdateLimitsExplanation": "Успіх фонової установки може бути визначений лише після відкриття Obtainium.",
|
||||
"verifyLatestTag": "Перевірити тег 'latest'",
|
||||
"intermediateLinkRegex": "Фільтр для 'Проміжного' Посилання для Відвідування",
|
||||
"filterByLinkText": "Фільтрувати посилання за текстом посилання",
|
||||
"intermediateLinkNotFound": "Проміжне посилання не знайдено",
|
||||
"intermediateLink": "Проміжне посилання",
|
||||
"exemptFromBackgroundUpdates": "Виключено з фонових оновлень (якщо ввімкнено)",
|
||||
"bgUpdatesOnWiFiOnly": "Вимкнути фонові оновлення поза Wi-Fi",
|
||||
"autoSelectHighestVersionCode": "Автоматичний вибір APK з найвищим кодом версії",
|
||||
"versionExtractionRegEx": "Регулярний вираз для вилучення рядка версії",
|
||||
"matchGroupToUse": "Група співпадінь для використання в регулярному виразі вилучення версії",
|
||||
"highlightTouchTargets": "Підсвічувати менш очевидні області дотику",
|
||||
"pickExportDir": "Вибрати каталог експорту",
|
||||
"autoExportOnChanges": "Автоматичний експорт при змінах",
|
||||
"includeSettings": "Включити налаштування",
|
||||
"filterVersionsByRegEx": "Фільтрувати версії за регулярним виразом",
|
||||
"trySelectingSuggestedVersionCode": "Спробуйте вибрати запропонований код версії APK",
|
||||
"dontSortReleasesList": "Зберігати порядок випуску з API",
|
||||
"reverseSort": "Зворотне сортування",
|
||||
"takeFirstLink": "Вибрати перше посилання",
|
||||
"skipSort": "Пропустити сортування",
|
||||
"debugMenu": "Меню налагодження",
|
||||
"bgTaskStarted": "Запущено фонове завдання - перевірте журнали.",
|
||||
"runBgCheckNow": "Запустити перевірку оновлень в фоновому режимі зараз",
|
||||
"versionExtractWholePage": "Застосувати регулярний вираз вилучення версії до всієї сторінки",
|
||||
"installing": "Встановлення",
|
||||
"skipUpdateNotifications": "Пропустити сповіщення про оновлення",
|
||||
"updatesAvailableNotifChannel": "Доступні оновлення",
|
||||
"appsUpdatedNotifChannel": "Додатки оновлені",
|
||||
"appsPossiblyUpdatedNotifChannel": "Спроба оновлення додатків",
|
||||
"errorCheckingUpdatesNotifChannel": "Помилка перевірки оновлень",
|
||||
"appsRemovedNotifChannel": "Додатки видалені",
|
||||
"downloadingXNotifChannel": "Завантаження {}",
|
||||
"completeAppInstallationNotifChannel": "Завершення встановлення застосунку",
|
||||
"checkingForUpdatesNotifChannel": "Перевірка оновлень",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Перевіряти лише встановлені та додатки, які відстежуються для оновлень",
|
||||
"supportFixedAPKURL": "Підтримка фіксованих посилань на APK",
|
||||
"selectX": "Вибрати {}",
|
||||
"parallelDownloads": "Дозволити паралельні завантаження",
|
||||
"installMethod": "Метод встановлення",
|
||||
"normal": "Звичайний",
|
||||
"root": "Root",
|
||||
"shizukuBinderNotFound": "Сумісний сервіс Shizuku не було знайдено",
|
||||
"useSystemFont": "Використовувати системний шрифт",
|
||||
"systemFontError": "Помилка завантаження системного шрифту: {}",
|
||||
"useVersionCodeAsOSVersion": "Використовувати код версії застосунку як версію, визначену операційною системою",
|
||||
"requestHeader": "Заголовок запиту",
|
||||
"useLatestAssetDateAsReleaseDate": "Використовувати останню дату завантаження ресурсу як дату випуску",
|
||||
"defaultPseudoVersioningMethod": "Метод за замовчуванням псевдо-версіонування",
|
||||
"partialAPKHash": "Хеш часткового APK",
|
||||
"APKLinkHash": "Хеш посилання на APK",
|
||||
"directAPKLink": "Пряме посилання на APK",
|
||||
"pseudoVersionInUse": "Використовується псевдо-версія",
|
||||
"installed": "Встановлено",
|
||||
"latest": "Остання",
|
||||
"invertRegEx": "Інвертувати регулярний вираз",
|
||||
"note": "Примітка",
|
||||
"selfHostedNote": "Випадаючий список \"{}\" може використовуватися для доступу до власних/призначених для самостійного використання екземплярів будь-якого джерела.",
|
||||
"badDownload": "APK не вдалося розпарсити (несумісний або часткове завантаження)",
|
||||
"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": "{} і {} інших додатків було оновлено."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} і 1 інше Застосунок можливо було оновлено.",
|
||||
"other": "{} і {} інших додатків можливо було оновлено."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK-файли"
|
||||
}
|
||||
}
|
@ -72,7 +72,7 @@
|
||||
"unpinFromTop": "Bỏ ghim khỏi đầu trang",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Đặt lại trạng thái cài đặt cho ứng dụng đã chọn?",
|
||||
"installStatusOfXWillBeResetExplanation": "Trạng thái cài đặt của mọi Ứng dụng đã chọn sẽ được đặt lại.\n\nĐiều này có thể hữu ích khi phiên bản Ứng dụng hiển thị trong Obtainium không chính xác do cập nhật không thành công hoặc các sự cố khác.",
|
||||
"customLinkMessage": "Các liên kết này hoạt động trên các thiết bị có cài đặt Gainium",
|
||||
"customLinkMessage": "Các liên kết này hoạt động trên các thiết bị có cài đặt Obtainium",
|
||||
"shareAppConfigLinks": "Chia sẻ cấu hình ứng dụng dưới dạng liên kết HTML",
|
||||
"shareSelectedAppURLs": "Chia sẻ URL ứng dụng đã chọn",
|
||||
"resetInstallStatus": "Đặt lại trạng thái cài đặt",
|
||||
@ -80,14 +80,13 @@
|
||||
"removeOutdatedFilter": "Xóa bộ lọc ứng dụng lỗi thời",
|
||||
"showOutdatedOnly": "Chỉ hiển thị các ứng dụng lỗi thời",
|
||||
"filter": "Lọc",
|
||||
"filterActive": "Lọc *",
|
||||
"filterApps": "Lọc ứng dụng",
|
||||
"appName": "Tên ứng dụng",
|
||||
"author": "Tác giả",
|
||||
"upToDateApps": "Ứng dụng cập nhật",
|
||||
"nonInstalledApps": "Ứng dụng chưa được cài đặt",
|
||||
"importExport": "Nhập/Xuất",
|
||||
"settings": "Cài đặt",
|
||||
"settings": "Thiết đặt",
|
||||
"exportedTo": "Đã xuất sang {}",
|
||||
"obtainiumExport": "Xuất",
|
||||
"invalidInput": "Đầu vào không hợp lệ",
|
||||
@ -132,7 +131,7 @@
|
||||
"close": "Đóng",
|
||||
"share": "Chia sẻ",
|
||||
"appNotFound": "Không tìm thấy ứng dụng",
|
||||
"obtainiumExportHyphenatedLowercase": "xuất khẩu-obtainium",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Chọn một APK",
|
||||
"appHasMoreThanOnePackage": "{} có nhiều gói:",
|
||||
"deviceSupportsXArch": "Thiết bị của bạn hỗ trợ kiến trúc CPU {}.",
|
||||
@ -168,7 +167,7 @@
|
||||
"lastUpdateCheckX": "Kiểm tra cập nhật lần cuối: {}",
|
||||
"remove": "Loại bỏ",
|
||||
"yesMarkUpdated": "Có, Đánh dấu là đã cập nhật",
|
||||
"fdroid": "Chính thức của F-Droid",
|
||||
"fdroid": "F-Droid Chính thức",
|
||||
"appIdOrName": "ID hoặc tên ứng dụng",
|
||||
"appId": "ID ứng dụng",
|
||||
"appWithIdOrNameNotFound": "Không tìm thấy ứng dụng nào có ID hoặc tên đó",
|
||||
@ -188,18 +187,18 @@
|
||||
"noAPKFound": "Không tìm thấy APK",
|
||||
"noVersionDetection": "Không phát hiện phiên bản",
|
||||
"categorize": "Phân loại",
|
||||
"categories": "Thể loại",
|
||||
"category": "Thể loại",
|
||||
"noCategory": "Không thể loại",
|
||||
"noCategories": "Không thể loại",
|
||||
"deleteCategoriesQuestion": "Xóa thể loại?",
|
||||
"categoryDeleteWarning": "Tất cả ứng dụng trong thể loại đã xóa sẽ được đặt thành chưa được phân loại.",
|
||||
"categories": "Danh mục",
|
||||
"category": "Danh mục",
|
||||
"noCategory": "Không danh mục",
|
||||
"noCategories": "Không danh mục",
|
||||
"deleteCategoriesQuestion": "Xóa danh mục?",
|
||||
"categoryDeleteWarning": "Tất cả ứng dụng trong danh mục đã xóa sẽ được đặt thành chưa được phân loại.",
|
||||
"addCategory": "Thêm thể loại",
|
||||
"label": "Nhãn",
|
||||
"language": "Ngôn ngữ",
|
||||
"copiedToClipboard": "Sao chép vào clipboard",
|
||||
"storagePermissionDenied": "Quyền lưu trữ bị từ chối",
|
||||
"selectedCategorizeWarning": "Điều này sẽ thay thế mọi cài đặt thể loại hiện có cho Ứng dụng đã chọn.",
|
||||
"selectedCategorizeWarning": "Điều này sẽ thay thế mọi thiết đặt danh mục hiện có cho Ứng dụng đã chọn.",
|
||||
"filterAPKsByRegEx": "Lọc APK theo biểu thức chính quy",
|
||||
"removeFromObtainium": "Loại khỏi Obtainium",
|
||||
"uninstallFromDevice": "Gỡ cài đặt khỏi thiết bị",
|
||||
@ -212,16 +211,16 @@
|
||||
"versionDetectionExplanation": "Đối chiếu chuỗi phiên bản với phiên bản được phát hiện từ hệ điều hành",
|
||||
"versionDetection": "Phát hiện phiên bản",
|
||||
"standardVersionDetection": "Phát hiện phiên bản tiêu chuẩn",
|
||||
"groupByCategory": "Nhóm theo thể loại",
|
||||
"groupByCategory": "Nhóm theo danh mục",
|
||||
"autoApkFilterByArch": "Cố gắng lọc APK theo kiến trúc CPU nếu có thể",
|
||||
"overrideSource": "Ghi đè nguồn",
|
||||
"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'",
|
||||
"dontShowAPKOriginWarnings": "Không hiển thị cảnh báo nguồn gốc APK",
|
||||
"moveNonInstalledAppsToBottom": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách",
|
||||
"gitlabPATLabel": "GitLab Token\n(Cho phép tìm kiếm và lọc APK tốt hơn)",
|
||||
"gitlabPATLabel": "GitLab Token",
|
||||
"about": "Giới thiệu",
|
||||
"requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Cài đặt)",
|
||||
"requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Thiết đặt)",
|
||||
"checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
|
||||
"tryInferAppIdFromCode": "Thử suy ra ID ứng dụng từ mã nguồn",
|
||||
"removeOnExternalUninstall": "Tự động xóa ứng dụng đã gỡ cài đặt bên ngoài",
|
||||
@ -231,9 +230,8 @@
|
||||
"reversePageTransitions": "Hoạt ảnh chuyển đổi trang đảo ngược",
|
||||
"minStarCount": "Số lượng sao tối thiểu",
|
||||
"addInfoBelow": "Thêm thông tin này vào bên dưới.",
|
||||
"addInfoInSettings": "Thêm thông tin này vào Cài đặt.",
|
||||
"addInfoInSettings": "Thêm thông tin này vào Thiết đặt.",
|
||||
"githubSourceNote": "Có thể tránh được việc giới hạn tốc độ GitHub bằng cách sử dụng khóa API.",
|
||||
"gitlabSourceNote": "Trích xuất APK GitLab có thể không hoạt động nếu không có khóa API.",
|
||||
"sortByLastLinkSegment": "Chỉ sắp xếp theo đoạn cuối của liên kết",
|
||||
"filterReleaseNotesByRegEx": "Lọc ghi chú phát hành theo biểu thức chính quy",
|
||||
"customLinkFilterRegex": "Bộ lọc liên kết APK tùy chỉnh theo biểu thức chính quy (Mặc định '.apk$')",
|
||||
@ -256,7 +254,7 @@
|
||||
"highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
|
||||
"pickExportDir": "Chọn thư mục xuất",
|
||||
"autoExportOnChanges": "Tự động xuất",
|
||||
"includeSettings": "Bao gồm cài đặt ứng dụng",
|
||||
"includeSettings": "Bao gồm thiết đặt",
|
||||
"filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy",
|
||||
"trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
|
||||
"dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",
|
||||
@ -298,9 +296,9 @@
|
||||
"installed": "Đã cài đặt",
|
||||
"latest": "Mới nhất",
|
||||
"invertRegEx": "Đảo ngược biểu thức chính quy",
|
||||
"note": "Note",
|
||||
"selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.",
|
||||
"badDownload": "The APK could not be parsed (incompatible or partial download)",
|
||||
"note": "Ghi chú",
|
||||
"selfHostedNote": "Trình đơn thả xuống \"{}\" có thể được dùng để tiếp cận các phiên bản tự lưu trữ/tùy chỉnh của bất kỳ nguồn nào.",
|
||||
"badDownload": "Không thể phân tích cú pháp APK (tải xuống một phần hoặc không tương thích)",
|
||||
"removeAppQuestion": {
|
||||
"one": "Gỡ ứng dụng?",
|
||||
"other": "Gỡ ứng dụng?"
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} và 1 ứng dụng khác có thể đã được cập nhật.",
|
||||
"other": "{} và {} ứng dụng khác có thể đã được cập nhật."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@
|
||||
"pleaseWait": "请稍候",
|
||||
"updateAvailable": "更新可用",
|
||||
"notInstalled": "未安装",
|
||||
"pseudoVersion": "伪版本",
|
||||
"pseudoVersion": "虚拟版本号",
|
||||
"selectAll": "全选",
|
||||
"deselectX": "取消选择 {}",
|
||||
"xWillBeRemovedButRemainInstalled": "“{}”将从 Obtainium 中删除,但仍安装在您的设备中。",
|
||||
@ -80,7 +80,6 @@
|
||||
"removeOutdatedFilter": "删除失效的应用筛选",
|
||||
"showOutdatedOnly": "只显示待更新应用",
|
||||
"filter": "筛选",
|
||||
"filterActive": "筛选 *",
|
||||
"filterApps": "筛选应用",
|
||||
"appName": "应用名称",
|
||||
"author": "作者",
|
||||
@ -209,7 +208,7 @@
|
||||
"changes": "更新日志",
|
||||
"releaseDate": "发行日期",
|
||||
"importFromURLsInFile": "从文件中的 URL 导入(如 OPML)",
|
||||
"versionDetectionExplanation": "将版本字符串与操作系统检测到的版本进行协调",
|
||||
"versionDetectionExplanation": "使发行版本号与应用定义的版本号一致",
|
||||
"versionDetection": "版本检测",
|
||||
"standardVersionDetection": "常规版本检测",
|
||||
"groupByCategory": "按类别分组显示",
|
||||
@ -219,13 +218,13 @@
|
||||
"dontShowTrackOnlyWarnings": "忽略“仅追踪”模式警告",
|
||||
"dontShowAPKOriginWarnings": "忽略 APK 文件来源警告",
|
||||
"moveNonInstalledAppsToBottom": "将未安装应用置底",
|
||||
"gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)",
|
||||
"gitlabPATLabel": "GitLab 个人访问令牌",
|
||||
"about": "相关文档",
|
||||
"requiresCredentialsInSettings": "{}:此功能需要额外的凭据(在“设置”中添加)",
|
||||
"checkOnStart": "启动时进行一次检查",
|
||||
"tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
|
||||
"removeOnExternalUninstall": "自动删除列表中已卸载的应用",
|
||||
"pickHighestVersionCode": "自动选择版本号最高的 APK 文件",
|
||||
"pickHighestVersionCode": "自动选取内部版本号最高的 APK 文件",
|
||||
"checkUpdateOnDetailPage": "打开应用详情页时进行检查",
|
||||
"disablePageTransitions": "禁用页面过渡动画效果",
|
||||
"reversePageTransitions": "反转页面过渡动画效果",
|
||||
@ -233,7 +232,6 @@
|
||||
"addInfoBelow": "在下方添加此凭据。",
|
||||
"addInfoInSettings": "在“设置”中添加此凭据。",
|
||||
"githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。",
|
||||
"gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。",
|
||||
"sortByLastLinkSegment": "仅根据链接的末尾部分进行筛选",
|
||||
"filterReleaseNotesByRegEx": "筛选发行说明(正则表达式)",
|
||||
"customLinkFilterRegex": "筛选自定义来源的 APK 文件链接\n(正则表达式,默认匹配模式为“.apk$”)",
|
||||
@ -250,7 +248,7 @@
|
||||
"intermediateLink": "中转链接",
|
||||
"exemptFromBackgroundUpdates": "禁用后台更新(如果已经全局启用)",
|
||||
"bgUpdatesOnWiFiOnly": "未连接 Wi-Fi 时禁用后台更新",
|
||||
"autoSelectHighestVersionCode": "自动选择版本号最高的 APK 文件",
|
||||
"autoSelectHighestVersionCode": "自动选择内部版本号最高的 APK 文件",
|
||||
"versionExtractionRegEx": "版本号提取规则(正则表达式)",
|
||||
"matchGroupToUse": "引用的捕获组",
|
||||
"highlightTouchTargets": "突出展示不明显的触摸区域",
|
||||
@ -287,20 +285,20 @@
|
||||
"shizukuBinderNotFound": "未发现兼容的 Shizuku 服务",
|
||||
"useSystemFont": "使用系统字体",
|
||||
"systemFontError": "加载系统字体出错:{}",
|
||||
"useVersionCodeAsOSVersion": "使用应用程序版本代码作为操作系统检测到的版本",
|
||||
"useVersionCodeAsOSVersion": "使用内部版本号代替应用定义的版本号",
|
||||
"requestHeader": "请求标头",
|
||||
"useLatestAssetDateAsReleaseDate": "使用最新资产上传作为发布日期",
|
||||
"defaultPseudoVersioningMethod": "默认伪版本控制方法",
|
||||
"partialAPKHash": "部分 APK 哈希值",
|
||||
"APKLinkHash": "APK 链接哈希",
|
||||
"directAPKLink": "直接 APK 链接",
|
||||
"pseudoVersionInUse": "伪版本正在使用",
|
||||
"installed": "已安装",
|
||||
"latest": "最新的",
|
||||
"invertRegEx": "反转正则表达式",
|
||||
"useLatestAssetDateAsReleaseDate": "使用最近文件上传时间作为发行日期",
|
||||
"defaultPseudoVersioningMethod": "默认虚拟版本方案",
|
||||
"partialAPKHash": "APK 文件散列值片段",
|
||||
"APKLinkHash": "APK 文件链接散列值",
|
||||
"directAPKLink": "APK 文件直链",
|
||||
"pseudoVersionInUse": "正在使用虚拟版本号",
|
||||
"installed": "当前版本",
|
||||
"latest": "最新版本",
|
||||
"invertRegEx": "反转匹配",
|
||||
"note": "备注",
|
||||
"selfHostedNote": "{}\"下拉菜单可用于访问任何来源的自托管/自定义实例。",
|
||||
"badDownload": "无法解析 APK(不兼容或部分下载)",
|
||||
"selfHostedNote": "可以通过“{}”下拉菜单来指向任意来源的自托管/自定义实例。",
|
||||
"badDownload": "无法解析 APK 文件(不兼容或文件不完整)",
|
||||
"removeAppQuestion": {
|
||||
"one": "是否删除应用?",
|
||||
"other": "是否删除应用?"
|
||||
@ -352,5 +350,9 @@
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} 和另外 1 个应用已尝试更新。",
|
||||
"other": "“{}”和另外 {} 个应用已尝试更新。"
|
||||
},
|
||||
"apk": {
|
||||
"one": "{}APK",
|
||||
"other": "{}APK"
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class APKCombo extends AppSource {
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
var html = parse(res.data);
|
||||
return html
|
||||
.querySelectorAll('#variants-tab > div > ul > li')
|
||||
.map((e) {
|
||||
@ -96,7 +96,7 @@ class APKCombo extends AppSource {
|
||||
if (preres.statusCode != 200) {
|
||||
throw getObtainiumHttpError(preres);
|
||||
}
|
||||
var res = parse(preres.body);
|
||||
var res = parse(preres.data);
|
||||
String? version = res.querySelector('div.version')?.text.trim();
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@ -62,7 +62,7 @@ class APKMirror extends AppSource {
|
||||
: null;
|
||||
Response res = await sourceRequest('$standardUrl/feed', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var items = parse(res.body).querySelectorAll('item');
|
||||
var items = parse(res.data).querySelectorAll('item');
|
||||
dynamic targetRelease;
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
if (!fallbackToOlderReleases && i > 0) break;
|
||||
|
@ -61,8 +61,8 @@ class APKPure extends AppSource {
|
||||
var res = await sourceRequest('$standardUrl/download', additionalSettings);
|
||||
var resChangelog = await sourceRequest(standardUrl, additionalSettings);
|
||||
if (res.statusCode == 200 && resChangelog.statusCode == 200) {
|
||||
var html = parse(res.body);
|
||||
var htmlChangelog = parse(resChangelog.body);
|
||||
var html = parse(res.data);
|
||||
var htmlChangelog = parse(resChangelog.data);
|
||||
String? version = html.querySelector('span.info-sdk span')?.text.trim();
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
|
@ -38,10 +38,10 @@ class Aptoide extends AppSource {
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body);
|
||||
var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.data);
|
||||
String? id;
|
||||
if (idMatch != null) {
|
||||
id = res.body.substring(idMatch.start + 12, idMatch.end);
|
||||
id = res.data.substring(idMatch.start + 12, idMatch.end);
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
@ -50,7 +50,7 @@ class Aptoide extends AppSource {
|
||||
if (res2.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
return jsonDecode(res2.body)?['nodes']?['meta']?['data'];
|
||||
return jsonDecode(res2.data)?['nodes']?['meta']?['data'];
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/app_sources/gitlab.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
@ -82,7 +82,7 @@ class FDroid extends AppSource {
|
||||
var res = await sourceRequest(
|
||||
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
|
||||
additionalSettings);
|
||||
var lines = res.body.split('\n');
|
||||
var lines = res.data.split('\n');
|
||||
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
|
||||
if (authorLines.isNotEmpty) {
|
||||
details.names.author =
|
||||
@ -112,7 +112,7 @@ class FDroid extends AppSource {
|
||||
details.changeLog = (await sourceRequest(
|
||||
details.changeLog!.replaceFirst('/blob/', '/raw/'),
|
||||
additionalSettings))
|
||||
.body;
|
||||
.data;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@ -132,7 +132,7 @@ class FDroid extends AppSource {
|
||||
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
|
||||
if (res.statusCode == 200) {
|
||||
Map<String, List<String>> urlsWithDescriptions = {};
|
||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||
parse(res.data).querySelectorAll('.package-header').forEach((e) {
|
||||
String? url = e.attributes['href'];
|
||||
if (url != null) {
|
||||
try {
|
||||
@ -172,7 +172,7 @@ class FDroid extends AppSource {
|
||||
? additionalSettings['apkFilterRegEx']
|
||||
: null;
|
||||
if (res.statusCode == 200) {
|
||||
var response = jsonDecode(res.body);
|
||||
var response = jsonDecode(res.data);
|
||||
List<dynamic> releases = response['packages'] ?? [];
|
||||
if (apkFilterRegEx != null) {
|
||||
releases = releases.where((rel) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@ -61,9 +61,10 @@ class FDroidRepo extends AppSource {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
url = removeQueryParamsFromUrl(standardizeUrl(url));
|
||||
var res = await sourceRequestWithURLVariants(url, {});
|
||||
var ress = await sourceRequestWithURLVariants(url, {});
|
||||
var res = ress.value;
|
||||
if (res.statusCode == 200) {
|
||||
var body = parse(res.body);
|
||||
var body = parse(res.data);
|
||||
Map<String, List<String>> results = {};
|
||||
body.querySelectorAll('application').toList().forEach((app) {
|
||||
String appId = app.attributes['id']!;
|
||||
@ -74,7 +75,7 @@ class FDroidRepo extends AppSource {
|
||||
appName.contains(query) ||
|
||||
appDesc.contains(query)) {
|
||||
results[
|
||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
|
||||
'${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
|
||||
appName,
|
||||
appDesc
|
||||
];
|
||||
@ -107,24 +108,24 @@ class FDroidRepo extends AppSource {
|
||||
return app;
|
||||
}
|
||||
|
||||
Future<Response> sourceRequestWithURLVariants(
|
||||
Future<MapEntry<String, Response>> sourceRequestWithURLVariants(
|
||||
String url,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
var res = await sourceRequest(
|
||||
'$url${url.endsWith('/index.xml') ? '' : '/index.xml'}',
|
||||
additionalSettings);
|
||||
var finalUrl = '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}';
|
||||
var res = await sourceRequest(finalUrl, additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
var base = url.endsWith('/index.xml')
|
||||
? url.split('/').reversed.toList().sublist(1).reversed.join('/')
|
||||
: url;
|
||||
res = await sourceRequest('$base/repo/index.xml', additionalSettings);
|
||||
finalUrl = '$base/repo/index.xml';
|
||||
res = await sourceRequest(finalUrl, additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
res = await sourceRequest(
|
||||
'$base/fdroid/repo/index.xml', additionalSettings);
|
||||
finalUrl = '$base/fdroid/repo/index.xml';
|
||||
res = await sourceRequest(finalUrl, additionalSettings);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
return MapEntry(finalUrl, res);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -142,10 +143,11 @@ class FDroidRepo extends AppSource {
|
||||
if (appIdOrName == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
var res =
|
||||
var ress =
|
||||
await sourceRequestWithURLVariants(standardUrl, additionalSettings);
|
||||
var res = ress.value;
|
||||
if (res.statusCode == 200) {
|
||||
var body = parse(res.body);
|
||||
var body = parse(res.data);
|
||||
var foundApps = body.querySelectorAll('application').where((element) {
|
||||
return element.attributes['id'] == appIdOrName;
|
||||
}).toList();
|
||||
@ -193,7 +195,7 @@ class FDroidRepo extends AppSource {
|
||||
}
|
||||
List<String> apkUrls = latestVersionReleases
|
||||
.map((e) =>
|
||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
|
||||
'${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
|
||||
.toList();
|
||||
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
||||
AppNames(authorName, appName),
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
@ -112,12 +112,12 @@ class GitHub extends AppSource {
|
||||
];
|
||||
for (var path in possibleBuildGradleLocations) {
|
||||
try {
|
||||
var res = await sourceRequest(
|
||||
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
|
||||
additionalSettings);
|
||||
var finalUrl =
|
||||
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path';
|
||||
var res = await sourceRequest(finalUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
try {
|
||||
var body = jsonDecode(res.body);
|
||||
var body = jsonDecode(res.data);
|
||||
var trimmedLines = utf8
|
||||
.decode(base64
|
||||
.decode(body['content'].toString().split('\n').join('')))
|
||||
@ -143,7 +143,7 @@ class GitHub extends AppSource {
|
||||
}
|
||||
} catch (err) {
|
||||
LogsProvider().add(
|
||||
'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}');
|
||||
'Error parsing build.gradle from $finalUrl: ${err.toString()}');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@ -256,11 +256,11 @@ class GitHub extends AppSource {
|
||||
}
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
latestRelease = jsonDecode(res.body);
|
||||
latestRelease = jsonDecode(res.data);
|
||||
}
|
||||
Response res = await sourceRequest(requestUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||
var releases = jsonDecode(res.data) as List<dynamic>;
|
||||
if (latestRelease != null) {
|
||||
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
|
||||
if (releases
|
||||
@ -344,12 +344,14 @@ class GitHub extends AppSource {
|
||||
});
|
||||
}
|
||||
if (latestRelease != null &&
|
||||
(latestRelease['tag_name'] ?? latestRelease['name']) != null &&
|
||||
releases.isNotEmpty &&
|
||||
latestRelease !=
|
||||
(releases[releases.length - 1]['tag_name'] ??
|
||||
releases[0]['name'])) {
|
||||
var ind = releases.indexWhere((element) =>
|
||||
latestRelease == (element['tag_name'] ?? element['name']));
|
||||
(latestRelease['tag_name'] ?? latestRelease['name']) ==
|
||||
(element['tag_name'] ?? element['name']));
|
||||
if (ind >= 0) {
|
||||
releases.add(releases.removeAt(ind));
|
||||
}
|
||||
@ -400,7 +402,7 @@ class GitHub extends AppSource {
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
var changeLog = targetRelease['body'].toString();
|
||||
var changeLog = (targetRelease['body'] ?? '').toString();
|
||||
return APKDetails(
|
||||
version,
|
||||
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
|
||||
@ -464,7 +466,7 @@ class GitHub extends AppSource {
|
||||
? int.parse(querySettings['minStarCount'])
|
||||
: 0;
|
||||
Map<String, List<String>> urlsWithDescriptions = {};
|
||||
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
|
||||
for (var e in (jsonDecode(res.data)[rootProp] as List<dynamic>)) {
|
||||
if ((e['stargazers_count'] ?? e['stars_count'] ?? 0) >= minStarCount) {
|
||||
urlsWithDescriptions.addAll({
|
||||
e['html_url'] as String: [
|
||||
@ -498,11 +500,13 @@ class GitHub extends AppSource {
|
||||
}
|
||||
|
||||
rateLimitErrorCheck(Response res) {
|
||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||
String? rateLimitHeader;
|
||||
if (res.headers.map['x-ratelimit-remaining']?.isNotEmpty == true) {
|
||||
rateLimitHeader = res.headers.map['x-ratelimit-remaining']![0];
|
||||
}
|
||||
if (rateLimitHeader == '0') {
|
||||
throw RateLimitError(
|
||||
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
||||
60000000)
|
||||
.round());
|
||||
(int.parse(rateLimitHeader ?? '1800000000') / 60000000).round());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
@ -72,14 +72,6 @@ class GitLab extends AppSource {
|
||||
return creds != null && creds.isNotEmpty ? creds : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getSourceNote() async {
|
||||
if ((await getPATIfAny({})) == null) {
|
||||
return '${tr('gitlabSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
@ -89,7 +81,7 @@ class GitLab extends AppSource {
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var json = jsonDecode(res.body) as List<dynamic>;
|
||||
var json = jsonDecode(res.data) as List<dynamic>;
|
||||
Map<String, List<String>> results = {};
|
||||
for (var element in json) {
|
||||
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
|
||||
@ -104,105 +96,87 @@ class GitLab extends AppSource {
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||
'$standardUrl/-/releases';
|
||||
|
||||
@override
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
{bool forAPKDownload = false}) async {
|
||||
// Change headers to pacify, e.g. cloudflare protection
|
||||
// Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697)
|
||||
var headers = <String, String>{};
|
||||
headers[HttpHeaders.refererHeader] = 'https://${hosts[0]}';
|
||||
if (headers.isNotEmpty) {
|
||||
return headers;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
// Prepare request params
|
||||
var names = GitHub().getAppNames(standardUrl);
|
||||
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
||||
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
|
||||
|
||||
// Request data from REST API
|
||||
Response res = await sourceRequest(
|
||||
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/releases?$optionalAuth',
|
||||
additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
|
||||
// Extract .apk details from received data
|
||||
Iterable<APKDetails> apkDetailsList = [];
|
||||
var json = jsonDecode(res.data) as List<dynamic>;
|
||||
apkDetailsList = json.map((e) {
|
||||
var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? [])
|
||||
.map((e) {
|
||||
return (e['direct_asset_url'] ?? e['url'] ?? '') as String;
|
||||
})
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
List<String> uploadedAPKsFromDescription =
|
||||
((e['description'] ?? '') as String)
|
||||
.split('](')
|
||||
.join('\n')
|
||||
.split('.apk)')
|
||||
.join('.apk\n')
|
||||
.split('\n')
|
||||
.where((s) => s.startsWith('/uploads/') && s.endsWith('apk'))
|
||||
.map((s) => '$standardUrl$s')
|
||||
.toList();
|
||||
var apkUrlsSet = apkUrlsFromAssets.toSet();
|
||||
apkUrlsSet.addAll(uploadedAPKsFromDescription);
|
||||
var releaseDateString = e['released_at'] ?? e['created_at'];
|
||||
DateTime? releaseDate =
|
||||
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
|
||||
return APKDetails(
|
||||
e['tag_name'] ?? e['name'],
|
||||
getApkUrlsFromUrls(apkUrlsSet.toList()),
|
||||
GitHub().getAppNames(standardUrl),
|
||||
releaseDate: releaseDate);
|
||||
});
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
|
||||
// Fallback procedure
|
||||
bool fallbackToOlderReleases =
|
||||
additionalSettings['fallbackToOlderReleases'] == true;
|
||||
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
||||
Iterable<APKDetails> apkDetailsList = [];
|
||||
if (PAT != null) {
|
||||
var names = GitHub().getAppNames(standardUrl);
|
||||
Response res = await sourceRequest(
|
||||
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/releases?private_token=$PAT',
|
||||
additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var json = jsonDecode(res.body) as List<dynamic>;
|
||||
apkDetailsList = json.map((e) {
|
||||
var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? [])
|
||||
.map((e) {
|
||||
return (e['direct_asset_url'] ?? e['url'] ?? '') as String;
|
||||
})
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
List<String> uploadedAPKsFromDescription =
|
||||
((e['description'] ?? '') as String)
|
||||
.split('](')
|
||||
.join('\n')
|
||||
.split('.apk)')
|
||||
.join('.apk\n')
|
||||
.split('\n')
|
||||
.where((s) => s.startsWith('/uploads/') && s.endsWith('apk'))
|
||||
.map((s) => '$standardUrl$s')
|
||||
.toList();
|
||||
var apkUrlsSet = apkUrlsFromAssets.toSet();
|
||||
apkUrlsSet.addAll(uploadedAPKsFromDescription);
|
||||
var releaseDateString = e['released_at'] ?? e['created_at'];
|
||||
DateTime? releaseDate = releaseDateString != null
|
||||
? DateTime.parse(releaseDateString)
|
||||
: null;
|
||||
return APKDetails(
|
||||
e['tag_name'] ?? e['name'],
|
||||
getApkUrlsFromUrls(apkUrlsSet.toList()),
|
||||
GitHub().getAppNames(standardUrl),
|
||||
releaseDate: releaseDate);
|
||||
});
|
||||
} else {
|
||||
Response res = await sourceRequest(
|
||||
'$standardUrl/-/tags?format=atom', additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var standardUri = Uri.parse(standardUrl);
|
||||
var parsedHtml = parse(res.body);
|
||||
apkDetailsList = parsedHtml.querySelectorAll('entry').map((entry) {
|
||||
var entryContent = parse(
|
||||
parseFragment(entry.querySelector('content')!.innerHtml).text);
|
||||
var apkUrls = [
|
||||
...getLinksFromParsedHTML(
|
||||
entryContent,
|
||||
RegExp(
|
||||
'^${standardUri.path.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
||||
return '\\${x[0]}';
|
||||
})}/uploads/[^/]+/[^/]+\\.apk\$',
|
||||
caseSensitive: false),
|
||||
standardUri.origin),
|
||||
// GitLab releases may contain links to externally hosted APKs
|
||||
...getLinksFromParsedHTML(entryContent,
|
||||
RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
|
||||
.where((element) => Uri.parse(element).host != '')
|
||||
];
|
||||
var entryId = entry.querySelector('id')?.innerHtml;
|
||||
var version =
|
||||
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
||||
var releaseDateString = entry.querySelector('updated')?.innerHtml;
|
||||
DateTime? releaseDate = releaseDateString != null
|
||||
? DateTime.parse(releaseDateString)
|
||||
: null;
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
|
||||
GitHub().getAppNames(standardUrl),
|
||||
releaseDate: releaseDate);
|
||||
});
|
||||
}
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError(note: tr('gitlabSourceNote'));
|
||||
}
|
||||
if (fallbackToOlderReleases) {
|
||||
if (additionalSettings['trackOnly'] != true) {
|
||||
apkDetailsList =
|
||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
||||
}
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError(note: tr('gitlabSourceNote'));
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
|
||||
return apkDetailsList.first;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
@ -213,11 +213,11 @@ class HTML extends AppSource {
|
||||
// Given an HTTP response, grab some links according to the common additional settings
|
||||
// (those that apply to intermediate and final steps)
|
||||
Future<List<MapEntry<String, String>>> grabLinksCommon(
|
||||
Response res, Map<String, dynamic> additionalSettings) async {
|
||||
Response res, Uri url, Map<String, dynamic> additionalSettings) async {
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
var html = parse(res.data);
|
||||
List<MapEntry<String, String>> allLinks = html
|
||||
.querySelectorAll('a')
|
||||
.map((element) => MapEntry(
|
||||
@ -226,13 +226,12 @@ class HTML extends AppSource {
|
||||
? element.text
|
||||
: (element.attributes['href'] ?? '').split('/').last))
|
||||
.where((element) => element.key.isNotEmpty)
|
||||
.map((e) =>
|
||||
MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
|
||||
.map((e) => MapEntry(ensureAbsoluteUrl(e.key, url), e.value))
|
||||
.toList();
|
||||
if (allLinks.isEmpty) {
|
||||
allLinks = RegExp(
|
||||
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
|
||||
.allMatches(res.body)
|
||||
.allMatches(res.data)
|
||||
.map((match) =>
|
||||
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
|
||||
.toList();
|
||||
@ -285,6 +284,7 @@ class HTML extends AppSource {
|
||||
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
|
||||
var intLinks = await grabLinksCommon(
|
||||
await sourceRequest(currentUrl, additionalSettings),
|
||||
Uri.parse(currentUrl),
|
||||
additionalSettings['intermediateLink'][i]);
|
||||
if (intLinks.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
@ -298,8 +298,9 @@ class HTML extends AppSource {
|
||||
if (additionalSettings['directAPKLink'] != true) {
|
||||
Response res = await sourceRequest(currentUrl, additionalSettings);
|
||||
versionExtractionWholePageString =
|
||||
res.body.split('\r\n').join('\n').split('\n').join('\\n');
|
||||
links = await grabLinksCommon(res, additionalSettings);
|
||||
res.data.split('\r\n').join('\n').split('\n').join('\\n');
|
||||
links =
|
||||
await grabLinksCommon(res, Uri.parse(currentUrl), additionalSettings);
|
||||
links = filterApks(links, additionalSettings['apkFilterRegEx'],
|
||||
additionalSettings['invertAPKFilter']);
|
||||
if (links.isEmpty) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -57,8 +57,8 @@ class HuaweiAppGallery extends AppSource {
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
String dlUrl = getDlUrl(standardUrl);
|
||||
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
||||
return res.headers['location'] != null
|
||||
? appIdFromRedirectDlUrl(res.headers['location']!)
|
||||
return res.headers.map['location']?.isNotEmpty == true
|
||||
? appIdFromRedirectDlUrl(res.headers.map['location']![0])
|
||||
: null;
|
||||
}
|
||||
|
||||
@ -72,9 +72,12 @@ class HuaweiAppGallery extends AppSource {
|
||||
if (res.headers['location'] == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String appId = appIdFromRedirectDlUrl(res.headers['location']!);
|
||||
var relDateStr =
|
||||
res.headers['location']?.split('?')[0].split('.').reversed.toList()[1];
|
||||
String appId = appIdFromRedirectDlUrl(res.headers.map['location']![0]);
|
||||
var relDateStr = res.headers.map['location']?[0]
|
||||
.split('?')[0]
|
||||
.split('.')
|
||||
.reversed
|
||||
.toList()[1];
|
||||
var relDateStrAdj = relDateStr?.split('');
|
||||
var tempLen = relDateStrAdj?.length ?? 0;
|
||||
var i = 2;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -33,7 +33,7 @@ class Jenkins extends AppSource {
|
||||
Response res = await sourceRequest(
|
||||
'$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var json = jsonDecode(res.body);
|
||||
var json = jsonDecode(res.data);
|
||||
var releaseDate = json['timestamp'] == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@ -33,7 +33,7 @@ class Mullvad extends AppSource {
|
||||
Response res = await sourceRequest(
|
||||
'$standardUrl/en/download/android', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var versions = parse(res.body)
|
||||
var versions = parse(res.data)
|
||||
.querySelectorAll('p')
|
||||
.map((e) => e.innerHtml)
|
||||
.where((p) => p.contains('Latest version: '))
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -83,7 +83,7 @@ class NeutronCode extends AppSource {
|
||||
) async {
|
||||
Response res = await sourceRequest(standardUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var http = parse(res.body);
|
||||
var http = parse(res.data);
|
||||
var name = http.querySelector('.pd-title')?.innerHtml;
|
||||
var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml;
|
||||
if (filename == null) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -21,7 +21,7 @@ class Signal extends AppSource {
|
||||
Response res = await sourceRequest(
|
||||
'https://updates.${hosts[0]}/android/latest.json', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var json = jsonDecode(res.body);
|
||||
var json = jsonDecode(res.data);
|
||||
String? apkUrl = json['url'];
|
||||
List<String> apkUrls = apkUrl == null ? [] : [apkUrl];
|
||||
String? version = json['versionName'];
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -49,7 +49,7 @@ class SourceForge extends AppSource {
|
||||
'${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
|
||||
additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var parsedHtml = parse(res.body);
|
||||
var parsedHtml = parse(res.data);
|
||||
var allDownloadLinks = parsedHtml
|
||||
.querySelectorAll('guid')
|
||||
.map((e) => e.innerHtml)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@ -55,7 +55,7 @@ class SourceHut extends AppSource {
|
||||
Response res =
|
||||
await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var parsedHtml = parse(res.body);
|
||||
var parsedHtml = parse(res.data);
|
||||
List<APKDetails> apkDetailsList = [];
|
||||
int ind = 0;
|
||||
|
||||
@ -85,7 +85,7 @@ class SourceHut extends AppSource {
|
||||
var res2 = await sourceRequest(releasePage, additionalSettings);
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
if (res2.statusCode == 200) {
|
||||
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
||||
apkUrls = getApkUrlsFromUrls(parse(res2.data)
|
||||
.querySelectorAll('a')
|
||||
.map((e) => e.attributes['href'] ?? '')
|
||||
.where((e) => e.toLowerCase().endsWith('.apk'))
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@ -38,7 +38,7 @@ class SteamMobile extends AppSource {
|
||||
}
|
||||
String apkInURLRegexPattern =
|
||||
'/$apkNamePrefix-([0-9]+\\.)*[0-9]+\\.apk\$';
|
||||
var links = parse(res.body)
|
||||
var links = parse(res.data)
|
||||
.querySelectorAll('a')
|
||||
.map((e) => e.attributes['href'] ?? '')
|
||||
.where((e) => RegExp('https://.*$apkInURLRegexPattern').hasMatch(e))
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -23,7 +23,7 @@ class TelegramApp extends AppSource {
|
||||
Response res =
|
||||
await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var http = parse(res.body);
|
||||
var http = parse(res.data);
|
||||
var messages =
|
||||
http.querySelectorAll('.tgme_widget_message_text.js-message_text');
|
||||
var version = messages.isNotEmpty
|
||||
|
@ -37,7 +37,7 @@ class Uptodown extends AppSource {
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
var html = parse(res.data);
|
||||
String? version = html.querySelector('div.version')?.innerHtml;
|
||||
String? apkUrl =
|
||||
'${standardUrl.split('/').reversed.toList().sublist(1).reversed.join('/')}/post-download';
|
||||
@ -94,7 +94,7 @@ class Uptodown extends AppSource {
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
var html = parse(res.data);
|
||||
var finalUrlKey =
|
||||
html.querySelector('.post-download')?.attributes['data-url'];
|
||||
if (finalUrlKey == null) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class VLC extends AppSource {
|
||||
@ -29,7 +30,7 @@ class VLC extends AppSource {
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||
Response res = await sourceRequest(dwUrlBase, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var dwLinks = parse(res.body)
|
||||
var dwLinks = parse(res.data)
|
||||
.querySelectorAll('a')
|
||||
.where((element) => element.attributes['href'] != 'last/')
|
||||
.map((e) => e.attributes['href']?.split('/')[0])
|
||||
@ -49,11 +50,11 @@ class VLC extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res = await get(
|
||||
Uri.parse('https://www.videolan.org/vlc/download-android.html'));
|
||||
Response res =
|
||||
await dio.get('https://www.videolan.org/vlc/download-android.html');
|
||||
if (res.statusCode == 200) {
|
||||
var dwUrlBase = 'get.videolan.org/vlc-android';
|
||||
var dwLinks = parse(res.body)
|
||||
var dwLinks = parse(res.data)
|
||||
.querySelectorAll('a')
|
||||
.where((element) =>
|
||||
element.attributes['href']?.contains(dwUrlBase) ?? false)
|
||||
@ -84,14 +85,14 @@ class VLC extends AppSource {
|
||||
Response res = await sourceRequest(apkUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
String? apkUrl =
|
||||
parse(res.body).querySelector('#alt_link')?.attributes['href'];
|
||||
parse(res.data).querySelector('#alt_link')?.attributes['href'];
|
||||
if (apkUrl == null) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
return apkUrl;
|
||||
} else if (res.statusCode == 500 &&
|
||||
res.body.toLowerCase().indexOf('mirror') > 0) {
|
||||
var html = parse(res.body);
|
||||
res.data.toLowerCase().indexOf('mirror') > 0) {
|
||||
var html = parse(res.data);
|
||||
var err = '';
|
||||
html.body?.nodes.forEach((element) {
|
||||
if (element.text != null) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -20,7 +20,7 @@ class WhatsApp extends AppSource {
|
||||
Response res =
|
||||
await sourceRequest('$standardUrl/android', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var targetLinks = parse(res.body)
|
||||
var targetLinks = parse(res.data)
|
||||
.querySelectorAll('a')
|
||||
.map((e) => e.attributes['href'] ?? '')
|
||||
.where((e) => e.isNotEmpty)
|
||||
|
@ -18,6 +18,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:easy_localization/src/easy_localization_controller.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:easy_localization/src/localization.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
List<MapEntry<Locale, String>> supportedLocales = const [
|
||||
MapEntry(Locale('en'), 'English'),
|
||||
@ -38,6 +39,7 @@ List<MapEntry<Locale, String>> supportedLocales = const [
|
||||
MapEntry(Locale('nl'), 'Nederlands'),
|
||||
MapEntry(Locale('vi'), 'Tiếng Việt'),
|
||||
MapEntry(Locale('tr'), 'Türkçe'),
|
||||
MapEntry(Locale('uk'), 'Українська'),
|
||||
];
|
||||
const fallbackLocale = Locale('en');
|
||||
const localeDir = 'assets/translations';
|
||||
@ -45,6 +47,9 @@ var fdroid = false;
|
||||
|
||||
final globalNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final dio = Dio(BaseOptions(
|
||||
responseType: ResponseType.plain, receiveDataWhenStatusError: true));
|
||||
|
||||
Future<void> loadTranslations() async {
|
||||
// See easy_localization/issues/210
|
||||
await EasyLocalizationController.initEasyLocation();
|
||||
|
@ -1,9 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class GitHubStars implements MassAppUrlSource {
|
||||
@ -15,13 +16,12 @@ class GitHubStars implements MassAppUrlSource {
|
||||
|
||||
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||
String username, int page) async {
|
||||
Response res = await get(
|
||||
Uri.parse(
|
||||
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
|
||||
headers: await GitHub().getRequestHeaders({}));
|
||||
Response res = await dio.get(
|
||||
'https://api.github.com/users/$username/starred?per_page=100&page=$page',
|
||||
options: Options(headers: await GitHub().getRequestHeaders({})));
|
||||
if (res.statusCode == 200) {
|
||||
Map<String, List<String>> urlsWithDescriptions = {};
|
||||
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||
for (var e in (jsonDecode(res.data) as List<dynamic>)) {
|
||||
urlsWithDescriptions.addAll({
|
||||
e['html_url'] as String: [
|
||||
e['full_name'] as String,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
@ -62,18 +61,6 @@ class AddAppPageState extends State<AddAppPage> {
|
||||
var prevHost = pickedSource?.hosts.isNotEmpty == true
|
||||
? pickedSource?.hosts[0]
|
||||
: null;
|
||||
try {
|
||||
var naturalSource =
|
||||
valid ? sourceProvider.getSource(userInput) : null;
|
||||
if (naturalSource != null &&
|
||||
naturalSource.runtimeType.toString() !=
|
||||
HTML().runtimeType.toString()) {
|
||||
// If input has changed to match a regular source, reset the override
|
||||
pickedSourceOverride = null;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
var source = valid
|
||||
? sourceProvider.getSource(userInput,
|
||||
overrideSource: pickedSourceOverride)
|
||||
@ -361,8 +348,9 @@ class AddAppPageState extends State<AddAppPage> {
|
||||
[
|
||||
GeneratedFormDropdown(
|
||||
'overrideSource',
|
||||
defaultValue: HTML().runtimeType.toString(),
|
||||
defaultValue: '',
|
||||
[
|
||||
MapEntry('', tr('none')),
|
||||
...sourceProvider.sources.map(
|
||||
(s) => MapEntry(s.runtimeType.toString(), s.name))
|
||||
],
|
||||
@ -577,11 +565,7 @@ class AddAppPageState extends State<AddAppPage> {
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (pickedSourceOverride != null ||
|
||||
(pickedSource != null &&
|
||||
pickedSource.runtimeType.toString() ==
|
||||
HTML().runtimeType.toString()))
|
||||
getHTMLSourceOverrideDropdown(),
|
||||
if (pickedSource != null) getHTMLSourceOverrideDropdown(),
|
||||
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||
if (pickedSource != null)
|
||||
FutureBuilder(
|
||||
|
@ -104,6 +104,10 @@ class _AppPageState extends State<AppPage> {
|
||||
if (installedVersionIsEstimate) {
|
||||
infoLines = '${tr('pseudoVersionInUse')}\n$infoLines';
|
||||
}
|
||||
if ((app?.app.apkUrls.length ?? 0) > 0) {
|
||||
infoLines =
|
||||
'$infoLines\n${app?.app.apkUrls.length == 1 ? app?.app.apkUrls[0].key : plural('apk', app?.app.apkUrls.length ?? 0)}';
|
||||
}
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
@ -5,14 +5,15 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:android_intent_plus/flag.dart';
|
||||
import 'package:android_package_installer/android_package_installer.dart';
|
||||
import 'package:android_package_manager/android_package_manager.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -28,7 +29,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:flutter_archive/flutter_archive.dart';
|
||||
import 'package:shared_storage/shared_storage.dart' as saf;
|
||||
@ -146,10 +146,10 @@ Future<File> downloadFileWithRetry(
|
||||
Map<String, String>? headers,
|
||||
int retries = 3}) async {
|
||||
try {
|
||||
return await downloadFile(url, fileNameNoExt, onProgress, destDir,
|
||||
return await downloadApk(url, fileNameNoExt, onProgress, destDir,
|
||||
useExisting: useExisting, headers: headers);
|
||||
} catch (e) {
|
||||
if (retries > 0 && e is ClientException) {
|
||||
if (retries > 0 && e is DioException) {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
return await downloadFileWithRetry(
|
||||
url, fileNameNoExt, onProgress, destDir,
|
||||
@ -183,9 +183,162 @@ Future<String> checkPartialDownloadHashDynamic(String url,
|
||||
throw NoVersionError();
|
||||
}
|
||||
|
||||
Future<File> downloadApk(
|
||||
String url, String fileNameNoExt, Function? onProgress, String destDir,
|
||||
{bool useExisting = true, Map<String, String>? headers}) async {
|
||||
var resHeaders = await getHeaders(url, headers: headers);
|
||||
|
||||
String ext = resHeaders['content-disposition']?.split('.').last ?? 'apk';
|
||||
if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||
ext = ext.substring(0, ext.length - 1);
|
||||
}
|
||||
if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
|
||||
ext = 'apk';
|
||||
}
|
||||
File file = File('$destDir/$fileNameNoExt.$ext');
|
||||
|
||||
final contentLength = await getContentLengthIfRangeSupported(resHeaders);
|
||||
|
||||
if (useExisting && file.existsSync()) {
|
||||
var length = file.lengthSync();
|
||||
if (contentLength == null) {
|
||||
return file;
|
||||
} else {
|
||||
if (length == contentLength) {
|
||||
return file;
|
||||
}
|
||||
if (length > contentLength) {
|
||||
useExisting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double progress = -1;
|
||||
|
||||
try {
|
||||
if (contentLength == null) {
|
||||
Response response = await dio.download(
|
||||
url,
|
||||
file.path,
|
||||
options: Options(headers: headers),
|
||||
onReceiveProgress: (count, total) {
|
||||
progress = (total > 0 ? count / total * 100 : 30);
|
||||
if (onProgress != null) {
|
||||
onProgress(progress);
|
||||
}
|
||||
},
|
||||
);
|
||||
if ((response.statusCode ?? 200) < 200 ||
|
||||
(response.statusCode ?? 200) > 299) {
|
||||
throw response.statusMessage ?? tr('unexpectedError');
|
||||
}
|
||||
} else {
|
||||
var targetFileLength =
|
||||
useExisting && file.existsSync() ? file.lengthSync() : null;
|
||||
int bufferSize = 1024 * 1024; // 1 Megabyte
|
||||
final sink = file.openWrite(
|
||||
mode: useExisting ? FileMode.writeOnlyAppend : FileMode.writeOnly,
|
||||
);
|
||||
int rangeStart = targetFileLength ?? 0;
|
||||
int rangeEnd = min(
|
||||
rangeStart + bufferSize - 1,
|
||||
contentLength - 1,
|
||||
);
|
||||
if (onProgress != null) {
|
||||
progress = ((rangeStart / contentLength) * 100);
|
||||
onProgress(progress);
|
||||
}
|
||||
while (true) {
|
||||
var headersCurrent = headers ?? {};
|
||||
headersCurrent['range'] = 'bytes=$rangeStart-$rangeEnd';
|
||||
Response response = await dio.get(
|
||||
url,
|
||||
onReceiveProgress: (count, total) {
|
||||
if (onProgress != null) {
|
||||
final newProgress =
|
||||
(((rangeStart + count) / contentLength) * 100);
|
||||
if (newProgress != progress) {
|
||||
progress = newProgress;
|
||||
onProgress(progress);
|
||||
}
|
||||
}
|
||||
},
|
||||
options: Options(
|
||||
headers: headersCurrent,
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
|
||||
if ((response.statusCode ?? 200) < 200 ||
|
||||
(response.statusCode ?? 200) > 299) {
|
||||
throw response.statusMessage ?? tr('unexpectedError');
|
||||
}
|
||||
|
||||
final Uint8List data = response.data;
|
||||
sink.add(data);
|
||||
if (rangeEnd == contentLength - 1) {
|
||||
break;
|
||||
}
|
||||
rangeStart = rangeEnd + 1;
|
||||
rangeEnd = min(
|
||||
rangeStart + bufferSize - 1,
|
||||
contentLength - 1,
|
||||
);
|
||||
}
|
||||
await sink.flush();
|
||||
await sink.close();
|
||||
}
|
||||
} finally {
|
||||
if (onProgress != null) {
|
||||
onProgress(null);
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
Future<int?> getContentLengthIfRangeSupported(
|
||||
Map<String, String> headers) async {
|
||||
try {
|
||||
int? contentLength;
|
||||
{
|
||||
var contentLengthHeaderValue = headers['content-length'];
|
||||
if (contentLengthHeaderValue?.isNotEmpty == true) {
|
||||
contentLength = int.tryParse(contentLengthHeaderValue!);
|
||||
}
|
||||
}
|
||||
bool rangeFeatureEnabled = false;
|
||||
if (headers['accept-ranges']?.isNotEmpty == true) {
|
||||
rangeFeatureEnabled =
|
||||
headers['accept-ranges']!.trim().toLowerCase() == 'bytes';
|
||||
}
|
||||
if (!rangeFeatureEnabled) {
|
||||
contentLength = null;
|
||||
}
|
||||
return contentLength;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>> getHeaders(String url,
|
||||
{Map<String, String>? headers}) async {
|
||||
var req = http.Request('GET', Uri.parse(url));
|
||||
if (headers != null) {
|
||||
req.headers.addAll(headers);
|
||||
}
|
||||
var client = http.Client();
|
||||
var response = await client.send(req);
|
||||
if (response.statusCode < 200 || response.statusCode > 299) {
|
||||
throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError'));
|
||||
}
|
||||
var returnHeaders = response.headers;
|
||||
client.close();
|
||||
return returnHeaders;
|
||||
}
|
||||
|
||||
Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
|
||||
{Map<String, String>? headers}) async {
|
||||
var req = Request('GET', Uri.parse(url));
|
||||
var req = http.Request('GET', Uri.parse(url));
|
||||
if (headers != null) {
|
||||
req.headers.addAll(headers);
|
||||
}
|
||||
@ -199,58 +352,41 @@ Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
|
||||
return hashListOfLists(bytes);
|
||||
}
|
||||
|
||||
Future<File> downloadFile(
|
||||
String url, String fileNameNoExt, Function? onProgress, String destDir,
|
||||
{bool useExisting = true, Map<String, String>? headers}) async {
|
||||
var req = Request('GET', Uri.parse(url));
|
||||
if (headers != null) {
|
||||
req.headers.addAll(headers);
|
||||
}
|
||||
var client = http.Client();
|
||||
StreamedResponse response = await client.send(req);
|
||||
String ext =
|
||||
response.headers['content-disposition']?.split('.').last ?? 'apk';
|
||||
if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||
ext = ext.substring(0, ext.length - 1);
|
||||
}
|
||||
if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
|
||||
ext = 'apk';
|
||||
}
|
||||
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
|
||||
if (!(downloadedFile.existsSync() && useExisting)) {
|
||||
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
||||
if (tempDownloadedFile.existsSync()) {
|
||||
tempDownloadedFile.deleteSync(recursive: true);
|
||||
}
|
||||
var length = response.contentLength;
|
||||
var received = 0;
|
||||
double? progress;
|
||||
var sink = tempDownloadedFile.openWrite();
|
||||
await response.stream.map((s) {
|
||||
received += s.length;
|
||||
progress = (length != null ? received / length * 100 : 30);
|
||||
if (onProgress != null) {
|
||||
onProgress(progress);
|
||||
}
|
||||
return s;
|
||||
}).pipe(sink);
|
||||
await sink.close();
|
||||
progress = null;
|
||||
if (onProgress != null) {
|
||||
onProgress(progress);
|
||||
}
|
||||
if (response.statusCode != 200) {
|
||||
tempDownloadedFile.deleteSync(recursive: true);
|
||||
throw response.reasonPhrase ?? tr('unexpectedError');
|
||||
}
|
||||
if (tempDownloadedFile.existsSync()) {
|
||||
tempDownloadedFile.renameSync(downloadedFile.path);
|
||||
}
|
||||
} else {
|
||||
client.close();
|
||||
}
|
||||
return downloadedFile;
|
||||
}
|
||||
// Future<File> downloadFile(
|
||||
// String url, String fileNameNoExt, Function? onProgress, String destDir,
|
||||
// {bool useExisting = true, Map<String, String>? headers}) async {
|
||||
// var resHead = await dio.head(url);
|
||||
// String ext =
|
||||
// resHead.headers.map['content-disposition']?[0].split('.').last ?? 'apk';
|
||||
// if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||
// ext = ext.substring(0, ext.length - 1);
|
||||
// }
|
||||
// if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
|
||||
// ext = 'apk';
|
||||
// }
|
||||
// File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
|
||||
// if (!(downloadedFile.existsSync() && useExisting)) {
|
||||
// double? progress;
|
||||
// var response = await dio.download(
|
||||
// url,
|
||||
// downloadedFile.path,
|
||||
// options: Options(headers: headers),
|
||||
// onReceiveProgress: (count, total) {
|
||||
// progress = (total > 0 ? count / total * 100 : 30);
|
||||
// if (onProgress != null) {
|
||||
// onProgress(progress);
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// if (onProgress != null) {
|
||||
// onProgress(null);
|
||||
// }
|
||||
// if (response.statusCode != 200) {
|
||||
// throw response.statusMessage ?? tr('unexpectedError');
|
||||
// }
|
||||
// }
|
||||
// return downloadedFile;
|
||||
// }
|
||||
|
||||
Future<PackageInfo?> getInstalledInfo(String? packageName,
|
||||
{bool printErr = true}) async {
|
||||
@ -717,7 +853,7 @@ class AppsProvider with ChangeNotifier {
|
||||
appsToInstall =
|
||||
moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
|
||||
|
||||
Future<void> updateFn(String id, {bool skipInstalls = false}) async {
|
||||
Future<String> updateFn(String id, {bool skipInstalls = false}) async {
|
||||
try {
|
||||
var downloadedArtifact =
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -730,8 +866,8 @@ class AppsProvider with ChangeNotifier {
|
||||
} else {
|
||||
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||
}
|
||||
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
|
||||
id = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||
bool willBeSilent = await canInstallSilently(apps[id]!.app);
|
||||
switch (settingsProvider.installMethod) {
|
||||
case InstallMethodSettings.normal:
|
||||
if (!(await settingsProvider.getInstallPermission(
|
||||
@ -773,18 +909,19 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
if (willBeSilent && context == null) {
|
||||
notificationsProvider?.notify(SilentUpdateAttemptNotification(
|
||||
[apps[appId]!.app],
|
||||
id: appId.hashCode));
|
||||
[apps[id]!.app],
|
||||
id: id.hashCode));
|
||||
}
|
||||
installedIds.add(id);
|
||||
}
|
||||
} finally {
|
||||
apps[id]?.downloadProgress = null;
|
||||
notifyListeners();
|
||||
}
|
||||
installedIds.add(id);
|
||||
} catch (e) {
|
||||
errors.add(id, e, appName: apps[id]?.name);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
if (forceParallelDownloads || !settingsProvider.parallelDownloads) {
|
||||
@ -792,9 +929,9 @@ class AppsProvider with ChangeNotifier {
|
||||
await updateFn(id);
|
||||
}
|
||||
} else {
|
||||
await Future.wait(
|
||||
List<String> ids = await Future.wait(
|
||||
appsToInstall.map((id) => updateFn(id, skipInstalls: true)));
|
||||
for (var id in appsToInstall) {
|
||||
for (var id in ids) {
|
||||
if (!errors.appIdNames.containsKey(id)) {
|
||||
await updateFn(id);
|
||||
}
|
||||
@ -1620,7 +1757,7 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
|
||||
// Next task interval is based on the error with the longest retry time
|
||||
int minRetryIntervalForThisApp = err is RateLimitError
|
||||
? (err.remainingMinutes * 60)
|
||||
: e is ClientException
|
||||
: e is DioException
|
||||
? (15 * 60)
|
||||
: (toCheckApp.value + 1);
|
||||
if (minRetryIntervalForThisApp > maxRetryWaitSeconds) {
|
||||
|
@ -30,8 +30,22 @@ enum SortOrderSettings { ascending, descending }
|
||||
|
||||
const maxAPIRateLimitMinutes = 30;
|
||||
const minUpdateIntervalMinutes = maxAPIRateLimitMinutes + 30;
|
||||
const maxUpdateIntervalMinutes = 4320;
|
||||
List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
|
||||
const maxUpdateIntervalMinutes = 43200;
|
||||
List<int> updateIntervals = [
|
||||
15,
|
||||
30,
|
||||
60,
|
||||
120,
|
||||
180,
|
||||
360,
|
||||
720,
|
||||
1440,
|
||||
4320,
|
||||
10080,
|
||||
20160,
|
||||
43200,
|
||||
0
|
||||
]
|
||||
.where((element) =>
|
||||
(element >= minUpdateIntervalMinutes &&
|
||||
element <= maxUpdateIntervalMinutes) ||
|
||||
|
@ -4,9 +4,9 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||
import 'package:obtainium/app_sources/apkpure.dart';
|
||||
import 'package:obtainium/app_sources/aptoide.dart';
|
||||
@ -31,6 +31,7 @@ import 'package:obtainium/app_sources/vlc.dart';
|
||||
import 'package:obtainium/app_sources/whatsapp.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
|
||||
@ -442,16 +443,9 @@ abstract class AppSource {
|
||||
String url, Map<String, dynamic> additionalSettings,
|
||||
{bool followRedirects = true}) async {
|
||||
var requestHeaders = await getRequestHeaders(additionalSettings);
|
||||
if (requestHeaders != null || followRedirects == false) {
|
||||
var req = Request('GET', Uri.parse(url));
|
||||
req.followRedirects = followRedirects;
|
||||
if (requestHeaders != null) {
|
||||
req.headers.addAll(requestHeaders);
|
||||
}
|
||||
return Response.fromStream(await Client().send(req));
|
||||
} else {
|
||||
return get(Uri.parse(url));
|
||||
}
|
||||
return await dio.get(url,
|
||||
options:
|
||||
Options(headers: requestHeaders, followRedirects: followRedirects));
|
||||
}
|
||||
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
@ -618,10 +612,10 @@ abstract class AppSource {
|
||||
}
|
||||
|
||||
ObtainiumError getObtainiumHttpError(Response res) {
|
||||
return ObtainiumError((res.reasonPhrase != null &&
|
||||
res.reasonPhrase != null &&
|
||||
res.reasonPhrase!.isNotEmpty)
|
||||
? res.reasonPhrase!
|
||||
return ObtainiumError((res.statusMessage != null &&
|
||||
res.statusMessage != null &&
|
||||
res.statusMessage!.isNotEmpty)
|
||||
? res.statusMessage!
|
||||
: tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
|
||||
}
|
||||
|
||||
|
74
pubspec.lock
74
pubspec.lock
@ -38,10 +38,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
|
||||
sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
version: "3.5.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -70,10 +70,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: background_fetch
|
||||
sha256: "34550cf9b383e5a1844e7d22119aa500508c7df9421fa967c9fb4430d6cb2878"
|
||||
sha256: eb3af263d390d7e68ecb90f2ae984d2bfd96dceb4c7b4f72418dd5383b49de0a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.2.4"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -150,10 +150,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "2f9d2cbccb76127ba28528cb3ae2c2326a122446a83de5a056aaa3880d3882c5"
|
||||
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+7"
|
||||
version: "0.3.4+1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -202,22 +202,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.1"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
|
||||
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.9"
|
||||
version: "1.7.0"
|
||||
easy_localization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_localization
|
||||
sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af"
|
||||
sha256: c145aeb6584aedc7c862ab8c737c3277788f47488bfdf9bae0fe112bd0a4789c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
version: "3.0.5"
|
||||
easy_logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -254,10 +262,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6"
|
||||
sha256: caa6bc229eab3e32eb2f37b53a5f9d22a6981474afd210c512a7546c1e1a04f6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.1"
|
||||
version: "6.2.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -307,10 +315,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3
|
||||
sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.3.2"
|
||||
version: "16.3.3"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -336,10 +344,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "21b085a1c185e46701373866144ced56cfb7a0c33f63c916bb8fe2d0c1491278"
|
||||
sha256: cb44f7831b23a6bdd0f501718b0d2e8045cbc625a15f668af37ddb80314821db
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.19"
|
||||
version: "0.6.21"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -474,10 +482,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: markdown
|
||||
sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90"
|
||||
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.1"
|
||||
version: "7.2.2"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -666,10 +674,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.1"
|
||||
version: "6.1.2"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -730,10 +738,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
|
||||
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.3.0"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -855,10 +863,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
||||
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
version: "6.2.5"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -871,10 +879,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
|
||||
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
version: "6.2.5"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -903,10 +911,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
|
||||
sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.3.0"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -943,10 +951,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.5.1"
|
||||
webview_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -983,10 +991,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
||||
sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
version: "5.3.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1021,4 +1029,4 @@ packages:
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.16.6"
|
||||
flutter: ">=3.19.0"
|
||||
|
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.3+2253 # When changing this, update the tag in main() accordingly
|
||||
version: 1.0.5+2255 # When changing this, update the tag in main() accordingly
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@ -68,6 +68,7 @@ dependencies:
|
||||
crypto: ^3.0.3
|
||||
app_links: ^3.5.0
|
||||
background_fetch: ^1.2.1
|
||||
dio: ^5.4.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user