Compare commits
123 Commits
v0.9.2-bet
...
v0.10.9-be
Author | SHA1 | Date | |
---|---|---|---|
8002a946b2 | |||
fd9aebc5b2 | |||
1be38d361f | |||
32c40ae7b3 | |||
07223d81c7 | |||
78baee7265 | |||
348c33dfe9 | |||
c408d70ae6 | |||
3ae4e7cc8a | |||
dab0f2bb72 | |||
4baf6bcd3b | |||
db4517aa13 | |||
55d4d1f978 | |||
f89ac5965f | |||
d5ebaa161f | |||
a4c014a8bf | |||
bbaa42fb01 | |||
4fe311bc03 | |||
ea68b97ff7 | |||
6e0f6b528e | |||
a2c227931e | |||
15ad3bb439 | |||
b03d7fba1a | |||
31c491d7c5 | |||
71c80f11f5 | |||
eef4d33431 | |||
d56342e907 | |||
c72c0fdb57 | |||
ffe29009ed | |||
60e3b68ebd | |||
ee4d0f259f | |||
0ecfbef0a0 | |||
1b60e75ca7 | |||
abcfa389e8 | |||
a64bd67ef1 | |||
4252c2711b | |||
52913b0450 | |||
427b0ed8d2 | |||
a85d6d4f08 | |||
05f712603c | |||
fa2a80e34c | |||
f43e5a2ff1 | |||
b72aa8273e | |||
520f186e4a | |||
e1e97672cf | |||
1494bcd013 | |||
3457a0a12f | |||
b165400a6e | |||
c47bf937f1 | |||
2e19a8c04c | |||
05d4da86ec | |||
e9d1b04d54 | |||
cff5334c25 | |||
a55346fc22 | |||
885df678e5 | |||
bf7b0c5702 | |||
2972da4609 | |||
b8567af98e | |||
ea62c68b40 | |||
08a5af0449 | |||
36f327c16e | |||
768213cb34 | |||
e888fb7120 | |||
1fb68dd674 | |||
5c4bb8f84c | |||
1c8e759494 | |||
081c2a07d2 | |||
02751fe8fa | |||
95f3362a84 | |||
b68cf5a1be | |||
4eb7499591 | |||
98fafe2aa4 | |||
9bac74aadd | |||
0a93117bf0 | |||
451cc41c45 | |||
3b449d0982 | |||
1863f55372 | |||
0c4b8ac79d | |||
e287087753 | |||
82bcc46d42 | |||
1f26188ec6 | |||
794c3e1a81 | |||
16369b4adf | |||
8f16f745be | |||
8ddeb3d776 | |||
21cf9c98d9 | |||
358f910d19 | |||
7a3d74bd05 | |||
6f27f64699 | |||
3341fecb68 | |||
d3bce63ca4 | |||
8aa8b6b698 | |||
3d6c9bbf98 | |||
7af0a8628c | |||
4573ce6bcf | |||
e29d38fa32 | |||
dc82431235 | |||
424b0028bf | |||
46fba9e0a4 | |||
b40be7569b | |||
a173be11eb | |||
0c97b25d99 | |||
f836fd20d8 | |||
2f6917592d | |||
b864fef3ad | |||
8e487592b3 | |||
e9a44746a5 | |||
9123737bf3 | |||
12f70951c2 | |||
c1d56f89f0 | |||
4dfd29f5de | |||
226cfa25e0 | |||
4e0c655538 | |||
45a23e9025 | |||
1e5aa0999a | |||
beeec356e5 | |||
01fa9a2e96 | |||
0da7a36f1a | |||
ed2a4e674f | |||
0f6a683faa | |||
fa4d46b622 | |||
a3f9947f28 | |||
6977858b99 |
19
README.md
@ -1,4 +1,4 @@
|
|||||||
#  Obtainium
|
#  Obtainium
|
||||||
|
|
||||||
Get Android App Updates Directly From the Source.
|
Get Android App Updates Directly From the Source.
|
||||||
|
|
||||||
@ -9,14 +9,27 @@ Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to
|
|||||||
Currently supported App sources:
|
Currently supported App sources:
|
||||||
- [GitHub](https://github.com/)
|
- [GitHub](https://github.com/)
|
||||||
- [GitLab](https://gitlab.com/)
|
- [GitLab](https://gitlab.com/)
|
||||||
|
- [Codeberg](https://codeberg.org/)
|
||||||
- [F-Droid](https://f-droid.org/)
|
- [F-Droid](https://f-droid.org/)
|
||||||
- [IzzyOnDroid](https://android.izzysoft.de/)
|
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||||
- [Mullvad](https://mullvad.net/en/)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
- [Signal](https://signal.org/)
|
- [Signal](https://signal.org/)
|
||||||
- [SourceForge](https://sourceforge.net/)
|
- [SourceForge](https://sourceforge.net/)
|
||||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||||
- Third Party F-Droid Repos (URLs ending with `/fdroid/repo`)
|
- Third Party F-Droid Repos
|
||||||
|
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- [Steam](https://store.steampowered.com/mobile)
|
||||||
|
- "HTML" (Fallback)
|
||||||
|
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
[<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/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
|
||||||
|
alt="Get it on IzzyOnDroid"
|
||||||
|
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
||||||
@ -27,4 +40,4 @@ Currently supported App sources:
|
|||||||
|
|
||||||
| <img src="./assets/screenshots/1.apps.png" alt="Apps Page" /> | <img src="./assets/screenshots/2.dark_theme.png" alt="Dark Theme" /> | <img src="./assets/screenshots/3.material_you.png" alt="Material You" /> |
|
| <img src="./assets/screenshots/1.apps.png" alt="Apps Page" /> | <img src="./assets/screenshots/2.dark_theme.png" alt="Dark Theme" /> | <img src="./assets/screenshots/3.material_you.png" alt="Material You" /> |
|
||||||
| ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||||
| <img src="./assets/screenshots/4.app.png" alt="App Page" /> | <img src="./assets/screenshots/5.apk_picker.png" alt="Multiple APK Support" /> | <img src="./assets/screenshots/6.apk_install.png" alt="App Installation" /> |
|
| <img src="./assets/screenshots/4.app.png" alt="App Page" /> | <img src="./assets/screenshots/5.app_opts.png" alt="App Options" /> | <img src="./assets/screenshots/6.app_webview.png" alt="App Web View" /> |
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
<application
|
<application
|
||||||
android:label="Obtainium"
|
android:label="Obtainium"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@ -51,4 +52,8 @@
|
|||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29"/>
|
||||||
</manifest>
|
</manifest>
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.1 KiB |
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
BIN
assets/graphics/icon_small.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 188 KiB |
BIN
assets/screenshots/5.app_opts.png
Normal file
After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 192 KiB |
BIN
assets/screenshots/6.app_webview.png
Normal file
After Width: | Height: | Size: 262 KiB |
@ -74,7 +74,6 @@
|
|||||||
"changeX": "Ändern {}",
|
"changeX": "Ändern {}",
|
||||||
"installUpdateApps": "Apps installieren/aktualisieren",
|
"installUpdateApps": "Apps installieren/aktualisieren",
|
||||||
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
||||||
"onlyWorksWithNonEVDApps": "Funktioniert nur bei Apps, deren Installationsstatus nicht automatisch erkannt werden kann (ungewöhnlich).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
@ -178,7 +177,6 @@
|
|||||||
"installedVersionX": "Installierte Version: {}",
|
"installedVersionX": "Installierte Version: {}",
|
||||||
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
|
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"removeAppQuestion": "App entfernen?",
|
|
||||||
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "App ID oder Name",
|
"appIdOrName": "App ID oder Name",
|
||||||
@ -188,25 +186,37 @@
|
|||||||
"steam": "Steam",
|
"steam": "Steam",
|
||||||
"steamMobile": "Steam Mobile",
|
"steamMobile": "Steam Mobile",
|
||||||
"steamChat": "Steam Chat",
|
"steamChat": "Steam Chat",
|
||||||
"install": "Install",
|
"install": "Installieren",
|
||||||
"markInstalled": "Mark Installed",
|
"markInstalled": "Als Installiert markieren",
|
||||||
"update": "Update",
|
"update": "Aktualisieren",
|
||||||
"markUpdated": "Mark Updated",
|
"markUpdated": "Als Aktuell markieren",
|
||||||
"additionalOptions": "Additional Options",
|
"additionalOptions": "Zusätzliche Optionen",
|
||||||
"disableVersionDetection": "Disable Version Detection",
|
"disableVersionDetection": "Versionsermittlung deaktivieren",
|
||||||
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
|
"noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.",
|
||||||
"downloadingX": "Downloading {}",
|
"downloadingX": "Lade {} herunter",
|
||||||
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
|
"downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App",
|
||||||
"noAPKFound": "No APK found",
|
"noAPKFound": "Keine APK gefunden",
|
||||||
"noVersionDetection": "No version detection",
|
"noVersionDetection": "Keine Versionserkennung",
|
||||||
"categorize": "Categorize",
|
"categorize": "Kategorisieren",
|
||||||
"categories": "Categories",
|
"categories": "Kategorien",
|
||||||
"category": "Category",
|
"category": "Kategorie",
|
||||||
"noCategory": "No Category",
|
"noCategory": "Keine Kategorie",
|
||||||
"deleteCategoryQuestion": "Delete Category?",
|
"noCategories": "Keine Kategorien",
|
||||||
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
|
"deleteCategoriesQuestion": "Kategorien löschen?",
|
||||||
"addCategory": "Add Category",
|
"categoryDeleteWarning": "Alle Apps in gelöschten Kategorien werden auf nicht kategorisiert gesetzt.",
|
||||||
"label": "Label",
|
"addCategory": "Kategorie hinzufügen",
|
||||||
|
"label": "Bezeichnung",
|
||||||
|
"language": "Sprache",
|
||||||
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
|
"removeFromObtainium": "Remove from Obtainium",
|
||||||
|
"uninstallFromDevice": "Uninstall from Device",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "App entfernen?",
|
||||||
|
"other": "App entfernen?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
||||||
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
|
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
|
||||||
|
@ -74,7 +74,6 @@
|
|||||||
"changeX": "Change {}",
|
"changeX": "Change {}",
|
||||||
"installUpdateApps": "Install/Update Apps",
|
"installUpdateApps": "Install/Update Apps",
|
||||||
"installUpdateSelectedApps": "Install/Update Selected Apps",
|
"installUpdateSelectedApps": "Install/Update Selected Apps",
|
||||||
"onlyWorksWithNonEVDApps": "Only works for Apps whose install status cannot be automatically detected (uncommon).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Mark {} Selected Apps as Updated?",
|
"markXSelectedAppsAsUpdated": "Mark {} Selected Apps as Updated?",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
@ -178,7 +177,6 @@
|
|||||||
"installedVersionX": "Installed Version: {}",
|
"installedVersionX": "Installed Version: {}",
|
||||||
"lastUpdateCheckX": "Last Update Check: {}",
|
"lastUpdateCheckX": "Last Update Check: {}",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"removeAppQuestion": "Remove App?",
|
|
||||||
"yesMarkUpdated": "Yes, Mark as Updated",
|
"yesMarkUpdated": "Yes, Mark as Updated",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "App ID or Name",
|
"appIdOrName": "App ID or Name",
|
||||||
@ -203,10 +201,22 @@
|
|||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
"noCategory": "No Category",
|
"noCategory": "No Category",
|
||||||
"deleteCategoryQuestion": "Delete Category?",
|
"noCategories": "No Categories",
|
||||||
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
|
"deleteCategoriesQuestion": "Delete Categories?",
|
||||||
|
"categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.",
|
||||||
"addCategory": "Add Category",
|
"addCategory": "Add Category",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
|
"language": "Language",
|
||||||
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
|
"removeFromObtainium": "Remove from Obtainium",
|
||||||
|
"uninstallFromDevice": "Uninstall from Device",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Remove App?",
|
||||||
|
"other": "Remove Apps?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Too many requests (rate limited) - try again in {} minute",
|
"one": "Too many requests (rate limited) - try again in {} minute",
|
||||||
"other": "Too many requests (rate limited) - try again in {} minutes"
|
"other": "Too many requests (rate limited) - try again in {} minutes"
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
"and": "és",
|
"and": "és",
|
||||||
"startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva",
|
"startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva",
|
||||||
"bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}",
|
"bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}",
|
||||||
"startedActualBGUpdateCheck": "Elkezdődött a tényleges BG frissítés ellenőrzése",
|
"startedActualBGUpdateCheck": "Elkezdődött a tényleges háttérfrissítés ellenőrzése",
|
||||||
"bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött",
|
"bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött",
|
||||||
"firstRun": "Ez az Obtainium első futása",
|
"firstRun": "Ez az Obtainium első futása",
|
||||||
"settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása a erre: {}",
|
"settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása erre: {}",
|
||||||
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
||||||
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
||||||
"githubPATFormat": "felhasználónév:token",
|
"githubPATFormat": "felhasználónév:token",
|
||||||
@ -28,13 +28,13 @@
|
|||||||
"noDescription": "Nincs leírás",
|
"noDescription": "Nincs leírás",
|
||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"continue": "Tovább",
|
"continue": "Tovább",
|
||||||
"requiredInBrackets": "(Kötlező)",
|
"requiredInBrackets": "(Kötelező)",
|
||||||
"dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓBAN KELL RENDELNI",
|
"dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓHOZ KELL RENDELNI",
|
||||||
"colour": "Szín",
|
"colour": "Szín",
|
||||||
"githubStarredRepos": "GitHub Csillagozott Repo-k",
|
"githubStarredRepos": "GitHub Csillagos Repo-k",
|
||||||
"uname": "Felh.név",
|
"uname": "Felh.név",
|
||||||
"wrongArgNum": "Rossz számú argumentumot adott meg",
|
"wrongArgNum": "Rossz számú argumentumot adott meg",
|
||||||
"xIsTrackOnly": "{} csak nyomon követhető",
|
"xIsTrackOnly": "A(z) {} csak nyomkövethető",
|
||||||
"source": "Forrás",
|
"source": "Forrás",
|
||||||
"app": "App",
|
"app": "App",
|
||||||
"appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.",
|
"appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.",
|
||||||
@ -56,25 +56,24 @@
|
|||||||
"appsString": "Appok",
|
"appsString": "Appok",
|
||||||
"noApps": "Nincs App",
|
"noApps": "Nincs App",
|
||||||
"noAppsForFilter": "Nincsenek appok a szűrőhöz",
|
"noAppsForFilter": "Nincsenek appok a szűrőhöz",
|
||||||
"byX": "By {}",
|
"byX": "{} által",
|
||||||
"percentProgress": "Folyamat: {}%",
|
"percentProgress": "Folyamat: {}%",
|
||||||
"pleaseWait": "Kis türelmet",
|
"pleaseWait": "Kis türelmet",
|
||||||
"updateAvailable": "Frissítés elérhető",
|
"updateAvailable": "Frissítés érhető el",
|
||||||
"estimateInBracketsShort": "(Becsült)",
|
"estimateInBracketsShort": "(Becsült)",
|
||||||
"notInstalled": "Nem telepített",
|
"notInstalled": "Nem telepített",
|
||||||
"estimateInBrackets": "(Becslés)",
|
"estimateInBrackets": "(Becslés)",
|
||||||
"selectAll": "Mindet kiválaszt",
|
"selectAll": "Mindet kiválaszt",
|
||||||
"deselectN": "Törölje {} kijelölését",
|
"deselectN": "Törölje {} kijelölését",
|
||||||
"xWillBeRemovedButRemainInstalled": "{} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.",
|
"xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.",
|
||||||
"removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
|
"removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
|
||||||
"removeSelectedApps": "Távolítsa el a kiválasztott appokat",
|
"removeSelectedApps": "Távolítsa el a kiválasztott appokat",
|
||||||
"updateX": "Frissítés: {}",
|
"updateX": "Frissítés: {}",
|
||||||
"installX": "Telepítés {}",
|
"installX": "Telepítés: {}",
|
||||||
"markXTrackOnlyAsUpdated": "Jelölje meg: {}\n(Csak nyomon követhető)\nas Frissítve",
|
"markXTrackOnlyAsUpdated": "Jelölje meg: {}\n(Csak nyomon követhető)\nmint Frissített",
|
||||||
"changeX": "Változás {}",
|
"changeX": "Változás {}",
|
||||||
"installUpdateApps": "Appok telepítése/frissítése",
|
"installUpdateApps": "Appok telepítése/frissítése",
|
||||||
"installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat",
|
"installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat",
|
||||||
"onlyWorksWithNonEVDApps": "Csak azoknál az alkalmazásoknál működik, amelyek telepítési állapota nem észlelhető automatikusan (nem gyakori).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?",
|
"markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?",
|
||||||
"no": "Nem",
|
"no": "Nem",
|
||||||
"yes": "Igen",
|
"yes": "Igen",
|
||||||
@ -86,8 +85,8 @@
|
|||||||
"shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit",
|
"shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit",
|
||||||
"resetInstallStatus": "Telepítési állapot visszaállítása",
|
"resetInstallStatus": "Telepítési állapot visszaállítása",
|
||||||
"more": "További",
|
"more": "További",
|
||||||
"removeOutdatedFilter": "Távolítsa el az elavult alkalmazásszűrőt",
|
"removeOutdatedFilter": "Távolítsa el az elavult app szűrőt",
|
||||||
"showOutdatedOnly": "Csak az elavult alkalmazások megjelenítése",
|
"showOutdatedOnly": "Csak az elavult appok megjelenítése",
|
||||||
"filter": "Szűrő",
|
"filter": "Szűrő",
|
||||||
"filterActive": "Szűrő *",
|
"filterActive": "Szűrő *",
|
||||||
"filterApps": "Appok szűrése",
|
"filterApps": "Appok szűrése",
|
||||||
@ -118,7 +117,7 @@
|
|||||||
"selectURLs": "Kiválasztott URL-ek",
|
"selectURLs": "Kiválasztott URL-ek",
|
||||||
"pick": "Válasszon",
|
"pick": "Válasszon",
|
||||||
"theme": "Téma",
|
"theme": "Téma",
|
||||||
"dark": "Söét",
|
"dark": "Sötét",
|
||||||
"light": "Világos",
|
"light": "Világos",
|
||||||
"followSystem": "Rendszer szerint",
|
"followSystem": "Rendszer szerint",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
@ -126,11 +125,11 @@
|
|||||||
"appSortBy": "App rendezés...",
|
"appSortBy": "App rendezés...",
|
||||||
"authorName": "Szerző/Név",
|
"authorName": "Szerző/Név",
|
||||||
"nameAuthor": "Név/Szerző",
|
"nameAuthor": "Név/Szerző",
|
||||||
"asAdded": "Mint hozzáadott",
|
"asAdded": "Mint Hozzáadott",
|
||||||
"appSortOrder": "Appok rendezése",
|
"appSortOrder": "Appok rendezése",
|
||||||
"ascending": "Emelkedő",
|
"ascending": "Emelkedő",
|
||||||
"descending": "Csökkenő",
|
"descending": "Csökkenő",
|
||||||
"bgUpdateCheckInterval": "Háttérfrissítés ellenőrzési időköz",
|
"bgUpdateCheckInterval": "Háttérfrissítés ellenőrzés időköze",
|
||||||
"neverManualOnly": "Soha – csak manuális",
|
"neverManualOnly": "Soha – csak manuális",
|
||||||
"appearance": "Megjelenés",
|
"appearance": "Megjelenés",
|
||||||
"showWebInAppView": "Forrás megjelenítése az Appok nézetben",
|
"showWebInAppView": "Forrás megjelenítése az Appok nézetben",
|
||||||
@ -145,7 +144,7 @@
|
|||||||
"appNotFound": "App nem található",
|
"appNotFound": "App nem található",
|
||||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
"pickAnAPK": "Válasszon egy APK-t",
|
"pickAnAPK": "Válasszon egy APK-t",
|
||||||
"appHasMoreThanOnePackage": "{} egynél több csomaggal rendelkezik:",
|
"appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:",
|
||||||
"deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.",
|
"deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.",
|
||||||
"deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:",
|
"deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:",
|
||||||
"warning": "Figyelem",
|
"warning": "Figyelem",
|
||||||
@ -153,16 +152,16 @@
|
|||||||
"updatesAvailable": "Frissítések érhetők el",
|
"updatesAvailable": "Frissítések érhetők el",
|
||||||
"updatesAvailableNotifDescription": "Értesíti a felhasználót, hogy frissítések állnak rendelkezésre egy vagy több, az Obtainium által nyomon követett alkalmazáshoz",
|
"updatesAvailableNotifDescription": "Értesíti a felhasználót, hogy frissítések állnak rendelkezésre egy vagy több, az Obtainium által nyomon követett alkalmazáshoz",
|
||||||
"noNewUpdates": "Nincsenek új frissítések.",
|
"noNewUpdates": "Nincsenek új frissítések.",
|
||||||
"xHasAnUpdate": "{} frissítést kapott.",
|
"xHasAnUpdate": "A(z) {} frissítést kapott.",
|
||||||
"appsUpdated": "Alkalmazások frissítve",
|
"appsUpdated": "Alkalmazások frissítve",
|
||||||
"appsUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több app frissítése történt a háttérben",
|
"appsUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy/több app frissítése megtörtént a háttérben",
|
||||||
"xWasUpdatedToY": "{} frissítve a következőre: {}.",
|
"xWasUpdatedToY": "{} frissítve a következőre: {}.",
|
||||||
"errorCheckingUpdates": "Hiba a frissítések keresésekor",
|
"errorCheckingUpdates": "Hiba a frissítések keresésekor",
|
||||||
"errorCheckingUpdatesNotifDescription": "Értesítés, amely akkor jelenik meg, ha a háttérbeli frissítések ellenőrzése sikertelen",
|
"errorCheckingUpdatesNotifDescription": "Értesítés, amely akkor jelenik meg, ha a háttérbeli frissítések ellenőrzése sikertelen",
|
||||||
"appsRemoved": "Alkalmazások eltávolítva",
|
"appsRemoved": "Alkalmazások eltávolítva",
|
||||||
"appsRemovedNotifDescription": "Értesíti a felhasználót egy vagy több alkalmazás eltávolításáról a betöltésük során fellépő hibák miatt",
|
"appsRemovedNotifDescription": "Értesíti a felhasználót egy vagy több alkalmazás eltávolításáról a betöltésük során fellépő hibák miatt",
|
||||||
"xWasRemovedDueToErrorY": "A(z) {} a következő hiba miatt lett eltávolítva: {}",
|
"xWasRemovedDueToErrorY": "A(z) {} a következő hiba miatt lett eltávolítva: {}",
|
||||||
"completeAppInstallation": "Teljes alkalmazástelepítés",
|
"completeAppInstallation": "Teljes app telepítés",
|
||||||
"obtainiumMustBeOpenToInstallApps": "Az Obtainiumnak megnyitva kell lennie az alkalmazások telepítéséhez",
|
"obtainiumMustBeOpenToInstallApps": "Az Obtainiumnak megnyitva kell lennie az alkalmazások telepítéséhez",
|
||||||
"completeAppInstallationNotifDescription": "Megkéri a felhasználót, hogy térjen vissza az Obtainiumhoz, hogy befejezze az alkalmazás telepítését",
|
"completeAppInstallationNotifDescription": "Megkéri a felhasználót, hogy térjen vissza az Obtainiumhoz, hogy befejezze az alkalmazás telepítését",
|
||||||
"checkingForUpdates": "Frissítések keresése",
|
"checkingForUpdates": "Frissítések keresése",
|
||||||
@ -178,35 +177,45 @@
|
|||||||
"installedVersionX": "Telepített verzió: {}",
|
"installedVersionX": "Telepített verzió: {}",
|
||||||
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
"removeAppQuestion": "Eltávolítja az alkalmazást?",
|
|
||||||
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "App ID vagy név",
|
"appIdOrName": "App ID vagy név",
|
||||||
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
||||||
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Harmadik fél Repo",
|
"fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo",
|
||||||
"steam": "Steam",
|
"steam": "Steam",
|
||||||
"steamMobile": "Steam Mobile",
|
"steamMobile": "Steam Mobile",
|
||||||
"steamChat": "Steam Chat",
|
"steamChat": "Steam Chat",
|
||||||
"install": "Install",
|
"install": "Telepít",
|
||||||
"markInstalled": "Mark Installed",
|
"markInstalled": "Telepítettnek jelöl",
|
||||||
"update": "Update",
|
"update": "Frissít",
|
||||||
"markUpdated": "Mark Updated",
|
"markUpdated": "Frissítettnek jelöl",
|
||||||
"additionalOptions": "Additional Options",
|
"additionalOptions": "További lehetőségek",
|
||||||
"disableVersionDetection": "Disable Version Detection",
|
"disableVersionDetection": "Verzióérzékelés letiltása",
|
||||||
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
|
"noVersionDetectionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzióérzékelés nem működik megfelelően.",
|
||||||
"downloadingX": "Downloading {}",
|
"downloadingX": "{} letöltés",
|
||||||
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
|
"downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról",
|
||||||
"noAPKFound": "No APK found",
|
"noAPKFound": "Nem található APK",
|
||||||
"noVersionDetection": "No version detection",
|
"noVersionDetection": "Nincs verzió érzékelés",
|
||||||
"categorize": "Categorize",
|
"categorize": "Kategorizálás",
|
||||||
"categories": "Categories",
|
"categories": "Kategóriák",
|
||||||
"category": "Category",
|
"category": "Kategória",
|
||||||
"noCategory": "No Category",
|
"noCategory": "Nincs kategória",
|
||||||
"deleteCategoryQuestion": "Delete Category?",
|
"deleteCategoryQuestion": "Törli a kategóriát?",
|
||||||
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
|
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
|
||||||
"addCategory": "Add Category",
|
"addCategory": "Új kategória",
|
||||||
"label": "Label",
|
"label": "Címke",
|
||||||
|
"language": "Nyelv",
|
||||||
|
"storagePermissionDenied": "Tárhely engedély megtagadva",
|
||||||
|
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
|
||||||
|
"filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
|
||||||
|
"removeFromObtainium": "Eltávolítás az Obtainiumból",
|
||||||
|
"uninstallFromDevice": "Eltávolítás a készülékről",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Eltávolítja az alkalmazást?",
|
||||||
|
"other": "Eltávolítja az alkalmazást?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
||||||
"other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
|
"other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
|
||||||
@ -251,4 +260,4 @@
|
|||||||
"one": "A(z) {} és 1 további alkalmazás frissítve.",
|
"one": "A(z) {} és 1 további alkalmazás frissítve.",
|
||||||
"other": "{} és további {} alkalmazás frissítve."
|
"other": "{} és további {} alkalmazás frissítve."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
"startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in background",
|
"startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in background",
|
||||||
"bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in background",
|
"bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in background",
|
||||||
"firstRun": "Questo è il primo avvio di sempre di Obtainium",
|
"firstRun": "Questo è il primo avvio di sempre di Obtainium",
|
||||||
"settingUpdateCheckIntervalTo": "Imposta l'intervallo di aggiornamento a {}",
|
"settingUpdateCheckIntervalTo": "Fissato intervallo di aggiornamento a {}",
|
||||||
"githubPATLabel": "GitHub Personal Access Token (aumenta il limite di traffico)",
|
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
||||||
"githubPATHint": "PAT deve seguire questo formato: username:token",
|
"githubPATHint": "PAT deve seguire questo formato: username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "Informazioni su GitHub PAT",
|
"githubPATLinkText": "Informazioni su GitHub PAT",
|
||||||
@ -31,18 +31,18 @@
|
|||||||
"requiredInBrackets": "(richiesto)",
|
"requiredInBrackets": "(richiesto)",
|
||||||
"dropdownNoOptsError": "ERRORE: LA TENDINA DEVE AVERE ALMENO UN'OPZIONE",
|
"dropdownNoOptsError": "ERRORE: LA TENDINA DEVE AVERE ALMENO UN'OPZIONE",
|
||||||
"colour": "Colore",
|
"colour": "Colore",
|
||||||
"githubStarredRepos": "i repository stellati da GitHub",
|
"githubStarredRepos": "repository stellati da GitHub",
|
||||||
"uname": "Username",
|
"uname": "Username",
|
||||||
"wrongArgNum": "Numero di argomenti forniti errato",
|
"wrongArgNum": "Numero di argomenti forniti errato",
|
||||||
"xIsTrackOnly": "{} è Solo-Monitoraggio",
|
"xIsTrackOnly": "{} è in modalità Solo-Monitoraggio",
|
||||||
"source": "Fonte",
|
"source": "Fonte",
|
||||||
"app": "App",
|
"app": "App",
|
||||||
"appsFromSourceAreTrackOnly": "Le App da questa fonte sono in modalità 'Solo-Monitoraggio'.",
|
"appsFromSourceAreTrackOnly": "Le App da questa fonte sono in modalità 'Solo-Monitoraggio'.",
|
||||||
"youPickedTrackOnly": "Hai selezionato l'opzione 'Solo-Monitoraggio'.",
|
"youPickedTrackOnly": "È stata selezionata l'opzione 'Solo-Monitoraggio'.",
|
||||||
"trackOnlyAppDescription": "L'App sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.",
|
"trackOnlyAppDescription": "L'App sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.",
|
||||||
"cancelled": "Annullato",
|
"cancelled": "Annullato",
|
||||||
"appAlreadyAdded": "App già aggiunta",
|
"appAlreadyAdded": "App già aggiunta",
|
||||||
"alreadyUpToDateQuestion": "App già aggiornata?",
|
"alreadyUpToDateQuestion": "L'App è già aggiornata?",
|
||||||
"addApp": "Aggiungi App",
|
"addApp": "Aggiungi App",
|
||||||
"appSourceURL": "URL della fonte dell'App",
|
"appSourceURL": "URL della fonte dell'App",
|
||||||
"error": "Errore",
|
"error": "Errore",
|
||||||
@ -60,21 +60,20 @@
|
|||||||
"percentProgress": "Progresso: {}%",
|
"percentProgress": "Progresso: {}%",
|
||||||
"pleaseWait": "Attendere prego",
|
"pleaseWait": "Attendere prego",
|
||||||
"updateAvailable": "Aggiornamento disponibile",
|
"updateAvailable": "Aggiornamento disponibile",
|
||||||
"estimateInBracketsShort": "(Prev.)",
|
"estimateInBracketsShort": "(prev.)",
|
||||||
"notInstalled": "Non installato",
|
"notInstalled": "Non installato",
|
||||||
"estimateInBrackets": "(Previsto)",
|
"estimateInBrackets": "(previsto)",
|
||||||
"selectAll": "Seleziona tutto",
|
"selectAll": "Seleziona tutto",
|
||||||
"deselectN": "Deseleziona {}",
|
"deselectN": "Deseleziona {}",
|
||||||
"xWillBeRemovedButRemainInstalled": "{} sarà rimosso da Obtainium ma resterà installato sul dispositivo.",
|
"xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
|
||||||
"removeSelectedAppsQuestion": "Rimuovere le App selezionate?",
|
"removeSelectedAppsQuestion": "Rimuovere le App selezionate?",
|
||||||
"removeSelectedApps": "Rimuovi le App selezionate",
|
"removeSelectedApps": "Rimuovi le App selezionate",
|
||||||
"updateX": "Aggiorna {}",
|
"updateX": "Aggiorna {}",
|
||||||
"installX": "Installa {}",
|
"installX": "Installa {}",
|
||||||
"markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornato",
|
"markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornato",
|
||||||
"changeX": "modifica {}",
|
"changeX": "Modifica {}",
|
||||||
"installUpdateApps": "Installa/Aggiorna le App",
|
"installUpdateApps": "Installa/Aggiorna App",
|
||||||
"installUpdateSelectedApps": "Installa/Aggiornale le App selezionate",
|
"installUpdateSelectedApps": "Installa/Aggiorna le App selezionate",
|
||||||
"onlyWorksWithNonEVDApps": "Funziona solo per le App il cui stato d'installazione non può essere rilevato automaticamente (inconsueto).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?",
|
"markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"yes": "Sì",
|
"yes": "Sì",
|
||||||
@ -95,7 +94,7 @@
|
|||||||
"author": "Autore",
|
"author": "Autore",
|
||||||
"upToDateApps": "App aggiornate",
|
"upToDateApps": "App aggiornate",
|
||||||
"nonInstalledApps": "App non installate",
|
"nonInstalledApps": "App non installate",
|
||||||
"importExport": "Importa/Esporta",
|
"importExport": "Importa - Esporta",
|
||||||
"settings": "Impostazioni",
|
"settings": "Impostazioni",
|
||||||
"exportedTo": "Esportato in {}",
|
"exportedTo": "Esportato in {}",
|
||||||
"obtainiumExport": "Esporta da Obtainium",
|
"obtainiumExport": "Esporta da Obtainium",
|
||||||
@ -134,7 +133,7 @@
|
|||||||
"neverManualOnly": "Mai - Solo manuale",
|
"neverManualOnly": "Mai - Solo manuale",
|
||||||
"appearance": "Aspetto",
|
"appearance": "Aspetto",
|
||||||
"showWebInAppView": "Mostra pagina web dell'App se selezionata",
|
"showWebInAppView": "Mostra pagina web dell'App se selezionata",
|
||||||
"pinUpdates": "Fissa in alto gli aggiornamenti disponibili",
|
"pinUpdates": "Fissa aggiornamenti disponibili in alto",
|
||||||
"updates": "Aggiornamenti",
|
"updates": "Aggiornamenti",
|
||||||
"sourceSpecific": "Specifiche per la fonte",
|
"sourceSpecific": "Specifiche per la fonte",
|
||||||
"appSource": "Sorgente dell'App",
|
"appSource": "Sorgente dell'App",
|
||||||
@ -146,21 +145,21 @@
|
|||||||
"obtainiumExportHyphenatedLowercase": "esportazione-obtainium",
|
"obtainiumExportHyphenatedLowercase": "esportazione-obtainium",
|
||||||
"pickAnAPK": "Seleziona un APK",
|
"pickAnAPK": "Seleziona un APK",
|
||||||
"appHasMoreThanOnePackage": "{} offre più di un pacchetto:",
|
"appHasMoreThanOnePackage": "{} offre più di un pacchetto:",
|
||||||
"deviceSupportsXArch": "Il tuo dispositivo supporta l'architettura {} della CPU.",
|
"deviceSupportsXArch": "Il dispositivo in uso supporta l'architettura {} della CPU.",
|
||||||
"deviceSupportsFollowingArchs": "Il tuo dispositivo supporta le seguenti architetture della CPU:",
|
"deviceSupportsFollowingArchs": "Il dispositivo in uso supporta le seguenti architetture della CPU:",
|
||||||
"warning": "Attenzione",
|
"warning": "Attenzione",
|
||||||
"sourceIsXButPackageFromYPrompt": "L'origine dell'App è '{}' ma il pacchetto della release proviene da '{}'. Continuare?",
|
"sourceIsXButPackageFromYPrompt": "L'origine dell'App è '{}' ma il pacchetto della release proviene da '{}'. Continuare?",
|
||||||
"updatesAvailable": "Aggiornamenti disponibili",
|
"updatesAvailable": "Aggiornamenti disponibili",
|
||||||
"updatesAvailableNotifDescription": "Avvisa l'utente che sono disponibili gli aggiornamenti di una o più App monitorate da Obtainium",
|
"updatesAvailableNotifDescription": "Notifica all'utente che sono disponibili gli aggiornamenti di una o più App monitorate da Obtainium",
|
||||||
"noNewUpdates": "Nessun nuovo aggiornamento.",
|
"noNewUpdates": "Nessun nuovo aggiornamento.",
|
||||||
"xHasAnUpdate": "{} è stato aggiornato.",
|
"xHasAnUpdate": "Aggiornamento disponibile per {}",
|
||||||
"appsUpdated": "App aggiornate",
|
"appsUpdated": "App aggiornate",
|
||||||
"appsUpdatedNotifDescription": "Avvisa l'utente che una o più App sono state aggiornate in background",
|
"appsUpdatedNotifDescription": "Notifica all'utente che una o più App sono state aggiornate in background",
|
||||||
"xWasUpdatedToY": "{} è stato aggiornato a {}.",
|
"xWasUpdatedToY": "{} è stato aggiornato a {}.",
|
||||||
"errorCheckingUpdates": "Controllo degli errori per gli aggiornamenti",
|
"errorCheckingUpdates": "Controllo degli errori per gli aggiornamenti",
|
||||||
"errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in background fallisce",
|
"errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in background fallisce",
|
||||||
"appsRemoved": "App rimosse",
|
"appsRemoved": "App rimosse",
|
||||||
"appsRemovedNotifDescription": "Avvisa l'utente che una o più App sono state rimosse a causa di errori durante il caricamento",
|
"appsRemovedNotifDescription": "Notifica all'utente che una o più App sono state rimosse a causa di errori durante il caricamento",
|
||||||
"xWasRemovedDueToErrorY": "{} è stata rimosso a causa di questo errore: {}",
|
"xWasRemovedDueToErrorY": "{} è stata rimosso a causa di questo errore: {}",
|
||||||
"completeAppInstallation": "Completa l'installazione dell'App",
|
"completeAppInstallation": "Completa l'installazione dell'App",
|
||||||
"obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le App",
|
"obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le App",
|
||||||
@ -178,7 +177,6 @@
|
|||||||
"installedVersionX": "Versione installata: {}",
|
"installedVersionX": "Versione installata: {}",
|
||||||
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
||||||
"remove": "Rimuovi",
|
"remove": "Rimuovi",
|
||||||
"removeAppQuestion": "Rimuovere App?",
|
|
||||||
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "ID o nome dell'App",
|
"appIdOrName": "ID o nome dell'App",
|
||||||
@ -188,36 +186,48 @@
|
|||||||
"steam": "Steam",
|
"steam": "Steam",
|
||||||
"steamMobile": "Steam Mobile",
|
"steamMobile": "Steam Mobile",
|
||||||
"steamChat": "Steam Chat",
|
"steamChat": "Steam Chat",
|
||||||
"install": "Install",
|
"install": "Installa",
|
||||||
"markInstalled": "Mark Installed",
|
"markInstalled": "Contrassegna come installato",
|
||||||
"update": "Update",
|
"update": "Aggiorna",
|
||||||
"markUpdated": "Mark Updated",
|
"markUpdated": "Contrassegna come aggiornato",
|
||||||
"additionalOptions": "Additional Options",
|
"additionalOptions": "Opzioni aggiuntive",
|
||||||
"disableVersionDetection": "Disable Version Detection",
|
"disableVersionDetection": "Disattiva il rilevamento della versione",
|
||||||
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
|
"noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le App la cui versione non viene rilevata correttamente.",
|
||||||
"downloadingX": "Downloading {}",
|
"downloadingX": "Scaricamento di {} in corso",
|
||||||
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
|
"downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'App",
|
||||||
"noAPKFound": "No APK found",
|
"noAPKFound": "Nessun APK trovato",
|
||||||
"noVersionDetection": "No version detection",
|
"noVersionDetection": "Disattiva rilevamento di versione",
|
||||||
"categorize": "Categorize",
|
"categorize": "Aggiungi a categoria",
|
||||||
"categories": "Categories",
|
"categories": "Categorie",
|
||||||
"category": "Category",
|
"category": "Categoria",
|
||||||
"noCategory": "No Category",
|
"noCategory": "Nessuna categoria",
|
||||||
"deleteCategoryQuestion": "Delete Category?",
|
"noCategories": "Nessuna categoria",
|
||||||
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
|
"deleteCategoriesQuestion": "Eliminare le categorie?",
|
||||||
"addCategory": "Add Category",
|
"categoryDeleteWarning": "Tutte le App nelle categorie eliminate saranno impostate come non categorizzate.",
|
||||||
"label": "Label",
|
"addCategory": "Aggiungi categoria",
|
||||||
|
"label": "Etichetta",
|
||||||
|
"language": "Lingua",
|
||||||
|
"storagePermissionDenied": "Accesso ai file non autorizzato",
|
||||||
|
"selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.",
|
||||||
|
"filterAPKsByRegEx": "Filtra file APK con espressioni regolari",
|
||||||
|
"removeFromObtainium": "Rimuovi da Obtainium",
|
||||||
|
"uninstallFromDevice": "Disinstalla dal dispositivo",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Funziona solo per le App con il rilevamento della versione disattivato.",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Rimuovere l'App?",
|
||||||
|
"other": "Rimuovere le App?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
||||||
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
||||||
},
|
},
|
||||||
"bgUpdateGotErrorRetryInMinutes": {
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
"one": "Il controllo degli aggiornamenti in background ha incontrato un {}, ricontrollo tra {} minuto",
|
"one": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuto",
|
||||||
"other": "Il controllo degli aggiornamenti in background ha incontrato un {}, ricontrollo tra {} minuti"
|
"other": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuti"
|
||||||
},
|
},
|
||||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
"one": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamento - avviserà l'utento se necessario",
|
"one": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamento - notificherà l'utente se necessario",
|
||||||
"other": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamenti - avviserà l'utento se necessario"
|
"other": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamenti - notificherà l'utente se necessario"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"one": "{} App",
|
"one": "{} App",
|
||||||
@ -251,4 +261,4 @@
|
|||||||
"one": "{} e un'altra App sono state aggiornate.",
|
"one": "{} e un'altra App sono state aggiornate.",
|
||||||
"other": "{} e altre {} App sono state aggiornate."
|
"other": "{} e altre {} App sono state aggiornate."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,12 @@
|
|||||||
"appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません",
|
"appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません",
|
||||||
"functionNotImplemented": "このクラスはこの機能を実装していません",
|
"functionNotImplemented": "このクラスはこの機能を実装していません",
|
||||||
"placeholder": "プレースホルダー",
|
"placeholder": "プレースホルダー",
|
||||||
"someErrors": "いくつかのエラーが発生しました",
|
"someErrors": "何らかのエラーが発生しました",
|
||||||
"unexpectedError": "予期せぬエラーが発生しました",
|
"unexpectedError": "予期せぬエラーが発生しました",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"and": "と",
|
"and": "と",
|
||||||
"startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始",
|
"startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始",
|
||||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||||
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
|
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
|
||||||
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
|
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
|
||||||
"firstRun": "これがObtainiumの最初の実行です",
|
"firstRun": "これがObtainiumの最初の実行です",
|
||||||
@ -27,7 +27,7 @@
|
|||||||
"invalidRegEx": "無効な正規表現",
|
"invalidRegEx": "無効な正規表現",
|
||||||
"noDescription": "説明はありません",
|
"noDescription": "説明はありません",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"continue": "続ける",
|
"continue": "続行",
|
||||||
"requiredInBrackets": "(必須)",
|
"requiredInBrackets": "(必須)",
|
||||||
"dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です",
|
"dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です",
|
||||||
"colour": "カラー",
|
"colour": "カラー",
|
||||||
@ -64,17 +64,16 @@
|
|||||||
"notInstalled": "未インストール",
|
"notInstalled": "未インストール",
|
||||||
"estimateInBrackets": "(推定)",
|
"estimateInBrackets": "(推定)",
|
||||||
"selectAll": "すべて選択",
|
"selectAll": "すべて選択",
|
||||||
"deselectN": "{}件を選択解除",
|
"deselectN": "{}件の選択を解除",
|
||||||
"xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。",
|
"xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
|
||||||
"removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
|
"removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
|
||||||
"removeSelectedApps": "選択したアプリを削除する",
|
"removeSelectedApps": "選択したアプリを削除する",
|
||||||
"updateX": "{}をアップデートする",
|
"updateX": "{} をアップデートする",
|
||||||
"installX": "{}をインストールする",
|
"installX": "{} をインストールする",
|
||||||
"markXTrackOnlyAsUpdated": "{}\n(追跡のみ)\nをアップデート済みとしてマークする",
|
"markXTrackOnlyAsUpdated": "{}\n(追跡のみ)\nをアップデート済みとしてマークする",
|
||||||
"changeX": "{}を変更する",
|
"changeX": "{} を変更する",
|
||||||
"installUpdateApps": "アプリのインストール/アップデート",
|
"installUpdateApps": "アプリのインストール/アップデート",
|
||||||
"installUpdateSelectedApps": "選択したアプリのインストール/アップデート",
|
"installUpdateSelectedApps": "選択したアプリのインストール/アップデート",
|
||||||
"onlyWorksWithNonEVDApps": "インストール状況を自動検出できないアプリ(一般的でないもの)のみ動作します。",
|
|
||||||
"markXSelectedAppsAsUpdated": "{}個の選択したアプリをアップデート済みとしてマークしますか?",
|
"markXSelectedAppsAsUpdated": "{}個の選択したアプリをアップデート済みとしてマークしますか?",
|
||||||
"no": "いいえ",
|
"no": "いいえ",
|
||||||
"yes": "はい",
|
"yes": "はい",
|
||||||
@ -82,7 +81,7 @@
|
|||||||
"pinToTop": "トップに固定",
|
"pinToTop": "トップに固定",
|
||||||
"unpinFromTop": "トップから固定解除",
|
"unpinFromTop": "トップから固定解除",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
||||||
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗するなどして、Obtainiumに表示されるアプリのバージョンが正しくない場合に役立ちます。",
|
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。",
|
||||||
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
||||||
"resetInstallStatus": "インストール状態をリセットする",
|
"resetInstallStatus": "インストール状態をリセットする",
|
||||||
"more": "もっと見る",
|
"more": "もっと見る",
|
||||||
@ -109,7 +108,7 @@
|
|||||||
"searchX": "{}で検索",
|
"searchX": "{}で検索",
|
||||||
"noResults": "結果は見つかりませんでした",
|
"noResults": "結果は見つかりませんでした",
|
||||||
"importX": "{}をインポートする",
|
"importX": "{}をインポートする",
|
||||||
"importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティーのインポートメソッドにのみ影響します。",
|
"importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティのインポートメソッドにのみ影響します。",
|
||||||
"importErrors": "インポートエラー",
|
"importErrors": "インポートエラー",
|
||||||
"importedXOfYApps": "{} / {} アプリをインポートしました",
|
"importedXOfYApps": "{} / {} アプリをインポートしました",
|
||||||
"followingURLsHadErrors": "以下のURLでエラーが発生しました:",
|
"followingURLsHadErrors": "以下のURLでエラーが発生しました:",
|
||||||
@ -133,9 +132,9 @@
|
|||||||
"bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔",
|
"bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔",
|
||||||
"neverManualOnly": "手動",
|
"neverManualOnly": "手動",
|
||||||
"appearance": "外観",
|
"appearance": "外観",
|
||||||
"showWebInAppView": "アプリビューにソースウェブページを表示する",
|
"showWebInAppView": "アプリページにソースのWebページを表示する",
|
||||||
"pinUpdates": "アップデートがあるアプリをトップに固定する",
|
"pinUpdates": "アップデートがあるアプリをトップに固定する",
|
||||||
"updates": "更新",
|
"updates": "アップデート",
|
||||||
"sourceSpecific": "Github アクセストークン",
|
"sourceSpecific": "Github アクセストークン",
|
||||||
"appSource": "アプリのソース",
|
"appSource": "アプリのソース",
|
||||||
"noLogs": "ログはありません",
|
"noLogs": "ログはありません",
|
||||||
@ -144,24 +143,24 @@
|
|||||||
"share": "共有",
|
"share": "共有",
|
||||||
"appNotFound": "アプリが見つかりません",
|
"appNotFound": "アプリが見つかりません",
|
||||||
"obtainiumExportHyphenatedLowercase": "obtainium-エクスポート",
|
"obtainiumExportHyphenatedLowercase": "obtainium-エクスポート",
|
||||||
"pickAnAPK": "APKを選ぶ",
|
"pickAnAPK": "APKを選択",
|
||||||
"appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ",
|
"appHasMoreThanOnePackage": "{} は複数のパッケージが存在します: ",
|
||||||
"deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。",
|
"deviceSupportsXArch": "お使いのデバイスは {} CPUアーキテクチャに対応しています。",
|
||||||
"deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:",
|
"deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?",
|
"sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?",
|
||||||
"updatesAvailable": "アップデートが利用可能",
|
"updatesAvailable": "アップデートが利用可能",
|
||||||
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
|
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
|
||||||
"noNewUpdates": "新しいアップデートはありません",
|
"noNewUpdates": "新しいアップデートはありません",
|
||||||
"xHasAnUpdate": "{}のアップデートが利用可能です",
|
"xHasAnUpdate": "{} のアップデートが利用可能です",
|
||||||
"appsUpdated": "アプリをアップデートしました",
|
"appsUpdated": "アプリをアップデートしました",
|
||||||
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
|
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
|
||||||
"xWasUpdatedToY": "{}が{}にアップデートされました",
|
"xWasUpdatedToY": "{} が {} にアップデートされました",
|
||||||
"errorCheckingUpdates": "アップデート確認中のエラー",
|
"errorCheckingUpdates": "アップデート確認中のエラー",
|
||||||
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
|
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
|
||||||
"appsRemoved": "削除されたアプリ",
|
"appsRemoved": "削除されたアプリ",
|
||||||
"appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する",
|
"appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する",
|
||||||
"xWasRemovedDueToErrorY": "このエラーのため、{}は削除されました: {}",
|
"xWasRemovedDueToErrorY": "このエラーのため、{} は削除されました: {}",
|
||||||
"completeAppInstallation": "アプリのインストールを完了する",
|
"completeAppInstallation": "アプリのインストールを完了する",
|
||||||
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開いている必要があります。",
|
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開いている必要があります。",
|
||||||
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります。",
|
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります。",
|
||||||
@ -178,13 +177,12 @@
|
|||||||
"installedVersionX": "インストールされたバージョン: {}",
|
"installedVersionX": "インストールされたバージョン: {}",
|
||||||
"lastUpdateCheckX": "最終アップデート確認: {}",
|
"lastUpdateCheckX": "最終アップデート確認: {}",
|
||||||
"remove": "削除",
|
"remove": "削除",
|
||||||
"removeAppQuestion": "アプリを削除しますか?",
|
|
||||||
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "アプリのIDまたは名前",
|
"appIdOrName": "アプリのIDまたは名前",
|
||||||
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
||||||
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
|
||||||
"steam": "Steam",
|
"steam": "Steam",
|
||||||
"steamMobile": "Steam Mobile",
|
"steamMobile": "Steam Mobile",
|
||||||
"steamChat": "Steam Chat",
|
"steamChat": "Steam Chat",
|
||||||
@ -203,10 +201,22 @@
|
|||||||
"categories": "カテゴリ",
|
"categories": "カテゴリ",
|
||||||
"category": "カテゴリ",
|
"category": "カテゴリ",
|
||||||
"noCategory": "カテゴリなし",
|
"noCategory": "カテゴリなし",
|
||||||
"deleteCategoryQuestion": "カテゴリを削除しますか?",
|
"noCategories": "カテゴリなし",
|
||||||
"categoryDeleteWarning": "「{}」内のすべてのアプリは未分類に設定されます。",
|
"deleteCategoriesQuestion": "カテゴリを削除しますか?",
|
||||||
|
"categoryDeleteWarning": "削除されたカテゴリ内のアプリは未分類に設定されます。",
|
||||||
"addCategory": "カテゴリを追加",
|
"addCategory": "カテゴリを追加",
|
||||||
"label": "ラベル",
|
"label": "ラベル",
|
||||||
|
"language": "言語",
|
||||||
|
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
||||||
|
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
||||||
|
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
|
||||||
|
"removeFromObtainium": "Obtainiumから削除する",
|
||||||
|
"uninstallFromDevice": "デバイスからアンインストールする",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "アプリを削除しますか?",
|
||||||
|
"other": "アプリを削除しますか?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
||||||
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
||||||
@ -244,11 +254,11 @@
|
|||||||
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
|
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesAvailable": {
|
"xAndNMoreUpdatesAvailable": {
|
||||||
"one": "{}とさらに{}個のアプリのアップデートが利用可能です",
|
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
|
||||||
"other": "{}とさらに{}個のアプリのアップデートが利用可能です"
|
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesInstalled": {
|
"xAndNMoreUpdatesInstalled": {
|
||||||
"one": "{}とさらに{}個のアプリがアップデートされました",
|
"one": "{} とさらに {} 個のアプリがアップデートされました",
|
||||||
"other": "{}とさらに{}個のアプリがアップデートされました"
|
"other": "{} とさらに {} 個のアプリがアップデートされました"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
"ok": "好的",
|
"ok": "好的",
|
||||||
"and": "和",
|
"and": "和",
|
||||||
"startedBgUpdateTask": "开始后台检查更新任务",
|
"startedBgUpdateTask": "开始后台检查更新任务",
|
||||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
"bgUpdateIgnoreAfterIs": "下次后台更新检查 {}",
|
||||||
"startedActualBGUpdateCheck": "后台检查更新已开始",
|
"startedActualBGUpdateCheck": "后台检查更新已开始",
|
||||||
"bgUpdateTaskFinished": "后台检查更新已完成",
|
"bgUpdateTaskFinished": "后台检查更新已完成",
|
||||||
"firstRun": "这是你第一次运行 Obtainium",
|
"firstRun": "这是你第一次运行 Obtainium",
|
||||||
@ -178,7 +178,6 @@
|
|||||||
"installedVersionX": "已安装: {}",
|
"installedVersionX": "已安装: {}",
|
||||||
"lastUpdateCheckX": "最后检查: {}",
|
"lastUpdateCheckX": "最后检查: {}",
|
||||||
"remove": "删除",
|
"remove": "删除",
|
||||||
"removeAppQuestion": "删除应用?",
|
|
||||||
"yesMarkUpdated": "'是的,标为已更新",
|
"yesMarkUpdated": "'是的,标为已更新",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "应用 ID 或名称",
|
"appIdOrName": "应用 ID 或名称",
|
||||||
@ -199,14 +198,25 @@
|
|||||||
"downloadNotifDescription": "通知用户下载进度",
|
"downloadNotifDescription": "通知用户下载进度",
|
||||||
"noAPKFound": "未找到安装包",
|
"noAPKFound": "未找到安装包",
|
||||||
"noVersionDetection": "无版本检测",
|
"noVersionDetection": "无版本检测",
|
||||||
"categorize": "Categorize",
|
"categorize": "归档",
|
||||||
"categories": "Categories",
|
"categories": "归档",
|
||||||
"category": "Category",
|
"category": "类别",
|
||||||
"noCategory": "No Category",
|
"noCategory": "无类别",
|
||||||
"deleteCategoryQuestion": "Delete Category?",
|
"noCategories": "无类别",
|
||||||
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
|
"deleteCategoriesQuestion": "删除所有类别?",
|
||||||
"addCategory": "Add Category",
|
"categoryDeleteWarning": "所有被删除类别的应用程序将被设置为无类别",
|
||||||
"label": "Label",
|
"addCategory": "添加类别",
|
||||||
|
"label": "标签",
|
||||||
|
"language": "语言",
|
||||||
|
"storagePermissionDenied": "存储权限已被拒绝",
|
||||||
|
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
|
"removeFromObtainium": "Remove from Obtainium",
|
||||||
|
"uninstallFromDevice": "Uninstall from Device",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "删除应用?",
|
||||||
|
"other": "删除应用?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
||||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
||||||
|
@ -46,7 +46,7 @@ class APKMirror extends AppSource {
|
|||||||
}
|
}
|
||||||
return APKDetails(version, [], getAppNames(standardUrl));
|
return APKDetails(version, [], getAppNames(standardUrl));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
149
lib/app_sources/codeberg.dart
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:easy_localization/easy_localization.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';
|
||||||
|
|
||||||
|
class Codeberg extends AppSource {
|
||||||
|
Codeberg() {
|
||||||
|
host = 'codeberg.org';
|
||||||
|
|
||||||
|
additionalSourceSpecificSettingFormItems = [];
|
||||||
|
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('includePrereleases',
|
||||||
|
label: tr('includePrereleases'), defaultValue: false)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
||||||
|
label: tr('filterReleaseTitlesByRegEx'),
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
return regExValidator(value);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
canSearch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
|
'$standardUrl/releases';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
bool includePrereleases = additionalSettings['includePrereleases'];
|
||||||
|
bool fallbackToOlderReleases =
|
||||||
|
additionalSettings['fallbackToOlderReleases'];
|
||||||
|
String? regexFilter =
|
||||||
|
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
||||||
|
?.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
|
: null;
|
||||||
|
Response res = await get(Uri.parse(
|
||||||
|
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
|
List<String> getReleaseAPKUrls(dynamic release) =>
|
||||||
|
(release['assets'] as List<dynamic>?)
|
||||||
|
?.map((e) {
|
||||||
|
return e['name'] != null && e['browser_download_url'] != null
|
||||||
|
? MapEntry(e['name'] as String,
|
||||||
|
e['browser_download_url'] as String)
|
||||||
|
: const MapEntry('', '');
|
||||||
|
})
|
||||||
|
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
dynamic targetRelease;
|
||||||
|
|
||||||
|
for (int i = 0; i < releases.length; i++) {
|
||||||
|
if (!fallbackToOlderReleases && i > 0) break;
|
||||||
|
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (releases[i]['draft'] == true) {
|
||||||
|
// Draft releases not supported
|
||||||
|
}
|
||||||
|
var nameToFilter = releases[i]['name'] as String?;
|
||||||
|
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
|
||||||
|
// Some leave titles empty so tag is used
|
||||||
|
nameToFilter = releases[i]['tag_name'] as String;
|
||||||
|
}
|
||||||
|
if (regexFilter != null &&
|
||||||
|
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||||
|
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
targetRelease = releases[i];
|
||||||
|
targetRelease['apkUrls'] = apkUrls;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (targetRelease == null) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
String? version = targetRelease['tag_name'];
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
||||||
|
getAppNames(standardUrl));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppNames getAppNames(String standardUrl) {
|
||||||
|
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
|
||||||
|
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
||||||
|
return AppNames(names[0], names[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, String>> search(String query) async {
|
||||||
|
Response res = await get(Uri.parse(
|
||||||
|
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
Map<String, String> urlsWithDescriptions = {};
|
||||||
|
for (var e in (jsonDecode(res.body)['data'] as List<dynamic>)) {
|
||||||
|
urlsWithDescriptions.addAll({
|
||||||
|
e['html_url'] as String: e['description'] != null
|
||||||
|
? e['description'] as String
|
||||||
|
: tr('noDescription')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return urlsWithDescriptions;
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,7 +54,7 @@ class FDroid extends AppSource {
|
|||||||
return APKDetails(latestVersion, apkUrls,
|
return APKDetails(latestVersion, apkUrls,
|
||||||
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
|
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class FDroidRepo extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
RegExp standardUrlRegExp =
|
RegExp standardUrlRegExp =
|
||||||
RegExp('^https?://.+/fdroid/(repo(/|\\?)|repo\$)');
|
RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)');
|
||||||
RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -80,7 +80,7 @@ class FDroidRepo extends AppSource {
|
|||||||
.toList();
|
.toList();
|
||||||
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName));
|
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ class GitHub extends AppSource {
|
|||||||
additionalSourceSpecificSettingFormItems = [
|
additionalSourceSpecificSettingFormItems = [
|
||||||
GeneratedFormTextField('github-creds',
|
GeneratedFormTextField('github-creds',
|
||||||
label: tr('githubPATLabel'),
|
label: tr('githubPATLabel'),
|
||||||
|
password: true,
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
@ -64,15 +65,7 @@ class GitHub extends AppSource {
|
|||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
if (value == null || value.isEmpty) {
|
return regExValidator(value);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
RegExp(value);
|
|
||||||
} catch (e) {
|
|
||||||
return tr('invalidRegEx');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
@ -118,7 +111,7 @@ class GitHub extends AppSource {
|
|||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
@ -140,10 +133,13 @@ class GitHub extends AppSource {
|
|||||||
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
var nameToFilter = releases[i]['name'] as String?;
|
||||||
|
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
|
||||||
|
// Some leave titles empty so tag is used
|
||||||
|
nameToFilter = releases[i]['tag_name'] as String;
|
||||||
|
}
|
||||||
if (regexFilter != null &&
|
if (regexFilter != null &&
|
||||||
!RegExp(regexFilter)
|
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
||||||
.hasMatch((releases[i]['name'] as String).trim())) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||||
|
@ -59,7 +59,7 @@ class GitLab extends AppSource {
|
|||||||
}
|
}
|
||||||
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl));
|
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
lib/app_sources/html.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class HTML extends AppSource {
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
var uri = Uri.parse(standardUrl);
|
||||||
|
Response res = await get(uri);
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
List<String> links = parse(res.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.map((element) => element.attributes['href'] ?? '')
|
||||||
|
.where((element) => element.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
|
||||||
|
if (links.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
var rel = links.last;
|
||||||
|
var apkName = rel.split('/').last;
|
||||||
|
var version = apkName.substring(0, apkName.length - 4);
|
||||||
|
List<String> apkUrls = [rel]
|
||||||
|
.map((e) => e.toLowerCase().startsWith('http://') ||
|
||||||
|
e.toLowerCase().startsWith('https://')
|
||||||
|
? e
|
||||||
|
: '${uri.origin}/$e')
|
||||||
|
.toList();
|
||||||
|
return APKDetails(version, apkUrls, AppNames(uri.host, tr('app')));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -43,7 +43,7 @@ class Mullvad extends AppSource {
|
|||||||
['https://mullvad.net/download/app/apk/latest'],
|
['https://mullvad.net/download/app/apk/latest'],
|
||||||
AppNames(name, 'Mullvad-VPN'));
|
AppNames(name, 'Mullvad-VPN'));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ class Signal extends AppSource {
|
|||||||
}
|
}
|
||||||
return APKDetails(version, apkUrls, AppNames(name, 'Signal'));
|
return APKDetails(version, apkUrls, AppNames(name, 'Signal'));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ class SourceForge extends AppSource {
|
|||||||
AppNames(
|
AppNames(
|
||||||
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
|
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ class SteamMobile extends AppSource {
|
|||||||
var apkUrls = [links[0]];
|
var apkUrls = [links[0]];
|
||||||
return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!));
|
return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!));
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
|
|
||||||
abstract class GeneratedFormItem {
|
abstract class GeneratedFormItem {
|
||||||
late String key;
|
late String key;
|
||||||
@ -20,6 +23,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
late bool required;
|
late bool required;
|
||||||
late int max;
|
late int max;
|
||||||
late String? hint;
|
late String? hint;
|
||||||
|
late bool password;
|
||||||
|
|
||||||
GeneratedFormTextField(String key,
|
GeneratedFormTextField(String key,
|
||||||
{String label = 'Input',
|
{String label = 'Input',
|
||||||
@ -28,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
List<String? Function(String? value)> additionalValidators = const [],
|
List<String? Function(String? value)> additionalValidators = const [],
|
||||||
this.required = true,
|
this.required = true,
|
||||||
this.max = 1,
|
this.max = 1,
|
||||||
this.hint})
|
this.hint,
|
||||||
|
this.password = false})
|
||||||
: super(key,
|
: super(key,
|
||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
@ -82,6 +87,35 @@ class GeneratedFormSwitch extends GeneratedFormItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GeneratedFormTagInput extends GeneratedFormItem {
|
||||||
|
late MapEntry<String, String>? deleteConfirmationMessage;
|
||||||
|
late bool singleSelect;
|
||||||
|
late WrapAlignment alignment;
|
||||||
|
late String emptyMessage;
|
||||||
|
late bool showLabelWhenNotEmpty;
|
||||||
|
GeneratedFormTagInput(String key,
|
||||||
|
{String label = 'Input',
|
||||||
|
List<Widget> belowWidgets = const [],
|
||||||
|
Map<String, MapEntry<int, bool>> defaultValue = const {},
|
||||||
|
List<String? Function(Map<String, MapEntry<int, bool>> value)>
|
||||||
|
additionalValidators = const [],
|
||||||
|
this.deleteConfirmationMessage,
|
||||||
|
this.singleSelect = false,
|
||||||
|
this.alignment = WrapAlignment.start,
|
||||||
|
this.emptyMessage = 'Input',
|
||||||
|
this.showLabelWhenNotEmpty = true})
|
||||||
|
: super(key,
|
||||||
|
label: label,
|
||||||
|
belowWidgets: belowWidgets,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
additionalValidators: additionalValidators);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, MapEntry<int, bool>> ensureType(val) {
|
||||||
|
return val is Map<String, MapEntry<int, bool>> ? val : {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typedef OnValueChanges = void Function(
|
typedef OnValueChanges = void Function(
|
||||||
Map<String, dynamic> values, bool valid, bool isBuilding);
|
Map<String, dynamic> values, bool valid, bool isBuilding);
|
||||||
|
|
||||||
@ -96,11 +130,27 @@ class GeneratedForm extends StatefulWidget {
|
|||||||
State<GeneratedForm> createState() => _GeneratedFormState();
|
State<GeneratedForm> createState() => _GeneratedFormState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generates a random light color
|
||||||
|
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
|
||||||
|
Color generateRandomLightColor() {
|
||||||
|
// Create a random number generator
|
||||||
|
final Random random = Random();
|
||||||
|
|
||||||
|
// Generate random hue, saturation, and value values
|
||||||
|
final double hue = random.nextDouble() * 360;
|
||||||
|
final double saturation = 0.5 + random.nextDouble() * 0.5;
|
||||||
|
final double value = 0.9 + random.nextDouble() * 0.1;
|
||||||
|
|
||||||
|
// Create a HSV color with the random values
|
||||||
|
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
|
||||||
|
}
|
||||||
|
|
||||||
class _GeneratedFormState extends State<GeneratedForm> {
|
class _GeneratedFormState extends State<GeneratedForm> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
Map<String, dynamic> values = {};
|
Map<String, dynamic> values = {};
|
||||||
late List<List<Widget>> formInputs;
|
late List<List<Widget>> formInputs;
|
||||||
List<List<Widget>> rows = [];
|
List<List<Widget>> rows = [];
|
||||||
|
String? initKey;
|
||||||
|
|
||||||
// If any value changes, call this to update the parent with value and validity
|
// If any value changes, call this to update the parent with value and validity
|
||||||
void someValueChanged({bool isBuilding = false}) {
|
void someValueChanged({bool isBuilding = false}) {
|
||||||
@ -109,24 +159,21 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
for (int r = 0; r < widget.items.length; r++) {
|
for (int r = 0; r < widget.items.length; r++) {
|
||||||
for (int i = 0; i < widget.items[r].length; i++) {
|
for (int i = 0; i < widget.items[r].length; i++) {
|
||||||
if (formInputs[r][i] is TextFormField) {
|
if (formInputs[r][i] is TextFormField) {
|
||||||
valid = valid &&
|
var fieldState =
|
||||||
((formInputs[r][i].key as GlobalKey<FormFieldState>)
|
(formInputs[r][i].key as GlobalKey<FormFieldState>).currentState;
|
||||||
.currentState
|
if (fieldState != null) {
|
||||||
?.isValid ??
|
valid = valid && fieldState.isValid;
|
||||||
false);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
widget.onValueChanges(returnValues, valid, isBuilding);
|
widget.onValueChanges(returnValues, valid, isBuilding);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
initForm() {
|
||||||
void initState() {
|
initKey = widget.key.toString();
|
||||||
super.initState();
|
|
||||||
|
|
||||||
// Initialize form values as all empty
|
// Initialize form values as all empty
|
||||||
values.clear();
|
values.clear();
|
||||||
int j = 0;
|
|
||||||
for (var row in widget.items) {
|
for (var row in widget.items) {
|
||||||
for (var e in row) {
|
for (var e in row) {
|
||||||
values[e.key] = e.defaultValue;
|
values[e.key] = e.defaultValue;
|
||||||
@ -140,6 +187,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
if (formItem is GeneratedFormTextField) {
|
if (formItem is GeneratedFormTextField) {
|
||||||
final formFieldKey = GlobalKey<FormFieldState>();
|
final formFieldKey = GlobalKey<FormFieldState>();
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
|
obscureText: formItem.password,
|
||||||
|
autocorrect: !formItem.password,
|
||||||
|
enableSuggestions: !formItem.password,
|
||||||
key: formFieldKey,
|
key: formFieldKey,
|
||||||
initialValue: values[formItem.key],
|
initialValue: values[formItem.key],
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
@ -193,8 +243,17 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
someValueChanged(isBuilding: true);
|
someValueChanged(isBuilding: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
initForm();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.key.toString() != initKey) {
|
||||||
|
initForm();
|
||||||
|
}
|
||||||
for (var r = 0; r < formInputs.length; r++) {
|
for (var r = 0; r < formInputs.length; r++) {
|
||||||
for (var e = 0; e < formInputs[r].length; e++) {
|
for (var e = 0; e < formInputs[r].length; e++) {
|
||||||
if (widget.items[r][e] is GeneratedFormSwitch) {
|
if (widget.items[r][e] is GeneratedFormSwitch) {
|
||||||
@ -212,6 +271,186 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
} else if (widget.items[r][e] is GeneratedFormTagInput) {
|
||||||
|
formInputs[r][e] =
|
||||||
|
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||||
|
if ((values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.isNotEmpty ==
|
||||||
|
true &&
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.showLabelWhenNotEmpty)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput).alignment ==
|
||||||
|
WrapAlignment.center
|
||||||
|
? CrossAxisAlignment.center
|
||||||
|
: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(widget.items[r][e].label),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
alignment:
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput).alignment,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
(values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.isEmpty ==
|
||||||
|
true
|
||||||
|
? Text(
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.emptyMessage,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
...(values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.entries
|
||||||
|
.map((e2) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: ChoiceChip(
|
||||||
|
label: Text(e2.key),
|
||||||
|
backgroundColor: Color(e2.value.key).withAlpha(50),
|
||||||
|
selectedColor: Color(e2.value.key),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
selected: e2.value.value,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
(values[widget.items[r][e].key] as Map<String,
|
||||||
|
MapEntry<int, bool>>)[e2.key] =
|
||||||
|
MapEntry(
|
||||||
|
(values[widget.items[r][e].key] as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>>)[e2.key]!
|
||||||
|
.key,
|
||||||
|
value);
|
||||||
|
if ((widget.items[r][e]
|
||||||
|
as GeneratedFormTagInput)
|
||||||
|
.singleSelect &&
|
||||||
|
value == true) {
|
||||||
|
for (var key in (values[
|
||||||
|
widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>)
|
||||||
|
.keys) {
|
||||||
|
if (key != e2.key) {
|
||||||
|
(values[widget.items[r][e].key] as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>>)[key] =
|
||||||
|
MapEntry(
|
||||||
|
(values[widget.items[r][e].key]
|
||||||
|
as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int,
|
||||||
|
bool>>)[key]!
|
||||||
|
.key,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
someValueChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}) ??
|
||||||
|
[const SizedBox.shrink()],
|
||||||
|
(values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.values
|
||||||
|
.where((e) => e.value)
|
||||||
|
.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
fn() {
|
||||||
|
setState(() {
|
||||||
|
var temp = values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>;
|
||||||
|
temp.removeWhere((key, value) => value.value);
|
||||||
|
values[widget.items[r][e].key] = temp;
|
||||||
|
someValueChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.deleteConfirmationMessage !=
|
||||||
|
null) {
|
||||||
|
var message =
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.deleteConfirmationMessage!;
|
||||||
|
showDialog<Map<String, dynamic>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: message.key,
|
||||||
|
message: message.value,
|
||||||
|
items: const []);
|
||||||
|
}).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tooltip: tr('remove'),
|
||||||
|
))
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog<Map<String, dynamic>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: widget.items[r][e].label,
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('label',
|
||||||
|
label: tr('label'))
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}).then((value) {
|
||||||
|
String? label = value?['label'];
|
||||||
|
if (label != null) {
|
||||||
|
setState(() {
|
||||||
|
var temp = values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?;
|
||||||
|
temp ??= {};
|
||||||
|
if (temp[label] == null) {
|
||||||
|
var singleSelect = (widget.items[r][e]
|
||||||
|
as GeneratedFormTagInput)
|
||||||
|
.singleSelect;
|
||||||
|
var someSelected = temp.entries
|
||||||
|
.where((element) => element.value.value)
|
||||||
|
.isNotEmpty;
|
||||||
|
temp[label] = MapEntry(
|
||||||
|
generateRandomLightColor().value,
|
||||||
|
!(someSelected && singleSelect));
|
||||||
|
values[widget.items[r][e].key] = temp;
|
||||||
|
someValueChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tooltip: tr('add'),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,16 @@ class GeneratedFormModal extends StatefulWidget {
|
|||||||
required this.title,
|
required this.title,
|
||||||
required this.items,
|
required this.items,
|
||||||
this.initValid = false,
|
this.initValid = false,
|
||||||
this.message = ''});
|
this.message = '',
|
||||||
|
this.additionalWidgets = const [],
|
||||||
|
this.singleNullReturnButton});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String message;
|
final String message;
|
||||||
final List<List<GeneratedFormItem>> items;
|
final List<List<GeneratedFormItem>> items;
|
||||||
final bool initValid;
|
final bool initValid;
|
||||||
|
final List<Widget> additionalWidgets;
|
||||||
|
final String? singleNullReturnButton;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
||||||
@ -54,24 +58,29 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
this.valid = valid;
|
this.valid = valid;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: Text(tr('cancel'))),
|
child: Text(widget.singleNullReturnButton == null
|
||||||
TextButton(
|
? tr('cancel')
|
||||||
onPressed: !valid
|
: widget.singleNullReturnButton!)),
|
||||||
? null
|
widget.singleNullReturnButton == null
|
||||||
: () {
|
? TextButton(
|
||||||
if (valid) {
|
onPressed: !valid
|
||||||
HapticFeedback.selectionClick();
|
? null
|
||||||
Navigator.of(context).pop(values);
|
: () {
|
||||||
}
|
if (valid) {
|
||||||
},
|
HapticFeedback.selectionClick();
|
||||||
child: Text(tr('continue')))
|
Navigator.of(context).pop(values);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(tr('continue')))
|
||||||
|
: const SizedBox.shrink()
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,10 @@ class ObtainiumError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RateLimitError {
|
class RateLimitError extends ObtainiumError {
|
||||||
late int remainingMinutes;
|
late int remainingMinutes;
|
||||||
RateLimitError(this.remainingMinutes);
|
RateLimitError(this.remainingMinutes)
|
||||||
|
: super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes));
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
plural('tooManyRequestsTryAgainInMinutes', remainingMinutes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvalidURLError extends ObtainiumError {
|
class InvalidURLError extends ObtainiumError {
|
||||||
@ -32,7 +29,7 @@ class NoReleasesError extends ObtainiumError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NoAPKError extends ObtainiumError {
|
class NoAPKError extends ObtainiumError {
|
||||||
NoAPKError() : super(tr('noReleaseFound'));
|
NoAPKError() : super(tr('noAPKFound'));
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoVersionError extends ObtainiumError {
|
class NoVersionError extends ObtainiumError {
|
||||||
|
@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
|
|||||||
// ignore: implementation_imports
|
// ignore: implementation_imports
|
||||||
import 'package:easy_localization/src/localization.dart';
|
import 'package:easy_localization/src/localization.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.9.2';
|
const String currentVersion = '0.10.9';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
@ -43,12 +43,16 @@ final globalNavigatorKey = GlobalKey<NavigatorState>();
|
|||||||
Future<void> loadTranslations() async {
|
Future<void> loadTranslations() async {
|
||||||
// See easy_localization/issues/210
|
// See easy_localization/issues/210
|
||||||
await EasyLocalizationController.initEasyLocation();
|
await EasyLocalizationController.initEasyLocation();
|
||||||
|
var s = SettingsProvider();
|
||||||
|
await s.initializeSettings();
|
||||||
|
var forceLocale = s.forcedLocale;
|
||||||
final controller = EasyLocalizationController(
|
final controller = EasyLocalizationController(
|
||||||
saveLocale: true,
|
saveLocale: true,
|
||||||
|
forceLocale: forceLocale != null ? Locale(forceLocale) : null,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
supportedLocales: supportedLocales,
|
supportedLocales: supportedLocales,
|
||||||
assetLoader: const RootBundleAssetLoader(),
|
assetLoader: const RootBundleAssetLoader(),
|
||||||
useOnlyLangCode: false,
|
useOnlyLangCode: true,
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
path: localeDir,
|
path: localeDir,
|
||||||
onLoadError: (FlutterError e) {
|
onLoadError: (FlutterError e) {
|
||||||
@ -160,6 +164,7 @@ void main() async {
|
|||||||
supportedLocales: supportedLocales,
|
supportedLocales: supportedLocales,
|
||||||
path: localeDir,
|
path: localeDir,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
|
useOnlyLangCode: true,
|
||||||
child: const Obtainium()),
|
child: const Obtainium()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:obtainium/custom_errors.dart';
|
|||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
import 'package:obtainium/pages/app.dart';
|
import 'package:obtainium/pages/app.dart';
|
||||||
import 'package:obtainium/pages/import_export.dart';
|
import 'package:obtainium/pages/import_export.dart';
|
||||||
|
import 'package:obtainium/pages/settings.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -23,39 +24,42 @@ class AddAppPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _AddAppPageState extends State<AddAppPage> {
|
class _AddAppPageState extends State<AddAppPage> {
|
||||||
bool gettingAppInfo = false;
|
bool gettingAppInfo = false;
|
||||||
|
bool searching = false;
|
||||||
|
|
||||||
String userInput = '';
|
String userInput = '';
|
||||||
String searchQuery = '';
|
String searchQuery = '';
|
||||||
AppSource? pickedSource;
|
AppSource? pickedSource;
|
||||||
Map<String, dynamic> additionalSettings = {};
|
Map<String, dynamic> additionalSettings = {};
|
||||||
bool additionalSettingsValid = true;
|
bool additionalSettingsValid = true;
|
||||||
|
List<String> pickedCategories = [];
|
||||||
|
int searchnum = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
|
|
||||||
changeUserInput(String input, bool valid, bool isBuilding) {
|
bool doingSomething = gettingAppInfo || searching;
|
||||||
userInput = input;
|
|
||||||
fn() {
|
|
||||||
var source = valid ? sourceProvider.getSource(userInput) : null;
|
|
||||||
if (pickedSource.runtimeType != source.runtimeType) {
|
|
||||||
pickedSource = source;
|
|
||||||
additionalSettings = source != null
|
|
||||||
? getDefaultValuesFromFormItems(
|
|
||||||
source.combinedAppSpecificSettingFormItems)
|
|
||||||
: {};
|
|
||||||
additionalSettingsValid = source != null
|
|
||||||
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
|
||||||
: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBuilding) {
|
changeUserInput(String input, bool valid, bool isBuilding,
|
||||||
fn();
|
{bool isSearch = false}) {
|
||||||
} else {
|
userInput = input;
|
||||||
|
if (!isBuilding) {
|
||||||
setState(() {
|
setState(() {
|
||||||
fn();
|
if (isSearch) {
|
||||||
|
searchnum++;
|
||||||
|
}
|
||||||
|
var source = valid ? sourceProvider.getSource(userInput) : null;
|
||||||
|
if (pickedSource.runtimeType != source.runtimeType) {
|
||||||
|
pickedSource = source;
|
||||||
|
additionalSettings = source != null
|
||||||
|
? getDefaultValuesFromFormItems(
|
||||||
|
source.combinedAppSpecificSettingFormItems)
|
||||||
|
: {};
|
||||||
|
additionalSettingsValid = source != null
|
||||||
|
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
||||||
|
: true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,6 +75,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
additionalSettings['noVersionDetection'] == true;
|
additionalSettings['noVersionDetection'] == true;
|
||||||
var cont = true;
|
var cont = true;
|
||||||
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -89,6 +94,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
cont = false;
|
cont = false;
|
||||||
}
|
}
|
||||||
if (userPickedNoVersionDetection &&
|
if (userPickedNoVersionDetection &&
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -131,6 +137,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
if (app.additionalSettings['trackOnly'] == true) {
|
if (app.additionalSettings['trackOnly'] == true) {
|
||||||
app.installedVersion = app.latestVersion;
|
app.installedVersion = app.latestVersion;
|
||||||
}
|
}
|
||||||
|
app.categories = pickedCategories;
|
||||||
await appsProvider.saveApps([app]);
|
await appsProvider.saveApps([app]);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@ -167,30 +174,32 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GeneratedForm(
|
child: GeneratedForm(
|
||||||
|
key: Key(searchnum.toString()),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('appSourceURL',
|
GeneratedFormTextField('appSourceURL',
|
||||||
label: tr('appSourceURL'),
|
label: tr('appSourceURL'),
|
||||||
additionalValidators: [
|
defaultValue: userInput,
|
||||||
(value) {
|
additionalValidators: [
|
||||||
try {
|
(value) {
|
||||||
sourceProvider
|
try {
|
||||||
.getSource(value ?? '')
|
sourceProvider
|
||||||
.standardizeURL(
|
.getSource(value ?? '')
|
||||||
preStandardizeUrl(
|
.standardizeURL(
|
||||||
value ?? ''));
|
preStandardizeUrl(
|
||||||
} catch (e) {
|
value ?? ''));
|
||||||
return e is String
|
} catch (e) {
|
||||||
? e
|
return e is String
|
||||||
: e is ObtainiumError
|
? e
|
||||||
? e.toString()
|
: e is ObtainiumError
|
||||||
: tr('error');
|
? e.toString()
|
||||||
}
|
: tr('error');
|
||||||
return null;
|
}
|
||||||
}
|
return null;
|
||||||
])
|
}
|
||||||
]
|
])
|
||||||
],
|
]
|
||||||
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
changeUserInput(values['appSourceURL']!,
|
changeUserInput(values['appSourceURL']!,
|
||||||
valid, isBuilding);
|
valid, isBuilding);
|
||||||
@ -201,7 +210,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
gettingAppInfo
|
gettingAppInfo
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
onPressed: gettingAppInfo ||
|
onPressed: doingSomething ||
|
||||||
pickedSource == null ||
|
pickedSource == null ||
|
||||||
(pickedSource!
|
(pickedSource!
|
||||||
.combinedAppSpecificSettingFormItems
|
.combinedAppSpecificSettingFormItems
|
||||||
@ -238,7 +247,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (values.isNotEmpty && valid) {
|
if (values.isNotEmpty &&
|
||||||
|
valid &&
|
||||||
|
!isBuilding) {
|
||||||
setState(() {
|
setState(() {
|
||||||
searchQuery =
|
searchQuery =
|
||||||
values['searchSomeSources']!.trim();
|
values['searchSomeSources']!.trim();
|
||||||
@ -250,9 +261,12 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
width: 16,
|
width: 16,
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: searchQuery.isEmpty || gettingAppInfo
|
onPressed: searchQuery.isEmpty || doingSomething
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
|
setState(() {
|
||||||
|
searching = true;
|
||||||
|
});
|
||||||
Future.wait(sourceProvider.sources
|
Future.wait(sourceProvider.sources
|
||||||
.where((e) => e.canSearch)
|
.where((e) => e.canSearch)
|
||||||
.map((e) =>
|
.map((e) =>
|
||||||
@ -289,19 +303,21 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
if (selectedUrls != null &&
|
if (selectedUrls != null &&
|
||||||
selectedUrls.isNotEmpty) {
|
selectedUrls.isNotEmpty) {
|
||||||
changeUserInput(
|
changeUserInput(
|
||||||
selectedUrls[0], true, true);
|
selectedUrls[0], true, false,
|
||||||
addApp(resetUserInputAfter: true);
|
isSearch: true);
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
searching = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text(tr('search')))
|
child: Text(tr('search')))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (pickedSource != null &&
|
if (pickedSource != null)
|
||||||
(pickedSource!
|
|
||||||
.combinedAppSpecificSettingFormItems.isNotEmpty))
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -318,6 +334,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
|
key: Key(pickedSource.runtimeType.toString()),
|
||||||
items: pickedSource!
|
items: pickedSource!
|
||||||
.combinedAppSpecificSettingFormItems,
|
.combinedAppSpecificSettingFormItems,
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
@ -328,6 +345,18 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
CategoryEditorSelector(
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
onSelected: (categories) {
|
||||||
|
pickedCategories = categories;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
|
import 'package:obtainium/pages/settings.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -34,7 +34,6 @@ class _AppPageState extends State<AppPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var categories = settingsProvider.categories;
|
|
||||||
var sourceProvider = SourceProvider();
|
var sourceProvider = SourceProvider();
|
||||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
||||||
@ -43,6 +42,108 @@ class _AppPageState extends State<AppPage> {
|
|||||||
getUpdate(app.app.id);
|
getUpdate(app.app.id);
|
||||||
}
|
}
|
||||||
var trackOnly = app?.app.additionalSettings['trackOnly'] == true;
|
var trackOnly = app?.app.additionalSettings['trackOnly'] == true;
|
||||||
|
var noVersionDetection =
|
||||||
|
app?.app.additionalSettings['noVersionDetection'] == true;
|
||||||
|
|
||||||
|
var infoColumn = Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (app?.app.url != null) {
|
||||||
|
launchUrlString(app?.app.url ?? '',
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
app?.app.url ?? '',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 12),
|
||||||
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tr('latestVersionX', args: [app?.app.latestVersion ?? tr('unknown')]),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${tr('installedVersionX', args: [
|
||||||
|
app?.app.installedVersion ?? tr('none')
|
||||||
|
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
||||||
|
tr('app')
|
||||||
|
])}' : ''}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tr('lastUpdateCheckX', args: [
|
||||||
|
app?.app.lastUpdateCheck == null
|
||||||
|
? tr('never')
|
||||||
|
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
|
||||||
|
]),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
CategoryEditorSelector(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
preselected:
|
||||||
|
app?.app.categories != null ? app!.app.categories.toSet() : {},
|
||||||
|
onSelected: (categories) {
|
||||||
|
if (app != null) {
|
||||||
|
app.app.categories = categories;
|
||||||
|
appsProvider.saveApps([app.app]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
var fullInfoColumn = Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 150),
|
||||||
|
app?.installedInfo != null
|
||||||
|
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Image.memory(
|
||||||
|
app!.installedInfo!.icon!,
|
||||||
|
height: 150,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
)
|
||||||
|
])
|
||||||
|
: Container(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
app?.installedInfo?.name ?? app?.app.name ?? tr('app'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.displayLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tr('byX', args: [app?.app.author ?? tr('unknown')]),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
infoColumn,
|
||||||
|
const SizedBox(height: 150)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@ -71,132 +172,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
: Container()
|
: Container()
|
||||||
: CustomScrollView(
|
: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverFillRemaining(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(children: [fullInfoColumn])),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
app?.installedInfo != null
|
|
||||||
? Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Image.memory(
|
|
||||||
app!.installedInfo!.icon!,
|
|
||||||
height: 150,
|
|
||||||
gaplessPlayback: true,
|
|
||||||
)
|
|
||||||
])
|
|
||||||
: Container(),
|
|
||||||
const SizedBox(
|
|
||||||
height: 25,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
app?.installedInfo?.name ??
|
|
||||||
app?.app.name ??
|
|
||||||
tr('app'),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.displayLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
tr('byX', args: [app?.app.author ?? tr('unknown')]),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
if (app?.app.url != null) {
|
|
||||||
launchUrlString(app?.app.url ?? '',
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
app?.app.url ?? '',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
fontSize: 12),
|
|
||||||
)),
|
|
||||||
const SizedBox(
|
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
tr('latestVersionX',
|
|
||||||
args: [app?.app.latestVersion ?? tr('unknown')]),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${tr('installedVersionX', args: [
|
|
||||||
app?.app.installedVersion ?? tr('none')
|
|
||||||
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
|
||||||
tr('app')
|
|
||||||
])}' : ''}',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
tr('lastUpdateCheckX', args: [
|
|
||||||
app?.app.lastUpdateCheck == null
|
|
||||||
? tr('never')
|
|
||||||
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
|
|
||||||
]),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontStyle: FontStyle.italic, fontSize: 12),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
app?.app.category != null
|
|
||||||
? Chip(
|
|
||||||
label: Text(app!.app.category!),
|
|
||||||
backgroundColor:
|
|
||||||
Color(categories[app.app.category!] ?? 0x0),
|
|
||||||
onDeleted: () {
|
|
||||||
app.app.category = null;
|
|
||||||
appsProvider.saveApps([app.app]);
|
|
||||||
},
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
showDialog<Map<String, dynamic>?>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return GeneratedFormModal(
|
|
||||||
title: 'Pick a Category',
|
|
||||||
items: [
|
|
||||||
[
|
|
||||||
settingsProvider
|
|
||||||
.getCategoryFormItem()
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}).then((value) {
|
|
||||||
if (value != null && app != null) {
|
|
||||||
String? cat = (value['category']
|
|
||||||
?.isNotEmpty ??
|
|
||||||
false)
|
|
||||||
? value['category']
|
|
||||||
: null;
|
|
||||||
app.app.category = cat;
|
|
||||||
appsProvider.saveApps([app.app]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(tr('categorize')))
|
|
||||||
])
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
@ -215,8 +192,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
if (app?.app.installedVersion != null &&
|
if (noVersionDetection &&
|
||||||
!trackOnly &&
|
!trackOnly &&
|
||||||
|
app?.app.installedVersion != null &&
|
||||||
app?.app.installedVersion != app?.app.latestVersion)
|
app?.app.installedVersion != app?.app.latestVersion)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
@ -228,13 +206,6 @@ class _AppPageState extends State<AppPage> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(tr(
|
title: Text(tr(
|
||||||
'alreadyUpToDateQuestion')),
|
'alreadyUpToDateQuestion')),
|
||||||
content: Text(
|
|
||||||
tr('onlyWorksWithNonEVDApps'),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight:
|
|
||||||
FontWeight.bold,
|
|
||||||
fontStyle:
|
|
||||||
FontStyle.italic)),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -292,8 +263,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
return row;
|
return row;
|
||||||
}).toList();
|
}).toList();
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('additionalOptions'),
|
title: tr('additionalOptions'),
|
||||||
items: items);
|
items: items,
|
||||||
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (app != null && values != null) {
|
if (app != null && values != null) {
|
||||||
var changedApp = app.app;
|
var changedApp = app.app;
|
||||||
@ -314,10 +286,43 @@ class _AppPageState extends State<AppPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip: tr('additionalOptions'),
|
tooltip: tr('additionalOptions'),
|
||||||
icon: const Icon(Icons.settings)),
|
icon: const Icon(Icons.edit)),
|
||||||
|
if (app != null && app.installedInfo != null)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
appsProvider.openAppSettings(app.app.id);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
tooltip: tr('settings'),
|
||||||
|
),
|
||||||
|
if (app != null && settingsProvider.showAppWebpage)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
content: infoColumn,
|
||||||
|
title: Text(
|
||||||
|
'${app.app.name} ${tr('byX', args: [
|
||||||
|
app.app.author
|
||||||
|
])}'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(tr('continue')))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
tooltip: tr('more')),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: TextButton(
|
||||||
onPressed: (app?.app.installedVersion == null ||
|
onPressed: (app?.app.installedVersion == null ||
|
||||||
app?.app.installedVersion !=
|
app?.app.installedVersion !=
|
||||||
app?.app.latestVersion) &&
|
app?.app.latestVersion) &&
|
||||||
@ -342,6 +347,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
if (res.isNotEmpty && mounted) {
|
if (res.isNotEmpty && mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
@ -356,44 +363,17 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? tr('update')
|
? tr('update')
|
||||||
: tr('markUpdated')))),
|
: tr('markUpdated')))),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
ElevatedButton(
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog(
|
appsProvider.removeAppsWithModal(
|
||||||
context: context,
|
context, [app!.app]).then((value) {
|
||||||
builder: (BuildContext ctx) {
|
if (value == true) {
|
||||||
return AlertDialog(
|
Navigator.of(context).pop();
|
||||||
title: Text(tr('removeAppQuestion')),
|
}
|
||||||
content: Text(tr(
|
});
|
||||||
'xWillBeRemovedButRemainInstalled',
|
|
||||||
args: [
|
|
||||||
app?.installedInfo?.name ??
|
|
||||||
app?.app.name ??
|
|
||||||
tr('app')
|
|
||||||
])),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
HapticFeedback
|
|
||||||
.selectionClick();
|
|
||||||
appsProvider.removeApps(
|
|
||||||
[app!.app.id]).then((_) {
|
|
||||||
int count = 0;
|
|
||||||
Navigator.of(context)
|
|
||||||
.popUntil((_) =>
|
|
||||||
count++ >= 2);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(tr('remove'))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: Text(tr('cancel')))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor:
|
foregroundColor:
|
||||||
@ -401,7 +381,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
surfaceTintColor:
|
surfaceTintColor:
|
||||||
Theme.of(context).colorScheme.error),
|
Theme.of(context).colorScheme.error),
|
||||||
child: Text(tr('remove')),
|
child: Text(tr('remove')),
|
||||||
),
|
)),
|
||||||
])),
|
])),
|
||||||
if (app?.downloadProgress != null)
|
if (app?.downloadProgress != null)
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -63,21 +63,29 @@ class _HomePageState extends State<HomePage> {
|
|||||||
.map((e) =>
|
.map((e) =>
|
||||||
NavigationDestination(icon: Icon(e.icon), label: e.title))
|
NavigationDestination(icon: Icon(e.icon), label: e.title))
|
||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: (int index) {
|
onDestinationSelected: (int index) async {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
setState(() {
|
if (index == 0) {
|
||||||
if (index == 0) {
|
while ((pages[0].widget.key as GlobalKey<AppsPageState>)
|
||||||
|
.currentState !=
|
||||||
|
null) {
|
||||||
|
// Avoid duplicate GlobalKey error
|
||||||
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
selectedIndexHistory.clear();
|
selectedIndexHistory.clear();
|
||||||
} else if (selectedIndexHistory.isEmpty ||
|
});
|
||||||
(selectedIndexHistory.isNotEmpty &&
|
} else if (selectedIndexHistory.isEmpty ||
|
||||||
selectedIndexHistory.last != index)) {
|
(selectedIndexHistory.isNotEmpty &&
|
||||||
|
selectedIndexHistory.last != index)) {
|
||||||
|
setState(() {
|
||||||
int existingInd = selectedIndexHistory.indexOf(index);
|
int existingInd = selectedIndexHistory.indexOf(index);
|
||||||
if (existingInd >= 0) {
|
if (existingInd >= 0) {
|
||||||
selectedIndexHistory.removeAt(existingInd);
|
selectedIndexHistory.removeAt(existingInd);
|
||||||
}
|
}
|
||||||
selectedIndexHistory.add(index);
|
selectedIndexHistory.add(index);
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
selectedIndex:
|
selectedIndex:
|
||||||
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||||
|
@ -9,6 +9,7 @@ import 'package:obtainium/components/generated_form.dart';
|
|||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
@ -28,6 +29,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
var appsProvider = context.read<AppsProvider>();
|
var appsProvider = context.read<AppsProvider>();
|
||||||
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
var outlineButtonStyle = ButtonStyle(
|
var outlineButtonStyle = ButtonStyle(
|
||||||
shape: MaterialStateProperty.all(
|
shape: MaterialStateProperty.all(
|
||||||
StadiumBorder(
|
StadiumBorder(
|
||||||
@ -66,6 +68,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
showError(
|
showError(
|
||||||
tr('exportedTo', args: [path]),
|
tr('exportedTo', args: [path]),
|
||||||
context);
|
context);
|
||||||
|
}).catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text(tr('obtainiumExport')))),
|
child: Text(tr('obtainiumExport')))),
|
||||||
@ -98,6 +102,21 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
appsProvider
|
appsProvider
|
||||||
.importApps(data)
|
.importApps(data)
|
||||||
.then((value) {
|
.then((value) {
|
||||||
|
var cats =
|
||||||
|
settingsProvider.categories;
|
||||||
|
appsProvider.apps
|
||||||
|
.forEach((key, value) {
|
||||||
|
for (var c
|
||||||
|
in value.app.categories) {
|
||||||
|
if (!cats.containsKey(c)) {
|
||||||
|
cats[c] =
|
||||||
|
generateRandomLightColor()
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
settingsProvider.categories =
|
||||||
|
cats;
|
||||||
showError(
|
showError(
|
||||||
tr('importedX', args: [
|
tr('importedX', args: [
|
||||||
plural('apps', value)
|
plural('apps', value)
|
||||||
@ -338,7 +357,9 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
() async {
|
() async {
|
||||||
var values = await showDialog(
|
var values = await showDialog<
|
||||||
|
Map<String,
|
||||||
|
dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(BuildContext ctx) {
|
(BuildContext ctx) {
|
||||||
@ -365,7 +386,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
var urlsWithDescriptions =
|
var urlsWithDescriptions =
|
||||||
await source
|
await source
|
||||||
.getUrlsWithDescriptions(
|
.getUrlsWithDescriptions(
|
||||||
values);
|
values.values
|
||||||
|
.map((e) =>
|
||||||
|
e.toString())
|
||||||
|
.toList());
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
await showDialog<
|
await showDialog<
|
||||||
List<String>?>(
|
List<String>?>(
|
||||||
@ -540,18 +564,22 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
||||||
|
select(bool? value) {
|
||||||
|
setState(() {
|
||||||
|
value ??= false;
|
||||||
|
if (value! && widget.onlyOneSelectionAllowed) {
|
||||||
|
selectOnlyOne(urlWithD.key);
|
||||||
|
} else {
|
||||||
|
urlWithDescriptionSelections[urlWithD] = value!;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Row(children: [
|
return Row(children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: urlWithDescriptionSelections[urlWithD],
|
value: urlWithDescriptionSelections[urlWithD],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
select(value);
|
||||||
value ??= false;
|
|
||||||
if (value! && widget.onlyOneSelectionAllowed) {
|
|
||||||
selectOnlyOne(urlWithD.key);
|
|
||||||
} else {
|
|
||||||
urlWithDescriptionSelections[urlWithD] = value!;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
@ -575,12 +603,17 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
const TextStyle(decoration: TextDecoration.underline),
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
)),
|
)),
|
||||||
Text(
|
GestureDetector(
|
||||||
urlWithD.value.length > 128
|
onTap: () {
|
||||||
? '${urlWithD.value.substring(0, 128)}...'
|
select(!(urlWithDescriptionSelections[urlWithD] ?? false));
|
||||||
: urlWithD.value,
|
},
|
||||||
style: const TextStyle(
|
child: Text(
|
||||||
fontStyle: FontStyle.italic, fontSize: 12),
|
urlWithD.value.length > 128
|
||||||
|
? '${urlWithD.value.substring(0, 128)}...'
|
||||||
|
: urlWithD.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
|
@ -4,9 +4,8 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/main.dart';
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -41,7 +40,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
|
||||||
if (settingsProvider.prefs == null) {
|
if (settingsProvider.prefs == null) {
|
||||||
settingsProvider.initializeSettings();
|
settingsProvider.initializeSettings();
|
||||||
}
|
}
|
||||||
@ -130,6 +128,28 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var localeDropdown = DropdownButtonFormField(
|
||||||
|
decoration: InputDecoration(labelText: tr('language')),
|
||||||
|
value: settingsProvider.forcedLocale,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: null,
|
||||||
|
child: Text(tr('followSystem')),
|
||||||
|
),
|
||||||
|
...supportedLocales.map((e) => DropdownMenuItem(
|
||||||
|
value: e.toLanguageTag(),
|
||||||
|
child: Text(e.toLanguageTag().toUpperCase()),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.forcedLocale = value;
|
||||||
|
if (value != null) {
|
||||||
|
context.setLocale(Locale(value));
|
||||||
|
} else {
|
||||||
|
context.resetLocale();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var intervalDropdown = DropdownButtonFormField(
|
var intervalDropdown = DropdownButtonFormField(
|
||||||
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
|
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
|
||||||
value: settingsProvider.updateInterval,
|
value: settingsProvider.updateInterval,
|
||||||
@ -163,7 +183,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
return [e];
|
return [e];
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (valid) {
|
if (valid && !isBuilding) {
|
||||||
values.forEach((key, value) {
|
values.forEach((key, value) {
|
||||||
settingsProvider.setSettingString(key, value);
|
settingsProvider.setSettingString(key, value);
|
||||||
});
|
});
|
||||||
@ -178,8 +198,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
);
|
);
|
||||||
|
|
||||||
var categories = settingsProvider.categories;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@ -213,6 +231,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
|
localeDropdown,
|
||||||
|
height16,
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -264,84 +284,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
Wrap(
|
const CategoryEditorSelector(
|
||||||
children: [
|
showLabelWhenNotEmpty: false,
|
||||||
...categories.entries.toList().map((e) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4),
|
|
||||||
child: Chip(
|
|
||||||
label: Text(e.key),
|
|
||||||
backgroundColor: Color(e.value),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
onDeleted: () {
|
|
||||||
showDialog<Map<String, dynamic>?>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return GeneratedFormModal(
|
|
||||||
title: tr(
|
|
||||||
'deleteCategoryQuestion'),
|
|
||||||
message: tr(
|
|
||||||
'categoryDeleteWarning',
|
|
||||||
args: [e.key]),
|
|
||||||
items: []);
|
|
||||||
}).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
setState(() {
|
|
||||||
categories.remove(e.key);
|
|
||||||
settingsProvider.categories =
|
|
||||||
categories;
|
|
||||||
});
|
|
||||||
appsProvider.saveApps(appsProvider
|
|
||||||
.apps.values
|
|
||||||
.where((element) =>
|
|
||||||
element.app.category ==
|
|
||||||
e.key)
|
|
||||||
.map((e) {
|
|
||||||
var a = e.app;
|
|
||||||
a.category = null;
|
|
||||||
return a;
|
|
||||||
}).toList());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
showDialog<Map<String, dynamic>?>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return GeneratedFormModal(
|
|
||||||
title: tr('addCategory'),
|
|
||||||
items: [
|
|
||||||
[
|
|
||||||
GeneratedFormTextField(
|
|
||||||
'label',
|
|
||||||
label: tr('label'))
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}).then((value) {
|
|
||||||
String? label = value?['label'];
|
|
||||||
if (label != null) {
|
|
||||||
setState(() {
|
|
||||||
categories[label] =
|
|
||||||
generateRandomLightColor()
|
|
||||||
.value;
|
|
||||||
settingsProvider.categories =
|
|
||||||
categories;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
tooltip: tr('add'),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
))),
|
))),
|
||||||
@ -457,3 +401,62 @@ class _LogsDialogState extends State<LogsDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CategoryEditorSelector extends StatefulWidget {
|
||||||
|
final void Function(List<String> categories)? onSelected;
|
||||||
|
final bool singleSelect;
|
||||||
|
final Set<String> preselected;
|
||||||
|
final WrapAlignment alignment;
|
||||||
|
final bool showLabelWhenNotEmpty;
|
||||||
|
const CategoryEditorSelector(
|
||||||
|
{super.key,
|
||||||
|
this.onSelected,
|
||||||
|
this.singleSelect = false,
|
||||||
|
this.preselected = const {},
|
||||||
|
this.alignment = WrapAlignment.start,
|
||||||
|
this.showLabelWhenNotEmpty = true});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CategoryEditorSelector> createState() => _CategoryEditorSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
|
||||||
|
Map<String, MapEntry<int, bool>> storedValues = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
|
storedValues = settingsProvider.categories.map((key, value) => MapEntry(
|
||||||
|
key,
|
||||||
|
MapEntry(value,
|
||||||
|
storedValues[key]?.value ?? widget.preselected.contains(key))));
|
||||||
|
return GeneratedForm(
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormTagInput('categories',
|
||||||
|
label: tr('categories'),
|
||||||
|
emptyMessage: tr('noCategories'),
|
||||||
|
defaultValue: storedValues,
|
||||||
|
alignment: widget.alignment,
|
||||||
|
deleteConfirmationMessage: MapEntry(
|
||||||
|
tr('deleteCategoriesQuestion'),
|
||||||
|
tr('categoryDeleteWarning')),
|
||||||
|
singleSelect: widget.singleSelect,
|
||||||
|
showLabelWhenNotEmpty: widget.showLabelWhenNotEmpty)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onValueChanges: ((values, valid, isBuilding) {
|
||||||
|
if (!isBuilding) {
|
||||||
|
storedValues =
|
||||||
|
values['categories'] as Map<String, MapEntry<int, bool>>;
|
||||||
|
settingsProvider.categories =
|
||||||
|
storedValues.map((key, value) => MapEntry(key, value.key));
|
||||||
|
if (widget.onSelected != null) {
|
||||||
|
widget.onSelected!(storedValues.keys
|
||||||
|
.where((k) => storedValues[k]!.value)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:android_intent_plus/flag.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -12,16 +13,20 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
||||||
import 'package:installed_apps/app_info.dart';
|
import 'package:installed_apps/app_info.dart';
|
||||||
import 'package:installed_apps/installed_apps.dart';
|
import 'package:installed_apps/installed_apps.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:package_archive_info/package_archive_info.dart';
|
import 'package:package_archive_info/package_archive_info.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
|
|
||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
@ -246,9 +251,10 @@ class AppsProvider with ChangeNotifier {
|
|||||||
!(await canDowngradeApps())) {
|
!(await canDowngradeApps())) {
|
||||||
throw DowngradeError();
|
throw DowngradeError();
|
||||||
}
|
}
|
||||||
if (appInfo == null ||
|
await InstallPlugin.installApk(file.file.path, obtainiumId);
|
||||||
int.parse(newInfo.buildNumber) > appInfo.versionCode!) {
|
if (file.appId == obtainiumId) {
|
||||||
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
|
// Obtainium prompt should be lowest
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
}
|
}
|
||||||
apps[file.appId]!.app.installedVersion =
|
apps[file.appId]!.app.installedVersion =
|
||||||
apps[file.appId]!.app.latestVersion;
|
apps[file.appId]!.app.latestVersion;
|
||||||
@ -257,6 +263,15 @@ class AppsProvider with ChangeNotifier {
|
|||||||
attemptToCorrectInstallStatus: false);
|
attemptToCorrectInstallStatus: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uninstallApp(String appId) async {
|
||||||
|
var intent = AndroidIntent(
|
||||||
|
action: 'android.intent.action.DELETE',
|
||||||
|
data: 'package:$appId',
|
||||||
|
flags: <int>[Flag.FLAG_ACTIVITY_NEW_TASK],
|
||||||
|
package: 'vnd.android.package-archive');
|
||||||
|
await intent.launch();
|
||||||
|
}
|
||||||
|
|
||||||
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
||||||
// If the App has more than one APK, the user should pick one (if context provided)
|
// If the App has more than one APK, the user should pick one (if context provided)
|
||||||
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
||||||
@ -264,6 +279,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
|
|
||||||
if (app.apkUrls.length > 1 && context != null) {
|
if (app.apkUrls.length > 1 && context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
apkUrl = await showDialog(
|
apkUrl = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -283,6 +299,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (apkUrl != null &&
|
if (apkUrl != null &&
|
||||||
getHost(apkUrl) != getHost(app.url) &&
|
getHost(apkUrl) != getHost(app.url) &&
|
||||||
context != null) {
|
context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
if (await showDialog(
|
if (await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -440,9 +457,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
if (!res) {
|
|
||||||
logs.add(tr('versionCorrectionDisabled'));
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -621,6 +635,57 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> removeAppsWithModal(BuildContext context, List<App> apps) async {
|
||||||
|
var showUninstallOption =
|
||||||
|
apps.where((a) => a.installedVersion != null).isNotEmpty;
|
||||||
|
var values = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: plural('removeAppQuestion', apps.length),
|
||||||
|
items: !showUninstallOption
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('rmAppEntry',
|
||||||
|
label: tr('removeFromObtainium'), defaultValue: true)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('uninstallApp',
|
||||||
|
label: tr('uninstallFromDevice'))
|
||||||
|
]
|
||||||
|
],
|
||||||
|
initValid: true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (values != null) {
|
||||||
|
bool uninstall = values['uninstallApp'] == true && showUninstallOption;
|
||||||
|
bool remove = values['rmAppEntry'] == true || !showUninstallOption;
|
||||||
|
if (uninstall) {
|
||||||
|
for (var i = 0; i < apps.length; i++) {
|
||||||
|
if (apps[i].installedVersion != null) {
|
||||||
|
uninstallApp(apps[i].id);
|
||||||
|
apps[i].installedVersion = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveApps(apps, attemptToCorrectInstallStatus: false);
|
||||||
|
}
|
||||||
|
if (remove) {
|
||||||
|
await removeApps(apps.map((e) => e.id).toList());
|
||||||
|
}
|
||||||
|
return uninstall || remove;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openAppSettings(String appId) async {
|
||||||
|
final AndroidIntent intent = AndroidIntent(
|
||||||
|
action: 'action_application_details_settings',
|
||||||
|
data: 'package:$appId',
|
||||||
|
);
|
||||||
|
await intent.launch();
|
||||||
|
}
|
||||||
|
|
||||||
Future<App?> checkUpdate(String appId) async {
|
Future<App?> checkUpdate(String appId) async {
|
||||||
App? currentApp = apps[appId]!.app;
|
App? currentApp = apps[appId]!.app;
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
@ -706,6 +771,14 @@ class AppsProvider with ChangeNotifier {
|
|||||||
exportDir = await getExternalStorageDirectory();
|
exportDir = await getExternalStorageDirectory();
|
||||||
path = exportDir!.path;
|
path = exportDir!.path;
|
||||||
}
|
}
|
||||||
|
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
|
||||||
|
if (await Permission.storage.isDenied) {
|
||||||
|
await Permission.storage.request();
|
||||||
|
}
|
||||||
|
if (await Permission.storage.isDenied) {
|
||||||
|
throw ObtainiumError(tr('storagePermissionDenied'));
|
||||||
|
}
|
||||||
|
}
|
||||||
File export = File(
|
File export = File(
|
||||||
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
|
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
|
||||||
export.writeAsStringSync(
|
export.writeAsStringSync(
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/main.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
@ -153,14 +154,29 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
set categories(Map<String, int> cats) {
|
set categories(Map<String, int> cats) {
|
||||||
prefs?.setString('categories', jsonEncode(cats));
|
prefs?.setString('categories', jsonEncode(cats));
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoryFormItem({String initCategory = ''}) => GeneratedFormDropdown(
|
String? get forcedLocale {
|
||||||
'category',
|
var fl = prefs?.getString('forcedLocale');
|
||||||
label: tr('category'),
|
return supportedLocales
|
||||||
[
|
.where((element) => element.toLanguageTag() == fl)
|
||||||
MapEntry('', tr('noCategory')),
|
.isNotEmpty
|
||||||
...categories.entries.map((e) => MapEntry(e.key, e.key)).toList()
|
? fl
|
||||||
],
|
: null;
|
||||||
defaultValue: initCategory);
|
}
|
||||||
|
|
||||||
|
set forcedLocale(String? fl) {
|
||||||
|
if (fl == null) {
|
||||||
|
prefs?.remove('forcedLocale');
|
||||||
|
} else if (supportedLocales
|
||||||
|
.where((element) => element.toLanguageTag() == fl)
|
||||||
|
.isNotEmpty) {
|
||||||
|
prefs?.setString('forcedLocale', fl);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setEqual(Set<String> a, Set<String> b) =>
|
||||||
|
a.length == b.length && a.union(b).length == a.length;
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,13 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||||
|
import 'package:obtainium/app_sources/codeberg.dart';
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/app_sources/gitlab.dart';
|
import 'package:obtainium/app_sources/gitlab.dart';
|
||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
import 'package:obtainium/app_sources/mullvad.dart';
|
import 'package:obtainium/app_sources/mullvad.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
@ -19,7 +21,6 @@ import 'package:obtainium/app_sources/steammobile.dart';
|
|||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
|
||||||
|
|
||||||
class AppNames {
|
class AppNames {
|
||||||
late String author;
|
late String author;
|
||||||
@ -48,7 +49,7 @@ class App {
|
|||||||
late Map<String, dynamic> additionalSettings;
|
late Map<String, dynamic> additionalSettings;
|
||||||
late DateTime? lastUpdateCheck;
|
late DateTime? lastUpdateCheck;
|
||||||
bool pinned = false;
|
bool pinned = false;
|
||||||
String? category;
|
List<String> categories;
|
||||||
App(
|
App(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
@ -61,7 +62,7 @@ class App {
|
|||||||
this.additionalSettings,
|
this.additionalSettings,
|
||||||
this.lastUpdateCheck,
|
this.lastUpdateCheck,
|
||||||
this.pinned,
|
this.pinned,
|
||||||
{this.category});
|
{this.categories = const []});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -103,6 +104,12 @@ class App {
|
|||||||
item.ensureType(additionalSettings[item.key]);
|
item.ensureType(additionalSettings[item.key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
int preferredApkIndex = json['preferredApkIndex'] == null
|
||||||
|
? 0
|
||||||
|
: json['preferredApkIndex'] as int;
|
||||||
|
if (preferredApkIndex < 0) {
|
||||||
|
preferredApkIndex = 0;
|
||||||
|
}
|
||||||
return App(
|
return App(
|
||||||
json['id'] as String,
|
json['id'] as String,
|
||||||
json['url'] as String,
|
json['url'] as String,
|
||||||
@ -115,15 +122,19 @@ class App {
|
|||||||
json['apkUrls'] == null
|
json['apkUrls'] == null
|
||||||
? []
|
? []
|
||||||
: List<String>.from(jsonDecode(json['apkUrls'])),
|
: List<String>.from(jsonDecode(json['apkUrls'])),
|
||||||
json['preferredApkIndex'] == null
|
preferredApkIndex,
|
||||||
? 0
|
|
||||||
: json['preferredApkIndex'] as int,
|
|
||||||
additionalSettings,
|
additionalSettings,
|
||||||
json['lastUpdateCheck'] == null
|
json['lastUpdateCheck'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||||
json['pinned'] ?? false,
|
json['pinned'] ?? false,
|
||||||
category: json['category']);
|
categories: json['categories'] != null
|
||||||
|
? (json['categories'] as List<dynamic>)
|
||||||
|
.map((e) => e.toString())
|
||||||
|
.toList()
|
||||||
|
: json['category'] != null
|
||||||
|
? [json['category'] as String]
|
||||||
|
: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@ -138,12 +149,16 @@ class App {
|
|||||||
'additionalSettings': jsonEncode(additionalSettings),
|
'additionalSettings': jsonEncode(additionalSettings),
|
||||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||||
'pinned': pinned,
|
'pinned': pinned,
|
||||||
'category': category
|
'categories': categories
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the input is starts with HTTPS and has no WWW
|
// Ensure the input is starts with HTTPS and has no WWW
|
||||||
preStandardizeUrl(String url) {
|
preStandardizeUrl(String url) {
|
||||||
|
var firstDotIndex = url.indexOf('.');
|
||||||
|
if (!(firstDotIndex >= 0 && firstDotIndex != url.length - 1)) {
|
||||||
|
throw UnsupportedURLError();
|
||||||
|
}
|
||||||
if (url.toLowerCase().indexOf('http://') != 0 &&
|
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||||
url.toLowerCase().indexOf('https://') != 0) {
|
url.toLowerCase().indexOf('https://') != 0) {
|
||||||
url = 'https://$url';
|
url = 'https://$url';
|
||||||
@ -210,7 +225,19 @@ class AppSource {
|
|||||||
label: tr('trackOnly'),
|
label: tr('trackOnly'),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))]
|
[
|
||||||
|
GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('apkFilterRegEx',
|
||||||
|
label: tr('filterAPKsByRegEx'),
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
return regExValidator(value);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
// Previous 2 variables combined into one at runtime for convenient usage
|
// Previous 2 variables combined into one at runtime for convenient usage
|
||||||
@ -254,11 +281,24 @@ abstract class MassAppUrlSource {
|
|||||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
regExValidator(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RegExp(value);
|
||||||
|
} catch (e) {
|
||||||
|
return tr('invalidRegEx');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
// Add more source classes here so they are available via the service
|
// Add more source classes here so they are available via the service
|
||||||
List<AppSource> sources = [
|
List<AppSource> sources = [
|
||||||
GitHub(),
|
GitHub(),
|
||||||
GitLab(),
|
GitLab(),
|
||||||
|
Codeberg(),
|
||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
IzzyOnDroid(),
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
@ -266,7 +306,8 @@ class SourceProvider {
|
|||||||
SourceForge(),
|
SourceForge(),
|
||||||
APKMirror(),
|
APKMirror(),
|
||||||
FDroidRepo(),
|
FDroidRepo(),
|
||||||
SteamMobile()
|
SteamMobile(),
|
||||||
|
HTML() // This should ALWAYS be the last option as they are tried in order
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add more mass url source classes here so they are available via the service
|
// Add more mass url source classes here so they are available via the service
|
||||||
@ -327,20 +368,28 @@ class SourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<App> getApp(
|
Future<App> getApp(
|
||||||
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
AppSource source,
|
||||||
{App? currentApp,
|
String url,
|
||||||
bool trackOnlyOverride = false,
|
Map<String, dynamic> additionalSettings, {
|
||||||
noVersionDetectionOverride = false}) async {
|
App? currentApp,
|
||||||
if (trackOnlyOverride) {
|
bool trackOnlyOverride = false,
|
||||||
|
noVersionDetectionOverride = false,
|
||||||
|
}) async {
|
||||||
|
if (trackOnlyOverride || source.enforceTrackOnly) {
|
||||||
additionalSettings['trackOnly'] = true;
|
additionalSettings['trackOnly'] = true;
|
||||||
}
|
}
|
||||||
if (noVersionDetectionOverride) {
|
if (noVersionDetectionOverride) {
|
||||||
additionalSettings['noVersionDetection'] = true;
|
additionalSettings['noVersionDetection'] = true;
|
||||||
}
|
}
|
||||||
var trackOnly = currentApp?.additionalSettings['trackOnly'] == true;
|
var trackOnly = additionalSettings['trackOnly'] == true;
|
||||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
||||||
|
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||||
|
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||||
|
apk.apkUrls =
|
||||||
|
apk.apkUrls.where((element) => reg.hasMatch(element)).toList();
|
||||||
|
}
|
||||||
if (apk.apkUrls.isEmpty && !trackOnly) {
|
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
@ -360,11 +409,11 @@ class SourceProvider {
|
|||||||
currentApp?.installedVersion,
|
currentApp?.installedVersion,
|
||||||
apkVersion,
|
apkVersion,
|
||||||
apk.apkUrls,
|
apk.apkUrls,
|
||||||
apk.apkUrls.length - 1,
|
apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0,
|
||||||
additionalSettings,
|
additionalSettings,
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
currentApp?.pinned ?? false,
|
currentApp?.pinned ?? false,
|
||||||
category: currentApp?.category);
|
categories: currentApp?.categories ?? const []);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns errors in [results, errors] instead of throwing them
|
// Returns errors in [results, errors] instead of throwing them
|
||||||
|
443
pubspec.lock
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.9.2+90 # When changing this, update the tag in main() accordingly
|
version: 0.10.9+115 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
@ -58,6 +58,7 @@ dependencies:
|
|||||||
android_alarm_manager_plus: ^2.1.0
|
android_alarm_manager_plus: ^2.1.0
|
||||||
sqflite: ^2.2.0+3
|
sqflite: ^2.2.0+3
|
||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
|
android_intent_plus: ^3.1.5
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|