Compare commits

...

67 Commits

Author SHA1 Message Date
Imran Remtulla
1be38d361f Merge pull request #272 from ImranR98/dev
No longer blocking App ID changes in updates
2023-02-12 14:20:04 -05:00
Imran Remtulla
32c40ae7b3 No longer blocking App ID changes in updates 2023-02-12 14:16:19 -05:00
Imran Remtulla
07223d81c7 Merge pull request #268 from gidano/main
Updated hu.json
2023-02-11 12:02:41 -05:00
gidano
78baee7265 Updated hu.json 2023-02-11 11:41:52 +01:00
Imran Remtulla
348c33dfe9 Merge pull request #266 from rollingmoai/patch-1
Add installation badges
2023-02-10 20:36:38 -05:00
Imran Remtulla
c408d70ae6 Merge pull request #260 from atilluF/ita
Update Italian translation
2023-02-10 20:23:13 -05:00
Imran Remtulla
3ae4e7cc8a Merge pull request #259 from bluefly000/japanese-translation
Update Japanese translation
2023-02-10 20:23:06 -05:00
rollingmoai
dab0f2bb72 Add installation badges 2023-02-09 22:13:16 +08:00
atilluF
4baf6bcd3b Update it.json 2023-02-05 12:10:59 +01:00
bluefly000
db4517aa13 Update Japanese translation 2023-02-05 12:23:30 +09:00
Imran Remtulla
55d4d1f978 Merge pull request #258 from ImranR98/dev
Removed unused commented code
2023-02-04 20:06:57 -05:00
Imran Remtulla
f89ac5965f Removed unused commented code 2023-02-04 20:06:22 -05:00
Imran Remtulla
d5ebaa161f Merge pull request #257 from ImranR98/dev
Remove unused class
2023-02-04 20:05:39 -05:00
Imran Remtulla
a4c014a8bf Remove unused class 2023-02-04 20:05:20 -05:00
Imran Remtulla
bbaa42fb01 Merge pull request #256 from ImranR98/dev
Bugfixes (#254, #252), App Uninstall and Settings Feature (#257)
2023-02-04 19:46:24 -05:00
Imran Remtulla
4fe311bc03 Update packages, increment version 2023-02-04 19:44:18 -05:00
Imran Remtulla
ea68b97ff7 Mark updated feature more clear 2023-02-04 19:30:41 -05:00
Imran Remtulla
6e0f6b528e Added App settings button 2023-02-04 19:11:28 -05:00
Imran Remtulla
a2c227931e Added uninstall option 2023-02-04 18:58:14 -05:00
Imran Remtulla
15ad3bb439 Removed unnecessary repetitive log 2023-02-04 17:21:39 -05:00
Imran Remtulla
b03d7fba1a Fix permission error on Android 10 #252 2023-02-04 17:07:59 -05:00
Imran Remtulla
31c491d7c5 Fix prev. commit. 2023-02-04 16:50:33 -05:00
Imran Remtulla
71c80f11f5 'Fix' for GlobalKey error #254 2023-02-04 12:30:34 -05:00
Imran Remtulla
eef4d33431 Merge pull request #246 from ImranR98/dev
Bugfixes for #242 and #245 + Various UI Improvements
2023-01-29 17:35:18 -05:00
Imran Remtulla
d56342e907 Merge pull request #243 from bluefly000/japanese-translation
Update Japanese translation
2023-01-29 17:32:09 -05:00
Imran Remtulla
c72c0fdb57 Increment version 2023-01-29 17:31:19 -05:00
Imran Remtulla
ffe29009ed URL select modal now works when tapping text 2023-01-29 17:29:41 -05:00
Imran Remtulla
60e3b68ebd Search allows option changes (no direct add) 2023-01-29 17:23:35 -05:00
Imran Remtulla
ee4d0f259f Generated form bugfix (initState not running) - #245 2023-01-29 17:07:11 -05:00
bluefly000
0ecfbef0a0 Update Japanese translation 2023-01-29 17:28:54 +09:00
Imran Remtulla
1b60e75ca7 Added delay after Obtainium install prompt 2023-01-28 20:59:17 -05:00
Imran Remtulla
abcfa389e8 Merge pull request #241 from ImranR98/dev
Updated screenshots
2023-01-28 00:47:26 -05:00
Imran Remtulla
a64bd67ef1 Updated screenshots 2023-01-28 00:46:54 -05:00
Imran Remtulla
4252c2711b Merge pull request #240 from ImranR98/dev
APK RegEx Filter, Increased GitHub/Codeberg Release Range, UI Tweaks
Addresses #237, #238.
2023-01-28 00:17:10 -05:00
Imran Remtulla
52913b0450 Slight UI tweak 2023-01-28 00:15:52 -05:00
Imran Remtulla
427b0ed8d2 Changed a string 2023-01-28 00:13:03 -05:00
Imran Remtulla
a85d6d4f08 Increment version, remove comment 2023-01-28 00:11:40 -05:00
Imran Remtulla
05f712603c GitHub & Codeberg - get first 100 releases (not 30) 2023-01-28 00:08:17 -05:00
Imran Remtulla
fa2a80e34c APK RegEx Filter + Updated Packages 2023-01-28 00:04:57 -05:00
Imran Remtulla
f43e5a2ff1 Merge pull request #235 from ImranR98/dev
Increment version, update packages
2023-01-22 19:55:35 -05:00
Imran Remtulla
b72aa8273e Increment version, update packages 2023-01-22 19:55:14 -05:00
Imran Remtulla
520f186e4a Merge pull request #234 from p1gp1g/themed-icon
Add themed icon for Android 13
2023-01-22 19:53:43 -05:00
sim
e1e97672cf Add themed icon for Android 13 2023-01-23 01:09:14 +01:00
Imran Remtulla
1494bcd013 Merge pull request #232 from ImranR98/dev
GitHub (and Codeberg) bugfix (#231)
2023-01-20 12:49:35 -05:00
Imran Remtulla
3457a0a12f GitHub (and Codeberg) bugfix (#231) 2023-01-20 12:48:55 -05:00
Imran Remtulla
b165400a6e Merge pull request #229 from ImranR98/dev
Increment version
2023-01-15 11:45:05 -05:00
Imran Remtulla
c47bf937f1 Increment version 2023-01-15 11:44:45 -05:00
Imran Remtulla
2e19a8c04c Merge pull request #228 from gidano/main
Update hu.json
2023-01-15 11:42:28 -05:00
gidano
05d4da86ec Update hu.json 2023-01-15 17:39:57 +01:00
Imran Remtulla
e9d1b04d54 Merge pull request #227 from ImranR98/dev
Increment version, upgrade packages
2023-01-15 11:20:45 -05:00
Imran Remtulla
cff5334c25 Increment version, upgrade packages 2023-01-15 11:20:30 -05:00
Imran Remtulla
a55346fc22 Merge pull request #226 from bluefly000/japanese-translation
Update Japanese translation
2023-01-15 11:19:01 -05:00
bluefly000
885df678e5 Update Japanese translation 2023-01-13 13:34:57 +09:00
Imran Remtulla
bf7b0c5702 Merge pull request #225 from ImranR98/dev
2 New Sources: Codeberg and HTML Fallback
2023-01-12 22:33:50 -05:00
Imran Remtulla
2972da4609 Upgraded packages 2023-01-12 22:28:47 -05:00
Imran Remtulla
b8567af98e Increment version 2023-01-12 22:24:52 -05:00
Imran Remtulla
ea62c68b40 Added the HTML fallback Source 2023-01-12 22:23:53 -05:00
Imran Remtulla
08a5af0449 Added Codeberg as a Source + search UI bugfix 2023-01-12 20:57:53 -05:00
Imran Remtulla
36f327c16e Merge pull request #220 from ImranR98/dev
- Obtainium would skip installing APKs that had the same [`versionCode`](https://developer.android.com/studio/publish/versioning#versioningsettings) since this number should be different for each new build of an App.
    - However, there are enough Apps that don't do this (#149, #219) so Obtainium now installs updates even if the `versionCode` has not changed.
- The GitHub release title filter has also been improved so that it filters by `tag_name` instead of `title` for releases with empty titles (as it seems like GitHub automatically displays the tag as the title in such cases).
2023-01-07 16:58:58 -05:00
Imran Remtulla
768213cb34 Increment version 2023-01-07 16:50:01 -05:00
Imran Remtulla
e888fb7120 Don't skip installing same-versionCode updates 2023-01-07 16:49:38 -05:00
Imran Remtulla
1fb68dd674 GitHub release filter bugfix 2023-01-07 16:18:26 -05:00
Imran Remtulla
5c4bb8f84c Merge pull request #217 from ImranR98/dev
Bugfixes and UI Tweaks (#213, #215, #216)
2023-01-06 21:11:58 -05:00
Imran Remtulla
1c8e759494 Increment version + updated packages 2023-01-06 21:11:13 -05:00
Imran Remtulla
081c2a07d2 Categories re-added on import (#213) 2023-01-06 21:10:04 -05:00
Imran Remtulla
02751fe8fa Made GitHub PATs hidden (password field) (#215) 2023-01-06 20:57:26 -05:00
Imran Remtulla
95f3362a84 Apps bottom bar tweaks (#216) 2023-01-06 20:47:22 -05:00
33 changed files with 1384 additions and 860 deletions

View File

@@ -9,6 +9,7 @@ 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/)
@@ -18,6 +19,17 @@ Currently supported App sources:
- Third Party F-Droid Repos - Third Party F-Droid Repos
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo` - 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.
@@ -28,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" /> |

View File

@@ -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,7 +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 <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/> android:maxSdkVersion="29"/>
</manifest> </manifest>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

View File

@@ -4,7 +4,6 @@
"noVersionFound": "Release-Version nicht ermittelbar", "noVersionFound": "Release-Version nicht ermittelbar",
"urlMatchesNoSource": "URL stimmt mit keiner bekannten Quelle überein", "urlMatchesNoSource": "URL stimmt mit keiner bekannten Quelle überein",
"cantInstallOlderVersion": "Installation einer älteren App-Version nicht möglich", "cantInstallOlderVersion": "Installation einer älteren App-Version nicht möglich",
"appIdMismatch": "Die heruntergeladene Paket-ID stimmt nicht mit der vorhandenen App-ID überein",
"functionNotImplemented": "Diese Klasse hat diese Funktion nicht implementiert", "functionNotImplemented": "Diese Klasse hat diese Funktion nicht implementiert",
"placeholder": "Platzhalter", "placeholder": "Platzhalter",
"someErrors": "Es traten einige Fehler auf", "someErrors": "Es traten einige Fehler auf",
@@ -74,7 +73,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 +176,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",
@@ -211,6 +208,14 @@
"language": "Sprache", "language": "Sprache",
"storagePermissionDenied": "Storage permission denied", "storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", "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"

View File

@@ -4,7 +4,6 @@
"noVersionFound": "Could not determine release version", "noVersionFound": "Could not determine release version",
"urlMatchesNoSource": "URL does not match a known source", "urlMatchesNoSource": "URL does not match a known source",
"cantInstallOlderVersion": "Cannot install an older version of an App", "cantInstallOlderVersion": "Cannot install an older version of an App",
"appIdMismatch": "Downloaded package ID does not match existing App ID",
"functionNotImplemented": "This class has not implemented this function", "functionNotImplemented": "This class has not implemented this function",
"placeholder": "Placeholder", "placeholder": "Placeholder",
"someErrors": "Some Errors Occurred", "someErrors": "Some Errors Occurred",
@@ -74,7 +73,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 +176,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",
@@ -211,6 +208,14 @@
"language": "Language", "language": "Language",
"storagePermissionDenied": "Storage permission denied", "storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", "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"

View File

@@ -4,7 +4,6 @@
"noVersionFound": "Nem sikerült meghatározni a kiadás verzióját", "noVersionFound": "Nem sikerült meghatározni a kiadás verzióját",
"urlMatchesNoSource": "Az URL nem egyezik ismert forrással", "urlMatchesNoSource": "Az URL nem egyezik ismert forrással",
"cantInstallOlderVersion": "Nem telepíthető egy app régebbi verziója", "cantInstallOlderVersion": "Nem telepíthető egy app régebbi verziója",
"appIdMismatch": "A letöltött csomagazonosító nem egyezik a meglévő app azonosítóval",
"functionNotImplemented": "Ez az osztály nem valósította meg ezt a függvényt", "functionNotImplemented": "Ez az osztály nem valósította meg ezt a függvényt",
"placeholder": "Helykitöltő", "placeholder": "Helykitöltő",
"someErrors": "Néhány hiba történt", "someErrors": "Néhány hiba történt",
@@ -74,7 +73,6 @@
"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ő autom. (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",
@@ -178,7 +176,6 @@
"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",
@@ -207,9 +204,17 @@
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.", "categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
"addCategory": "Új kategória", "addCategory": "Új kategória",
"label": "Címke", "label": "Címke",
"language": "Language", "language": "Nyelv",
"storagePermissionDenied": "Storage permission denied", "storagePermissionDenied": "Tárhely engedély megtagadva",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", "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"

View File

@@ -4,7 +4,6 @@
"noVersionFound": "Impossibile determinare la versione della release", "noVersionFound": "Impossibile determinare la versione della release",
"urlMatchesNoSource": "L'URL non corrisponde ad alcuna fonte conosciuta", "urlMatchesNoSource": "L'URL non corrisponde ad alcuna fonte conosciuta",
"cantInstallOlderVersion": "Impossibile installare una versione precedente di un'App", "cantInstallOlderVersion": "Impossibile installare una versione precedente di un'App",
"appIdMismatch": "L'ID del pacchetto scaricato non corrisponde all'ID dell'App esistente",
"functionNotImplemented": "Questa classe non ha implementato questa funzione", "functionNotImplemented": "Questa classe non ha implementato questa funzione",
"placeholder": "Segnaposto", "placeholder": "Segnaposto",
"someErrors": "Si sono verificati degli errori", "someErrors": "Si sono verificati degli errori",
@@ -74,7 +73,6 @@
"changeX": "Modifica {}", "changeX": "Modifica {}",
"installUpdateApps": "Installa/Aggiorna App", "installUpdateApps": "Installa/Aggiorna App",
"installUpdateSelectedApps": "Installa/Aggiorna 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ì",
@@ -178,7 +176,6 @@
"installedVersionX": "Versione installata: {}", "installedVersionX": "Versione installata: {}",
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}", "lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
"remove": "Rimuovi", "remove": "Rimuovi",
"removeAppQuestion": "Rimuovere l'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",
@@ -209,8 +206,16 @@
"addCategory": "Aggiungi categoria", "addCategory": "Aggiungi categoria",
"label": "Etichetta", "label": "Etichetta",
"language": "Lingua", "language": "Lingua",
"storagePermissionDenied": "Storage permission denied", "storagePermissionDenied": "Accesso ai file non autorizzato",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", "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"

View File

@@ -4,10 +4,9 @@
"noVersionFound": "リリースバージョンを特定できませんでした", "noVersionFound": "リリースバージョンを特定できませんでした",
"urlMatchesNoSource": "URLが既知のソースと一致しません", "urlMatchesNoSource": "URLが既知のソースと一致しません",
"cantInstallOlderVersion": "旧バージョンのアプリをインストールできません", "cantInstallOlderVersion": "旧バージョンのアプリをインストールできません",
"appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません",
"functionNotImplemented": "このクラスはこの機能を実装していません", "functionNotImplemented": "このクラスはこの機能を実装していません",
"placeholder": "プレースホルダー", "placeholder": "プレースホルダー",
"someErrors": "いくつかのエラーが発生しました", "someErrors": "何らかのエラーが発生しました",
"unexpectedError": "予期せぬエラーが発生しました", "unexpectedError": "予期せぬエラーが発生しました",
"ok": "OK", "ok": "OK",
"and": "と", "and": "と",
@@ -65,16 +64,15 @@
"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 +80,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 +107,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,7 +131,7 @@
"bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔", "bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔",
"neverManualOnly": "手動", "neverManualOnly": "手動",
"appearance": "外観", "appearance": "外観",
"showWebInAppView": "アプリビューにソースウェブページを表示する", "showWebInAppView": "アプリページにソースのWebページを表示する",
"pinUpdates": "アップデートがあるアプリをトップに固定する", "pinUpdates": "アップデートがあるアプリをトップに固定する",
"updates": "アップデート", "updates": "アップデート",
"sourceSpecific": "Github アクセストークン", "sourceSpecific": "Github アクセストークン",
@@ -145,23 +143,23 @@
"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 +176,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",
@@ -209,8 +206,16 @@
"addCategory": "カテゴリを追加", "addCategory": "カテゴリを追加",
"label": "ラベル", "label": "ラベル",
"language": "言語", "language": "言語",
"storagePermissionDenied": "Storage permission denied", "storagePermissionDenied": "ストレージ権限が拒否されました",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
"removeFromObtainium": "Obtainiumから削除する",
"uninstallFromDevice": "デバイスからアンインストールする",
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"
},
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
@@ -248,11 +253,11 @@
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})" "other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
}, },
"xAndNMoreUpdatesAvailable": { "xAndNMoreUpdatesAvailable": {
"one": "{}とさらに{}個のアプリのアップデートが利用可能です", "one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
"other": "{}とさらに{}個のアプリのアップデートが利用可能です" "other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
}, },
"xAndNMoreUpdatesInstalled": { "xAndNMoreUpdatesInstalled": {
"one": "{}とさらに{}個のアプリがアップデートされました", "one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{}とさらに{}個のアプリがアップデートされました" "other": "{} とさらに {} 個のアプリがアップデートされました"
} }
} }

View File

@@ -4,7 +4,6 @@
"noVersionFound": "无法确定更新版本", "noVersionFound": "无法确定更新版本",
"urlMatchesNoSource": "URL 与已知来源不符", "urlMatchesNoSource": "URL 与已知来源不符",
"cantInstallOlderVersion": "无法安装旧版应用程序", "cantInstallOlderVersion": "无法安装旧版应用程序",
"appIdMismatch": "下载的软件包名与现有的应用程序包名不一致",
"functionNotImplemented": "该类没有实现此功能", "functionNotImplemented": "该类没有实现此功能",
"placeholder": "占位符", "placeholder": "占位符",
"someErrors": "出现了一些错误", "someErrors": "出现了一些错误",
@@ -178,7 +177,6 @@
"installedVersionX": "已安装: {}", "installedVersionX": "已安装: {}",
"lastUpdateCheckX": "最后检查: {}", "lastUpdateCheckX": "最后检查: {}",
"remove": "删除", "remove": "删除",
"removeAppQuestion": "删除应用?",
"yesMarkUpdated": "'是的,标为已更新", "yesMarkUpdated": "'是的,标为已更新",
"fdroid": "F-Droid", "fdroid": "F-Droid",
"appIdOrName": "应用 ID 或名称", "appIdOrName": "应用 ID 或名称",
@@ -211,6 +209,13 @@
"language": "语言", "language": "语言",
"storagePermissionDenied": "存储权限已被拒绝", "storagePermissionDenied": "存储权限已被拒绝",
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别", "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 限制) - 在 {} 分钟后重试"

View 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);
}
}
}

View File

@@ -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]);

47
lib/app_sources/html.dart Normal file
View 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);
}
}
}

View File

@@ -3,7 +3,6 @@ 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'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/providers/settings_provider.dart';
abstract class GeneratedFormItem { abstract class GeneratedFormItem {
late String key; late String key;
@@ -24,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',
@@ -32,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,
@@ -129,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}) {
@@ -153,28 +170,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
widget.onValueChanges(returnValues, valid, isBuilding); widget.onValueChanges(returnValues, valid, isBuilding);
} }
// Generates a random light color initForm() {
// Courtesy of ChatGPT 😭 (with a bugfix 🥳) initKey = widget.key.toString();
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();
}
@override
void initState() {
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;
@@ -188,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,
@@ -241,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) {

View File

@@ -29,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 {
@@ -44,10 +44,6 @@ class DowngradeError extends ObtainiumError {
DowngradeError() : super(tr('cantInstallOlderVersion')); DowngradeError() : super(tr('cantInstallOlderVersion'));
} }
class IDChangedError extends ObtainiumError {
IDChangedError() : super(tr('appIdMismatch'));
}
class NotImplementedError extends ObtainiumError { class NotImplementedError extends ObtainiumError {
NotImplementedError() : super(tr('functionNotImplemented')); NotImplementedError() : super(tr('functionNotImplemented'));
} }

View File

@@ -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.13'; const String currentVersion = '0.10.8';
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

View File

@@ -24,6 +24,7 @@ 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 = '';
@@ -31,16 +32,23 @@ class _AddAppPageState extends State<AddAppPage> {
Map<String, dynamic> additionalSettings = {}; Map<String, dynamic> additionalSettings = {};
bool additionalSettingsValid = true; bool additionalSettingsValid = true;
List<String> pickedCategories = []; 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;
changeUserInput(String input, bool valid, bool isBuilding,
{bool isSearch = false}) {
userInput = input; userInput = input;
if (!isBuilding) { if (!isBuilding) {
setState(() { setState(() {
if (isSearch) {
searchnum++;
}
var source = valid ? sourceProvider.getSource(userInput) : null; var source = valid ? sourceProvider.getSource(userInput) : null;
if (pickedSource.runtimeType != source.runtimeType) { if (pickedSource.runtimeType != source.runtimeType) {
pickedSource = source; pickedSource = source;
@@ -67,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) {
@@ -85,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) {
@@ -164,10 +174,12 @@ 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'),
defaultValue: userInput,
additionalValidators: [ additionalValidators: [
(value) { (value) {
try { try {
@@ -198,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
@@ -249,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) =>
@@ -288,11 +303,15 @@ class _AddAppPageState extends State<AddAppPage> {
if (selectedUrls != null && if (selectedUrls != null &&
selectedUrls.isNotEmpty) { selectedUrls.isNotEmpty) {
changeUserInput( changeUserInput(
selectedUrls[0], true, false); 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')))
@@ -315,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) {

View File

@@ -42,6 +42,8 @@ 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( var infoColumn = Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -190,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
@@ -203,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: () {
@@ -268,7 +264,8 @@ class _AppPageState extends State<AppPage> {
}).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;
@@ -289,7 +286,15 @@ 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) if (app != null && settingsProvider.showAppWebpage)
IconButton( IconButton(
onPressed: () { onPressed: () {
@@ -317,7 +322,7 @@ class _AppPageState extends State<AppPage> {
tooltip: tr('more')), 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,43 +363,16 @@ 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(
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(); Navigator.of(context).pop();
}, }
child: Text(tr('cancel')))
],
);
}); });
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
@@ -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(

View File

@@ -344,12 +344,15 @@ class AppsPageState extends State<AppsPage> {
)); ));
}, childCount: sortedApps.length)) }, childCount: sortedApps.length))
])), ])),
persistentFooterButtons: [ persistentFooterButtons: appsProvider.apps.isEmpty
? null
: [
Row( Row(
children: [ children: [
selectedApps.isEmpty selectedApps.isEmpty
? IconButton( ? TextButton.icon(
visualDensity: VisualDensity.compact, style: const ButtonStyle(
visualDensity: VisualDensity.compact),
onPressed: () { onPressed: () {
selectThese(sortedApps.map((e) => e.app).toList()); selectThese(sortedApps.map((e) => e.app).toList());
}, },
@@ -357,13 +360,14 @@ class AppsPageState extends State<AppsPage> {
Icons.select_all_outlined, Icons.select_all_outlined,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
tooltip: tr('selectAll')) label: Text(sortedApps.length.toString()))
: TextButton.icon( : TextButton.icon(
style: style: const ButtonStyle(
const ButtonStyle(visualDensity: VisualDensity.compact), visualDensity: VisualDensity.compact),
onPressed: () { onPressed: () {
selectedApps.isEmpty selectedApps.isEmpty
? selectThese(sortedApps.map((e) => e.app).toList()) ? selectThese(
sortedApps.map((e) => e.app).toList())
: clearSelected(); : clearSelected();
}, },
icon: Icon( icon: Icon(
@@ -375,91 +379,104 @@ class AppsPageState extends State<AppsPage> {
label: Text(selectedApps.length.toString())), label: Text(selectedApps.length.toString())),
const VerticalDivider(), const VerticalDivider(),
Expanded( Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
selectedApps.isEmpty IconButton(
? const SizedBox()
: IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () { onPressed: selectedApps.isEmpty
showDialog<Map<String, dynamic>?>( ? null
context: context, : () {
builder: (BuildContext ctx) { appsProvider.removeAppsWithModal(
return GeneratedFormModal( context, selectedApps.toList());
title: tr('removeSelectedAppsQuestion'),
items: const [],
initValid: true,
message: tr(
'xWillBeRemovedButRemainInstalled',
args: [
plural('apps', selectedApps.length)
]),
);
}).then((values) {
if (values != null) {
appsProvider.removeApps(
selectedApps.map((e) => e.id).toList());
}
});
}, },
tooltip: tr('removeSelectedApps'), tooltip: tr('removeSelectedApps'),
icon: const Icon(Icons.delete_outline_outlined), icon: const Icon(Icons.delete_outline_outlined),
), ),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: appsProvider.areDownloadsRunning() || onPressed: appsProvider
(existingUpdateIdsAllOrSelected.isEmpty && .areDownloadsRunning() ||
newInstallIdsAllOrSelected.isEmpty && (existingUpdateIdsAllOrSelected
trackOnlyUpdateIdsAllOrSelected.isEmpty) .isEmpty &&
newInstallIdsAllOrSelected
.isEmpty &&
trackOnlyUpdateIdsAllOrSelected
.isEmpty)
? null ? null
: () { : () {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
List<GeneratedFormItem> formItems = []; List<GeneratedFormItem> formItems =
if (existingUpdateIdsAllOrSelected.isNotEmpty) { [];
formItems.add(GeneratedFormSwitch('updates', if (existingUpdateIdsAllOrSelected
.isNotEmpty) {
formItems.add(GeneratedFormSwitch(
'updates',
label: tr('updateX', args: [ label: tr('updateX', args: [
plural('apps', plural(
existingUpdateIdsAllOrSelected.length) 'apps',
existingUpdateIdsAllOrSelected
.length)
]), ]),
defaultValue: true)); defaultValue: true));
} }
if (newInstallIdsAllOrSelected.isNotEmpty) { if (newInstallIdsAllOrSelected
formItems.add(GeneratedFormSwitch('installs', .isNotEmpty) {
formItems.add(GeneratedFormSwitch(
'installs',
label: tr('installX', args: [ label: tr('installX', args: [
plural('apps', plural(
newInstallIdsAllOrSelected.length) 'apps',
newInstallIdsAllOrSelected
.length)
]), ]),
defaultValue: existingUpdateIdsAllOrSelected defaultValue:
existingUpdateIdsAllOrSelected
.isNotEmpty)); .isNotEmpty));
} }
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { if (trackOnlyUpdateIdsAllOrSelected
formItems.add(GeneratedFormSwitch('trackonlies', .isNotEmpty) {
label: tr('markXTrackOnlyAsUpdated', args: [ formItems.add(GeneratedFormSwitch(
plural('apps', 'trackonlies',
trackOnlyUpdateIdsAllOrSelected.length) label: tr(
'markXTrackOnlyAsUpdated',
args: [
plural(
'apps',
trackOnlyUpdateIdsAllOrSelected
.length)
]), ]),
defaultValue: existingUpdateIdsAllOrSelected defaultValue:
existingUpdateIdsAllOrSelected
.isNotEmpty || .isNotEmpty ||
newInstallIdsAllOrSelected.isNotEmpty)); newInstallIdsAllOrSelected
.isNotEmpty));
} }
showDialog<Map<String, dynamic>?>( showDialog<Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
var totalApps = existingUpdateIdsAllOrSelected var totalApps = existingUpdateIdsAllOrSelected
.length + .length +
newInstallIdsAllOrSelected.length + newInstallIdsAllOrSelected
trackOnlyUpdateIdsAllOrSelected.length; .length +
trackOnlyUpdateIdsAllOrSelected
.length;
return GeneratedFormModal( return GeneratedFormModal(
title: tr('changeX', title: tr('changeX', args: [
args: [plural('apps', totalApps)]), plural('apps', totalApps)
items: formItems.map((e) => [e]).toList(), ]),
items: formItems
.map((e) => [e])
.toList(),
initValid: true, initValid: true,
); );
}).then((values) { }).then((values) {
if (values != null) { if (values != null) {
if (values.isEmpty) { if (values.isEmpty) {
values = getDefaultValuesFromFormItems( values =
getDefaultValuesFromFormItems(
[formItems]); [formItems]);
} }
bool shouldInstallUpdates = bool shouldInstallUpdates =
@@ -478,20 +495,22 @@ class AppsPageState extends State<AppsPage> {
.then((_) { .then((_) {
List<String> toInstall = []; List<String> toInstall = [];
if (shouldInstallUpdates) { if (shouldInstallUpdates) {
toInstall toInstall.addAll(
.addAll(existingUpdateIdsAllOrSelected); existingUpdateIdsAllOrSelected);
} }
if (shouldInstallNew) { if (shouldInstallNew) {
toInstall toInstall.addAll(
.addAll(newInstallIdsAllOrSelected); newInstallIdsAllOrSelected);
} }
if (shouldMarkTrackOnlies) { if (shouldMarkTrackOnlies) {
toInstall.addAll( toInstall.addAll(
trackOnlyUpdateIdsAllOrSelected); trackOnlyUpdateIdsAllOrSelected);
} }
appsProvider appsProvider
.downloadAndInstallLatestApps(toInstall, .downloadAndInstallLatestApps(
globalNavigatorKey.currentContext) toInstall,
globalNavigatorKey
.currentContext)
.catchError((e) { .catchError((e) {
showError(e, context); showError(e, context);
}); });
@@ -505,16 +524,17 @@ class AppsPageState extends State<AppsPage> {
icon: const Icon( icon: const Icon(
Icons.file_download_outlined, Icons.file_download_outlined,
)), )),
selectedApps.isEmpty IconButton(
? const SizedBox()
: IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () async { onPressed: selectedApps.isEmpty
? null
: () async {
try { try {
Set<String>? preselected; Set<String>? preselected;
var showPrompt = false; var showPrompt = false;
for (var element in selectedApps) { for (var element in selectedApps) {
var currentCats = element.categories.toSet(); var currentCats =
element.categories.toSet();
if (preselected == null) { if (preselected == null) {
preselected = currentCats; preselected = currentCats;
} else { } else {
@@ -527,38 +547,47 @@ class AppsPageState extends State<AppsPage> {
} }
var cont = true; var cont = true;
if (showPrompt) { if (showPrompt) {
cont = await showDialog<Map<String, dynamic>?>( cont = await showDialog<
Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder:
(BuildContext ctx) {
return GeneratedFormModal( return GeneratedFormModal(
title: tr('categorize'), title: tr('categorize'),
items: const [], items: const [],
initValid: true, initValid: true,
message: message: tr(
tr('selectedCategorizeWarning'), 'selectedCategorizeWarning'),
); );
}) != }) !=
null; null;
} }
if (cont) { if (cont) {
await showDialog<Map<String, dynamic>?>( // ignore: use_build_context_synchronously
await showDialog<
Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return GeneratedFormModal( return GeneratedFormModal(
title: tr('categorize'), title: tr('categorize'),
items: const [], items: const [],
initValid: true, initValid: true,
singleNullReturnButton: tr('continue'), singleNullReturnButton:
tr('continue'),
additionalWidgets: [ additionalWidgets: [
CategoryEditorSelector( CategoryEditorSelector(
preselected: !showPrompt preselected: !showPrompt
? preselected ?? {} ? preselected ?? {}
: {}, : {},
showLabelWhenNotEmpty: false, showLabelWhenNotEmpty:
onSelected: (categories) { false,
appsProvider onSelected:
.saveApps(selectedApps.map((e) { (categories) {
e.categories = categories; appsProvider.saveApps(
selectedApps
.map((e) {
e.categories =
categories;
return e; return e;
}).toList()); }).toList());
}, },
@@ -574,78 +603,65 @@ class AppsPageState extends State<AppsPage> {
tooltip: tr('categorize'), tooltip: tr('categorize'),
icon: const Icon(Icons.category_outlined), icon: const Icon(Icons.category_outlined),
), ),
selectedApps.isEmpty IconButton(
? const SizedBox()
: IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () { onPressed: selectedApps.isEmpty
? null
: () {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return AlertDialog( return AlertDialog(
scrollable: true, scrollable: true,
content: Padding( content: Padding(
padding: const EdgeInsets.only(top: 6), padding:
const EdgeInsets.only(
top: 6),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.spaceAround, MainAxisAlignment
.spaceAround,
children: [ children: [
IconButton( IconButton(
onPressed: onPressed: appsProvider
appsProvider
.areDownloadsRunning() .areDownloadsRunning()
? null ? null
: () { : () {
showDialog( showDialog(
context: context, context:
context,
builder: builder:
(BuildContext (BuildContext
ctx) { ctx) {
return AlertDialog( return AlertDialog(
title: Text(tr( title:
'markXSelectedAppsAsUpdated', Text(tr('markXSelectedAppsAsUpdated', args: [
args: [ selectedApps.length.toString()
selectedApps
.length
.toString()
])), ])),
content: Text( content:
tr('onlyWorksWithNonEVDApps'), Text(
style: const TextStyle( tr('onlyWorksWithNonVersionDetectApps'),
fontWeight: style: const TextStyle(fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
FontWeight
.bold,
fontStyle:
FontStyle.italic),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed: () {
() { Navigator.of(context).pop();
Navigator.of(context)
.pop();
}, },
child: Text( child: Text(tr('no'))),
tr('no'))),
TextButton( TextButton(
onPressed: onPressed: () {
() { HapticFeedback.selectionClick();
HapticFeedback appsProvider.saveApps(selectedApps.map((a) {
.selectionClick(); if (a.installedVersion != null && a.additionalSettings['noVersionDetection'] == true) {
appsProvider
.saveApps(selectedApps.map((a) {
if (a.installedVersion !=
null) {
a.installedVersion = a.latestVersion; a.installedVersion = a.latestVersion;
} }
return a; return a;
}).toList()); }).toList());
Navigator.of(context) Navigator.of(context).pop();
.pop();
}, },
child: Text( child: Text(tr('yes')))
tr('yes')))
], ],
); );
}).whenComplete(() { }).whenComplete(() {
@@ -654,86 +670,117 @@ class AppsPageState extends State<AppsPage> {
.pop(); .pop();
}); });
}, },
tooltip: tooltip: tr(
tr('markSelectedAppsUpdated'), 'markSelectedAppsUpdated'),
icon: const Icon(Icons.done)), icon: const Icon(
Icons.done)),
IconButton( IconButton(
onPressed: () { onPressed: () {
var pinStatus = selectedApps var pinStatus = selectedApps
.where((element) => .where((element) =>
element.pinned) element
.pinned)
.isEmpty; .isEmpty;
appsProvider.saveApps( appsProvider
selectedApps.map((e) { .saveApps(
e.pinned = pinStatus; selectedApps
.map(
(e) {
e.pinned =
pinStatus;
return e; return e;
}).toList()); }).toList());
Navigator.of(context).pop(); Navigator.of(
context)
.pop();
}, },
tooltip: selectedApps tooltip: selectedApps
.where((element) => .where((element) =>
element.pinned) element
.pinned)
.isEmpty .isEmpty
? tr('pinToTop') ? tr('pinToTop')
: tr('unpinFromTop'), : tr(
'unpinFromTop'),
icon: Icon(selectedApps icon: Icon(selectedApps
.where((element) => .where((element) =>
element.pinned) element
.pinned)
.isEmpty .isEmpty
? Icons.bookmark_outline_rounded ? Icons
.bookmark_outline_rounded
: Icons : Icons
.bookmark_remove_outlined), .bookmark_remove_outlined),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
String urls = ''; String urls = '';
for (var a in selectedApps) { for (var a
urls += '${a.url}\n'; in selectedApps) {
urls +=
'${a.url}\n';
} }
urls = urls.substring( urls =
0, urls.length - 1); urls.substring(
0,
urls.length -
1);
Share.share(urls, Share.share(urls,
subject: tr( subject: tr(
'selectedAppURLsFromObtainium')); 'selectedAppURLsFromObtainium'));
Navigator.of(context).pop(); Navigator.of(
context)
.pop();
}, },
tooltip: tr('shareSelectedAppURLs'), tooltip: tr(
icon: const Icon(Icons.share), 'shareSelectedAppURLs'),
icon: const Icon(
Icons.share),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context:
builder: (BuildContext ctx) { context,
builder:
(BuildContext
ctx) {
return GeneratedFormModal( return GeneratedFormModal(
title: tr( title: tr(
'resetInstallStatusForSelectedAppsQuestion'), 'resetInstallStatusForSelectedAppsQuestion'),
items: const [], items: const [],
initValid: true, initValid:
true,
message: tr( message: tr(
'installStatusOfXWillBeResetExplanation', 'installStatusOfXWillBeResetExplanation',
args: [ args: [
plural( plural(
'app', 'app',
selectedApps selectedApps.length)
.length)
]), ]),
); );
}).then((values) { }).then((values) {
if (values != null) { if (values !=
null) {
appsProvider.saveApps( appsProvider.saveApps(
selectedApps.map((e) { selectedApps
e.installedVersion = null; .map(
(e) {
e.installedVersion =
null;
return e; return e;
}).toList()); }).toList());
} }
}).whenComplete(() { }).whenComplete(() {
Navigator.of(context).pop(); Navigator.of(
context)
.pop();
}); });
}, },
tooltip: tr('resetInstallStatus'), tooltip: tr(
icon: const Icon( 'resetInstallStatus'),
Icons.restore_page_outlined), icon: const Icon(Icons
.restore_page_outlined),
), ),
]), ]),
), ),
@@ -744,7 +791,7 @@ class AppsPageState extends State<AppsPage> {
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
), ),
], ],
)), ))),
const VerticalDivider(), const VerticalDivider(),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -767,11 +814,9 @@ class AppsPageState extends State<AppsPage> {
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
), ),
appsProvider.apps.isEmpty TextButton.icon(
? const SizedBox() style: const ButtonStyle(
: TextButton.icon( visualDensity: VisualDensity.compact),
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
label: Text( label: Text(
filter.isIdenticalTo(neutralFilter, settingsProvider) filter.isIdenticalTo(neutralFilter, settingsProvider)
? tr('filter') ? tr('filter')
@@ -819,7 +864,8 @@ class AppsPageState extends State<AppsPage> {
CategoryEditorSelector( CategoryEditorSelector(
preselected: filter.categoryFilter, preselected: filter.categoryFilter,
onSelected: (categories) { onSelected: (categories) {
filter.categoryFilter = categories.toSet(); filter.categoryFilter =
categories.toSet();
}, },
) )
], ],

View File

@@ -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 || } else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty && (selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) { 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,

View File

@@ -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(
@@ -100,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)
@@ -547,10 +564,7 @@ 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) {
return Row(children: [ select(bool? value) {
Checkbox(
value: urlWithDescriptionSelections[urlWithD],
onChanged: (value) {
setState(() { setState(() {
value ??= false; value ??= false;
if (value! && widget.onlyOneSelectionAllowed) { if (value! && widget.onlyOneSelectionAllowed) {
@@ -559,6 +573,13 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
urlWithDescriptionSelections[urlWithD] = value!; urlWithDescriptionSelections[urlWithD] = value!;
} }
}); });
}
return Row(children: [
Checkbox(
value: urlWithDescriptionSelections[urlWithD],
onChanged: (value) {
select(value);
}), }),
const SizedBox( const SizedBox(
width: 8, width: 8,
@@ -582,13 +603,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
const TextStyle(decoration: TextDecoration.underline), const TextStyle(decoration: TextDecoration.underline),
textAlign: TextAlign.start, textAlign: TextAlign.start,
)), )),
Text( GestureDetector(
onTap: () {
select(!(urlWithDescriptionSelections[urlWithD] ?? false));
},
child: Text(
urlWithD.value.length > 128 urlWithD.value.length > 128
? '${urlWithD.value.substring(0, 128)}...' ? '${urlWithD.value.substring(0, 128)}...'
: urlWithD.value, : urlWithD.value,
style: const TextStyle( style: const TextStyle(
fontStyle: FontStyle.italic, fontSize: 12), fontStyle: FontStyle.italic, fontSize: 12),
), ),
),
const SizedBox( const SizedBox(
height: 8, height: 8,
) )

View File

@@ -4,10 +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/main.dart'; import 'package:obtainium/main.dart';
import 'package:obtainium/providers/apps_provider.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';

View File

@@ -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,6 +13,8 @@ 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';
@@ -23,6 +26,7 @@ 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;
@@ -175,12 +179,9 @@ class AppsProvider with ChangeNotifier {
} }
} }
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
// The former case should be handled (give the App its real ID), the latter is a security issue // In either case, the app should be given the new ID
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
if (app.id != newInfo.packageName) { if (app.id != newInfo.packageName) {
if (apps[app.id] != null && !SourceProvider().isTempId(app.id)) {
throw IDChangedError();
}
var originalAppId = app.id; var originalAppId = app.id;
app.id = newInfo.packageName; app.id = newInfo.packageName;
downloadedFile = downloadedFile.renameSync( downloadedFile = downloadedFile.renameSync(
@@ -247,9 +248,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;
@@ -258,6 +260,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];
@@ -265,6 +276,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) {
@@ -284,6 +296,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) {
@@ -441,9 +454,6 @@ class AppsProvider with ChangeNotifier {
} catch (e) { } catch (e) {
// //
} }
if (!res) {
logs.add(tr('versionCorrectionDisabled'));
}
return res; return res;
} }
@@ -622,6 +632,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();
@@ -707,7 +768,7 @@ class AppsProvider with ChangeNotifier {
exportDir = await getExternalStorageDirectory(); exportDir = await getExternalStorageDirectory();
path = exportDir!.path; path = exportDir!.path;
} }
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 28) { if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
if (await Permission.storage.isDenied) { if (await Permission.storage.isDenied) {
await Permission.storage.request(); await Permission.storage.request();
} }

View File

@@ -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;
@@ -154,6 +155,10 @@ class App {
// 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';
@@ -220,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
@@ -264,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(),
@@ -276,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
@@ -337,10 +368,13 @@ class SourceProvider {
} }
Future<App> getApp( Future<App> getApp(
AppSource source, String url, Map<String, dynamic> additionalSettings, AppSource source,
{App? currentApp, String url,
Map<String, dynamic> additionalSettings, {
App? currentApp,
bool trackOnlyOverride = false, bool trackOnlyOverride = false,
noVersionDetectionOverride = false}) async { noVersionDetectionOverride = false,
}) async {
if (trackOnlyOverride || source.enforceTrackOnly) { if (trackOnlyOverride || source.enforceTrackOnly) {
additionalSettings['trackOnly'] = true; additionalSettings['trackOnly'] = true;
} }
@@ -351,6 +385,11 @@ class SourceProvider {
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();
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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.13+103 # When changing this, update the tag in main() accordingly version: 0.10.8+114 # 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: