Compare commits

...

134 Commits

Author SHA1 Message Date
9f19e4dc83 Merge pull request #945 from ImranR98/dev
Add export dir pick button + update packages
2023-09-29 12:13:38 -04:00
c9cb865c9c Add export dir pick button + update packages 2023-09-29 12:13:11 -04:00
b8a7dd984d Merge pull request #934 from LucasTavaresA/main
Update pt.json
2023-09-29 12:12:55 -04:00
3ede46c445 Merge pull request #937 from TangyWrecker/main
Update ru.json
2023-09-29 12:12:39 -04:00
87c4af94d3 Merge pull request #944 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-29 12:12:32 -04:00
7e85ccaf28 locale(pl): Update Polish translations 2023-09-29 02:39:18 +02:00
db3a262410 Update ru.json
oops
2023-09-26 01:13:01 +03:00
70162da491 Update ru.json 2023-09-26 01:09:19 +03:00
f3632a4033 Update pt.json 2023-09-24 15:22:56 -03:00
2f4e176116 Merge pull request #933 from ImranR98/dev
Downgrade permission handler to fix #932 + APKPure improvement
2023-09-23 17:19:52 -04:00
3ea8dc598c Downgrade permission handler to fix #932 + APKPure improvement 2023-09-23 17:18:12 -04:00
3f63dc0180 Merge pull request #930 from gidano/main
Update hu.json
2023-09-23 14:37:19 -04:00
1f6701b183 Update hu.json 2023-09-23 20:36:00 +02:00
3a170fbf90 Merge pull request #931 from ImranR98/dev
Fix major GitHub bug from previous release (#929)
2023-09-23 14:32:46 -04:00
b57e523a07 Increment version 2023-09-23 14:32:13 -04:00
8c4509f255 Fix major GitHub bug (#929) 2023-09-23 14:31:52 -04:00
3878c287be Update hu.json 2023-09-23 20:23:25 +02:00
7789e3f3a7 Removed excessive logs 2023-09-23 14:02:55 -04:00
94ac744f41 Merge pull request #928 from ImranR98/dev
Fix version number
2023-09-23 13:43:25 -04:00
a83211f260 Fix version number 2023-09-23 13:43:04 -04:00
4f733affc5 Merge pull request #927 from ImranR98/dev
- Change a default F-Droid Source option
- Add GitHub "don't sort" toggle (#925)
- Better IzzyOnDroid support (#926)
- Allow 'm.' subdomain for APKPure
- Added HTML reverse sorting (#915)
2023-09-23 13:41:08 -04:00
ffb2a982bc Merge branch 'main' into dev 2023-09-23 13:40:32 -04:00
9658a65661 Upgrade packages, increment version 2023-09-23 13:36:46 -04:00
68d4083708 Added HTML reverse sorting (#915)
+ translatable strings (#916)
2023-09-23 13:31:27 -04:00
af5377d2eb Allow 'm.' subdomain for APKPure 2023-09-23 13:21:48 -04:00
6ac8ad543c Better IzzyOnDroid support (#926) 2023-09-23 13:16:51 -04:00
c10a63e76e Add GitHub "don't sort" toggle (#925) 2023-09-23 12:48:21 -04:00
b423a2af52 Change a default F-Droid Source option 2023-09-22 00:22:54 -04:00
9b70289345 Merge pull request #914 from ygxbnet/main
Update zh.json
2023-09-18 20:11:08 -04:00
2a94c1f33a Update main.dart 2023-09-19 00:51:58 +08:00
ea205c1acb Update zh.json 2023-09-19 00:32:20 +08:00
6e1767730b Update zh.json 2023-09-18 00:45:45 +08:00
3fc1ab37d7 Merge pull request #911 from ImranR98/dev
Enable Brazil translation (#830)
2023-09-16 17:41:58 -04:00
c4a3f7333d Enable Brazil translation (#830) 2023-09-16 16:45:06 -04:00
fb820e01b1 Merge pull request #910 from ImranR98/dev
Remove nonsense hint
2023-09-16 16:35:37 -04:00
504aca5287 Merge pull request #908 from oSoWoSo/cz
Add Czech translation
2023-09-16 16:35:25 -04:00
ae7d5546ae Remove nonsense hint 2023-09-16 16:34:15 -04:00
8bcb10b281 Add Czech translation 2023-09-16 22:32:42 +02:00
845eaab73e Merge pull request #909 from ImranR98/dev
Bugfix: HTML Source was broken (could not add apps)
2023-09-16 15:27:11 -04:00
298ff190ec Bugfix: HTML Source was broken (could not add apps) 2023-09-16 15:26:35 -04:00
734a548bc3 Merge pull request #906 from ImranR98/dev
Increment version
2023-09-16 09:28:57 -04:00
591973d97f Increment version 2023-09-16 09:28:45 -04:00
a2f57ecd66 Merge pull request #903 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-16 09:26:34 -04:00
13dcfb479d Merge pull request #902 from gidano/main
Update hu.json
2023-09-16 09:26:28 -04:00
3328d80130 Merge pull request #905 from saymanrifat/saymanrifat-721
Add back button to app detail page (#721)
2023-09-16 09:26:03 -04:00
f9df3ac0e2 Reduce top padding as we no longer need it 2023-09-16 09:25:26 -04:00
61d66e80d8 Add back button to app detail page (#721) 2023-09-16 18:31:57 +06:00
93e0360116 locale(pl): Update Polish translations 2023-09-16 12:45:50 +02:00
2cec5f7934 Update hu.json 2023-09-16 11:28:36 +02:00
8d4a09b007 Merge pull request #894 from markus-gitdev/main
Update de.json
2023-09-16 04:39:06 -04:00
6d8d2fa99d Merge branch 'main' into main 2023-09-16 04:38:48 -04:00
59ecd55530 Merge pull request #895 from DwainZwerg/patch-1
edit de.json
2023-09-16 04:38:05 -04:00
77fcc89e37 Merge branch 'main' into patch-1 2023-09-16 04:35:12 -04:00
7d63da4d9c Merge pull request #893 from bluefly000/japanese-translation
Update ja.json
2023-09-16 04:32:42 -04:00
8eea0bae99 Merge pull request #892 from iDazai/main
Update de.json
2023-09-16 04:32:35 -04:00
f5dcd8f776 Merge pull request #901 from ImranR98/dev
Fix background update issue (#896)
2023-09-16 04:31:29 -04:00
e1f2baeeda Fix background update issue (#896) 2023-09-16 04:31:03 -04:00
84d8ff8b0b Small improvement of the previous translation (especially "" instead of the English ''; my translations of the new features to be translated are mostly based on iDazai (https://github.com/ImranR98/Obtainium/pull/892/files) and markus-gitdev (https://github.com/ImranR98/Obtainium/pull/894/files)) 2023-09-14 08:46:51 +00:00
9862c22fdb Update de.json 2023-09-14 09:27:36 +02:00
8c82f53dd2 Update ja.json 2023-09-14 15:44:27 +09:00
5cf4c21ce3 Update de.json
Translated latest strings
2023-09-14 08:37:46 +02:00
4951f62d4a Merge pull request #890 from ImranR98/dev
- Fix export encoding (#887)
- Fix repeating background install for some apps (#886)
- More version filtering options for F-Droid (#885)
- Trim user URL input
2023-09-13 22:20:49 -04:00
4c0c4b7010 Trim user URL input 2023-09-13 22:19:09 -04:00
96051e614c Merge remote-tracking branch 'origin/main' into dev 2023-09-13 22:17:11 -04:00
a2e494b2ba More version filtering options for F-Droid (#885) 2023-09-13 22:15:10 -04:00
2d5a9bec84 Increment version, update packages 2023-09-13 21:27:18 -04:00
7d9571cfdd Fix repeating background install for some apps (#886) 2023-09-13 21:24:25 -04:00
acc6a780fa Fix export encoding (#887) 2023-09-13 21:01:39 -04:00
0e36d42a06 Merge pull request #863 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-13 19:22:57 -04:00
5cfddd807a locale(pl): Update Polish translations 2023-09-11 22:05:32 +02:00
17b5604f2a Merge pull request #882 from ImranR98/dev
Bugfix
2023-09-10 23:29:38 -04:00
fdb6eed6d0 Slight tweak 2023-09-10 23:28:37 -04:00
13de0437b8 Bugfix 2023-09-10 23:23:35 -04:00
a43c45f310 Merge pull request #881 from ImranR98/dev
Pick export dir + auto-export (#283, #600) + UI fix
2023-09-10 23:15:36 -04:00
9c56a4d1fc Bugs 2023-09-10 23:15:16 -04:00
2aea1d2631 Bugfixes 2023-09-10 23:11:06 -04:00
118e05a0fa Bugfix 2023-09-10 22:47:04 -04:00
05f497787e Build script tweak 2023-09-10 22:37:32 -04:00
53cf4d0234 Pick export dir + auto-export (#283, #600) 2023-09-10 22:35:28 -04:00
6e735b1763 Enable auto-export on update checks 2023-09-10 22:24:18 -04:00
873a1a0683 Allow for alternative app dirs (unfinished) 2023-09-10 15:55:34 -04:00
27b1149d1c Fix padding when touch targets not highlighted 2023-09-10 14:16:19 -04:00
c1e64f111e Merge pull request #878 from ImranR98/dev
Fix Codeberg not finding APKs (#877), Make touch highlight visible in dark theme (#860)
2023-09-10 14:03:48 -04:00
b2af8448fd Increment version 2023-09-10 14:02:26 -04:00
8f44338e76 Make touch highlight visible in dark theme (#860) 2023-09-10 13:56:39 -04:00
e4a55abcb3 Fix Codeberg not finding APKs (#877) 2023-09-10 13:49:04 -04:00
d7348b4973 Merge pull request #871 from ImranR98/dev
Make less obvious target highlighting optional
2023-09-09 07:30:31 -04:00
09421230f2 Make less obvious target highlighting optional 2023-09-09 07:29:35 -04:00
4596e32258 Merge pull request #870 from ImranR98/dev
- Add Support for Private GitHub Repos (#857)
- Add Uptodown Source (#853)
- Add User-Defined Version Extraction for HTML Source (#861)
- Make release notes tap target more obvious (#860)
- Fix Missing Download Notification on App Add (#858)
- Prevent Access Token from Showing in Logs (#714)
2023-09-09 06:54:12 -04:00
4dc007a4f6 Update packages, increment version 2023-09-09 06:50:13 -04:00
c53a156969 Slight UI tweak 2023-09-09 06:48:55 -04:00
94bd0774fb Make release notes tap target more obvious (#860) 2023-09-09 06:00:36 -04:00
b178b1d780 Bugfix 2023-09-09 05:46:56 -04:00
cbc840378c Add Uptodown (#853) 2023-09-09 05:31:04 -04:00
aa7989c16d Custom version extraction for HTML (#861) 2023-09-09 04:41:44 -04:00
85f9336804 Re-add dl notification when adding app (#858) 2023-09-09 03:58:54 -04:00
d66be3ecda Private GitHub repos now work (#857) 2023-09-09 03:55:52 -04:00
c08e05bd6c Trying to use header-based HTTP auth (not working) 2023-09-06 21:30:45 -04:00
e08ab89fd4 Update issue templates 2023-09-06 18:33:04 -04:00
8ba0a0a776 Merge pull request #852 from ImranR98/dev
Fix Aptoide App ID inferral error (#851)
2023-09-04 03:57:56 -04:00
73ed0cea88 Increment version 2023-09-04 03:56:41 -04:00
58a378d212 Fix Aptoide App ID inferral error (#851) 2023-09-04 03:55:13 -04:00
553307ba70 Merge pull request #850 from ImranR98/dev
Auto Select Highest VersionCode APK (F-Droid) (#846)
2023-09-03 16:12:24 -04:00
78f73a9049 Auto Select Highest VersionCode APK (F-Droid) (#846) 2023-09-03 16:11:17 -04:00
abc69e7a0e Merge pull request #849 from ImranR98/dev
Bugfix in HTML Source (#848)
2023-09-03 15:37:43 -04:00
503914dbce Bugfix in HTML Source (#848) 2023-09-03 15:37:26 -04:00
69680f8680 Merge pull request #844 from gidano/main
Update hu.json
2023-09-03 00:15:41 -04:00
1a616aacb7 Merge pull request #847 from ImranR98/dev
Add Aptoide as a Source (#843)
2023-09-03 00:15:01 -04:00
52d19f267e Upgrade packages, increment version 2023-09-03 00:14:23 -04:00
75a8dc9ee4 Add Aptoide as a Source (#843) 2023-09-02 21:28:30 -04:00
8c63920ef6 Update hu.json 2023-09-02 15:41:10 +02:00
dda5619cdc Update issue templates 2023-09-01 11:25:53 -04:00
9cc25fd931 Update issue templates 2023-09-01 11:18:42 -04:00
f5a9eb4295 Update issue templates 2023-09-01 11:13:04 -04:00
ebe256e482 Update issue templates 2023-09-01 11:05:49 -04:00
e215585a64 Merge pull request #840 from ImranR98/dev
Bugfix: BG update fail leads to infinite retries (#838), Group update notifications into one (#829), Temporarily exclude Obtainium from BG updates (#836)
2023-08-30 22:36:31 -04:00
8b01fc03ec Increment version 2023-08-30 22:35:11 -04:00
9af2c8370d Bugfix: BG update fail leads to infinite retries (#838) 2023-08-30 22:34:12 -04:00
01f9003b8d Group update notifications into one (#829) 2023-08-30 22:07:30 -04:00
73a3c7eb71 Temporarily exclude Obtainium from BG updates (#836) 2023-08-30 21:41:26 -04:00
370ec1432e Fix BG update OS requirement 2023-08-30 21:40:05 -04:00
4b63c124bb Merge pull request #835 from ImranR98/dev
VLC: Postpone mirror-dependent request until download time (#821)
2023-08-30 18:43:47 -04:00
3d7df604b1 Upgrade packages 2023-08-30 18:43:03 -04:00
59d1d275bc Increment version 2023-08-30 18:41:55 -04:00
f7f0332b13 VLC: Postpone mirror-dependent request to download time (#821) 2023-08-30 18:41:34 -04:00
917c5f5083 Merge pull request #833 from iDazai/main
Update de.json
2023-08-30 18:14:06 -04:00
370cf05615 Merge pull request #834 from bluefly000/japanese-translation
Update ja.json
2023-08-30 18:13:58 -04:00
101d892c7f Update ja.json 2023-08-30 14:09:11 +09:00
db2faf6511 Update de.json
updated newly added strings, added "APK" for the line "customLinkFilterRegex" which was added in other languages too.
2023-08-30 07:01:06 +02:00
3a83d1bf79 Merge pull request #831 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-08-29 13:06:16 -04:00
070e309aa6 Merge pull request #832 from ImranR98/dev
Disable Brazil language to unblock stuck users (temporary - until fix is found) (#830)
2023-08-29 13:06:05 -04:00
11ca7c3f7d Increment version 2023-08-29 13:05:17 -04:00
7a0acaefb3 Disable BR lang. to unblock users (temp.) (#830) 2023-08-29 13:04:58 -04:00
486f3664ef locale(pl): Update Polish translations 2023-08-29 18:59:54 +02:00
40 changed files with 1722 additions and 599 deletions

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,32 @@
---
name: Bug report
about: Something isn't working right.
title: ''
labels: bug, to check
assignees: ''
---
**Prerequisites**
<!-- Please ensure your request is not part of an existing issue. -->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Steps to reproduce the behavior:
1. Go to '...'
2. Tap on '....'
3. Scroll down to '....'
4. See error -->
**Screenshots and Logs**
<!-- If applicable, add screenshots, logs, and any other artifacts (like some/all files under `/Android/data/dev.imranr.obtainium/`) that you think may help troubleshoot the issue. -->
**Please complete the following information:**
- Device: <!-- [e.g. Pixel 7] -->
- OS: <!-- [e.g. GrapheneOS] -->
- Obtainium Version: <!-- [e.g. 0.14.6-beta] -->
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@ -0,0 +1,29 @@
---
name: Feature request
about: Suggest a new Source, setting, or other feature.
title: ''
labels: enhancement, to check
assignees: ''
---
**Prerequisites**
<!-- Please ensure your request is not part of an existing issue. -->
**Describe the feature**
<!-- A clear and concise description of what you want to happen.
For new Sources, it's preferable (not required) if you suggest how the following details can be extracted from the Source in a reliable way (like an API or through web scraping):
- The App version (or any release-specific identifier - a "pseudo-version") for the latest release
- One or more APK URL(s) for the latest release
- Above details for previous releases (optional)
Note that the Web scraper cannot deal with JavaScript-enabled content. -->
**Describe alternatives you've considered (if applicable)**
<!-- A clear and concise description of any alternative solutions or features you've considered.
Note that app-specific Sources are less likely to be added. In those cases, see if the HTML Source will work for you (if not, see if a generally-applicable enhancement to the HTML Source would work, and suggest that instead). -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

View File

@ -2,30 +2,36 @@
Get Android App Updates Directly From the Source. Get Android App Updates Directly From the Source.
Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available. Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available.
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
Currently supported App sources: Currently supported App sources:
- [GitHub](https://github.com/) - Open Source - General:
- [GitLab](https://gitlab.com/) - [GitHub](https://github.com/)
- [Codeberg](https://codeberg.org/) - [GitLab](https://gitlab.com/)
- [F-Droid](https://f-droid.org/) - [Codeberg](https://codeberg.org/)
- [IzzyOnDroid](https://android.izzysoft.de/) - [F-Droid](https://f-droid.org/)
- [Mullvad](https://mullvad.net/en/) - Third Party F-Droid Repos
- [Signal](https://signal.org/) - [IzzyOnDroid](https://android.izzysoft.de/)
- [SourceForge](https://sourceforge.net/) - [SourceForge](https://sourceforge.net/)
- [SourceHut](https://git.sr.ht/) - [SourceHut](https://git.sr.ht/)
- [APKMirror](https://apkmirror.com/) (Track-Only) - Other - General:
- [APKPure](https://apkpure.com/) - [APKPure](https://apkpure.com/)
- [Huawei AppGallery](https://appgallery.huawei.com/) - [Aptoide](https://aptoide.com/)
- Third Party F-Droid Repos - [Uptodown](https://uptodown.com/)
- Jenkins Jobs - [APKMirror](https://apkmirror.com/) (Track-Only)
- [Steam](https://store.steampowered.com/mobile) - [Huawei AppGallery](https://appgallery.huawei.com/)
- [Telegram App](https://telegram.org) - Jenkins Jobs
- [Neutron Code](https://neutroncode.com) - Open Source - App-Specific:
- "HTML" (Fallback) - [Mullvad](https://mullvad.net/en/)
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked) - [Signal](https://signal.org/)
- [VLC](https://videolan.org/)
- Other - App-Specific:
- [Telegram App](https://telegram.org)
- [Steam Mobile Apps](https://store.steampowered.com/mobile)
- [Neutron Code](https://neutroncode.com)
- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
## Installation ## Installation

View File

@ -12,8 +12,6 @@
"ok": "Dobro", "ok": "Dobro",
"and": "i", "and": "i",
"githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)", "githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)",
"githubPATHint": "PAT mora biti u ovom formatu: korisničko_ime:token",
"githubPATFormat": "korisničko_ime:token",
"includePrereleases": "Uključi preliminarna izdanja", "includePrereleases": "Uključi preliminarna izdanja",
"fallbackToOlderReleases": "Povratak na starija izdanja", "fallbackToOlderReleases": "Povratak na starija izdanja",
"filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu", "filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)", "searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)",
"search": "Pretraživanje", "search": "Pretraživanje",
"additionalOptsFor": "Dodatne opcije za {}", "additionalOptsFor": "Dodatne opcije za {}",
"supportedSourcesBelow": "Podržani izvori:", "supportedSources": "Podržani izvori",
"trackOnlyInBrackets": "(Samo za praćenje)", "trackOnlyInBrackets": "(Samo za praćenje)",
"searchableInBrackets": "(Može se pretraživati)", "searchableInBrackets": "(Može se pretraživati)",
"appsString": "Aplikacije", "appsString": "Aplikacije",
@ -113,7 +111,7 @@
"dark": "Tamna", "dark": "Tamna",
"light": "Svijetla", "light": "Svijetla",
"followSystem": "Pratite sistem", "followSystem": "Pratite sistem",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Koristite čisto crnu tamnu temu", "useBlackTheme": "Koristite čisto crnu tamnu temu",
"appSortBy": "Aplikacije sortirane po", "appSortBy": "Aplikacije sortirane po",
@ -249,7 +247,20 @@
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Verify the 'latest' tag",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"removeAppQuestion": { "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?", "one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?" "other": "Želite li ukloniti aplikacije?"
}, },

319
assets/translations/cs.json Normal file
View File

@ -0,0 +1,319 @@
{
"invalidURLForSource": "Žádná platná {} adresa URL aplikace",
"noReleaseFound": "Nebyla nalezena odpovídající verze",
"noVersionFound": "Nelze určit verzi vydání",
"urlMatchesNoSource": "URL neodpovídá žádnému známému zdroji",
"cantInstallOlderVersion": "Nelze nainstalovat starší verzi aplikace",
"appIdMismatch": "ID staženého balíčku neodpovídá ID existující aplikace",
"functionNotImplemented": "Tato třída nemá implementovánu tuto funkci",
"placeholder": "Zástupce",
"someErrors": "Vyskytly se nějaké chyby",
"unexpectedError": "Neočekávaná chyba",
"ok": "Okay",
"and": "a",
"githubPATLabel": "GitHub Personal Access Token (Raises Rate Limit)",
"includePrereleases": "includepreleases",
"fallbackToOlderReleases": "Fallback to older releases",
"filterReleaseTitlesByRegEx": "Názvy vydání podle regulárního výrazu\filtr",
"invalidRegEx": "Neplatný regulární výraz",
"noDescription": "Žádný popis",
"cancel": "Zrušit",
"continue": "Pokračovat",
"requiredInBracets": "(Required)",
"dropdownNoOptsError": "ERROR: DROPDOWN MUSÍ MÍT AŽ JEDNU MOŽNOST",
"color": "barva",
"githubStarredRepos": "GitHub Starred Repos",
"uname": "username",
"wrongArgNum": "Špatný počet předložených argumentů",
"xIsTrackOnly":"{} je určeno pouze pro sledování",
"source": "zdroj",
"app": "App",
"appsFromSourceAreTrackOnly": "Aplikace z tohoto zdroje jsou 'Jen sledovány'.",
"youPickedTrackOnly": "Vybrali jste možnost 'Jen sledovat'.",
"trackOnlyAppDescription": "Aplikace je sledována kvůli aktualizacím, ale Obtainium ji nebude stahovat ani instalovat.",
"cancelled": "Zrušeno",
"appAlreadyAdded": "Aplikace již přidána",
"alreadyUpToDateQuestion": "App already up to date?",
"addApp": "Přidat aplikaci",
"appSourceURL": "zdrojová adresa URL aplikace",
"error": "Chyba",
"add": "Přidat",
"searchSomeSourcesLabel": "Vyhledávání (pouze konkrétní zdroje)",
"search": "Hledat",
"additionalOptsFor": "Další možnosti pro {}",
"supportedSources": "Podporované zdroje",
"trackOnlyInBrackets": "(Pouze stopy)",
"searchableInBrackets": "(s možností vyhledávání)",
"appsString": "Apky",
"noApps": "Žádné aplikace",
"noAppsForFilter": "žádné aplikace pro vybraný filtr",
"byX": "By {}",
"percentProgress": "Pokrok: {}%",
"pleaseWait": "Počkejte prosím",
"updateAvailable": "Aktualizace je k dispozici",
"estimateInBracketsShort": "(approx.)",
"notInstalled": "Není nainstalováno",
"estimateInBrackets": "(přibližně)",
"selectAll": "Vybrat Vše",
"deselectN": "{} deselected",
"xWillBeRemovedButRemainInstalled":"{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
"removeSelectedAppsQuestion": "Odebrat vybrané aplikace?",
"removeSelectedApps": "Odebrat vybrané aplikace",
"updateX": "Aktualizovat {}",
"installX": "Instalovat {}",
"markXTrackOnlyAsUpdated": "Označit {}\n(Track-Only)\njako aktualizované",
"changeX": "Změnit {}",
"installUpdateApps": "Instalovat/aktualizovat aplikace",
"installUpdateSelectedApps": "Instalovat/aktualizovat vybrané aplikace",
"markXSelectedAppsAsUpdated": "označit {} vybrané aplikace jako aktuální?",
"no": "Ne",
"yes": "ano",
"markSelectedAppsUpdated": "označit vybrané aplikace jako aktuální",
"pinToTop": "Připnout nahoru",
"unpinFromTop": "'Unpin Top'",
"resetInstallStatusForSelectedAppsQuestion": "Obnovit stav instalace vybraných aplikací?",
"installStatusOfXWillBeResetExplanation": "Stav instalace vybraných aplikací bude resetován. To může být užitečné, pokud je verze aplikace zobrazená v Obtainium nesprávná z důvodu neúspěšných aktualizací nebo jiných problémů.",
"shareSelectedAppURLs": "Sdílet adresy URL vybraných aplikací",
"resetInstallStatus": "Obnovení stavu instalace",
"more": "more",
"removeOutdatedFilter": "Odstranit filtr aplikace 'Not Current'",
"showOutdatedOnly": "Zobrazit pouze aplikace, které nejsou aktuální",
"filter": "Filtr",
"filterActive": "Filtr *",
"filterApps": "Filtrovat aplikace",
"appName": "název aplikace",
"author": "Autor",
"upToDateApps": "Apps with current version",
"nonInstalledApps": "Apps not installed",
"importExport": "Import/Export",
"settings": "Nastavení",
"exportedTo": "Exportováno do {}",
"obtainiumExport": "Obtainium Export",
"invalidInput": "Neplatný vstup",
"importedX": "Importováno {}",
"obtainiumImport": "Obtainium Import",
"importFromURLList": "Import ze seznamu URL",
"searchQuery": "Search Query",
"appURLList": "App URL List",
"line": "line",
"searchX": "Search {}",
"noResults": "Nebyly nalezeny žádné výsledky",
"importX": "Import {}",
"importedAppsIdDisclaimer": "Importované aplikace mohou být nesprávně zobrazeny jako \"Neinstalované\". Chcete-li to opravit, nainstalujte je znovu prostřednictvím Obtainium. To nemá vliv na data aplikací. Ovlivňuje pouze metody importu URL a třetích stran.",
"importErrors": "Import Errors",
"importedXOfYApps":"{}importováno {}aplikací.",
"followingURLsHadErrors": "U následujících adres URL došlo k chybám:",
"okay": "Okay",
"selectURL": "Select URL",
"selectURLs": "Select URLs",
"pick": "Vybrat",
"theme": "Téma",
"dark": "Tmavé",
"light": "Světlé",
"followSystem": "Follow System",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Použít čistě černé tmavé téma",
"appSortBy": "Seřadit aplikaci podle",
"authorName": "autor/jméno",
"nameAuthor": "jméno/autor",
"asAdded": "AsAdded",
"appSortOrder": "Sort App By",
"ascending": "Vzestupně",
"descending": "Sestupně",
"bgUpdateCheckInterval": "Background Update Check Interval",
"neverManualOnly": "Nikdy - pouze ručně",
"appearance": "Vzhled",
"showWebInAppView": "Zobrazit zdrojové webové stránky v zobrazení aplikace",
"pinUpdates": "Připnout aplikace s aktualizacemi nahoře",
"updates": "Updates",
"sourceSpecific": "source specific",
"appSource": "zdroj aplikace",
"noLogs": "Žádné protokoly",
"appLogs": "App Logs",
"close": "Zavřít",
"share": "Sdílet",
"appNotFound": "App not found",
"obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "Vybrat APK",
"appHasMoreThanOnePackage":"{} má více než jeden balíček:",
"deviceSupportsXArch": "Vaše zařízení podporuje architekturu CPU {}.",
"deviceSupportsFollowingArchs": "Vaše zařízení podporuje následující architektury CPU:",
"warning": "Varování",
"sourceIsXButPackageFromYPrompt": "The app source is '{}' but the release package is from '{}'. Pokračovat?",
"updatesAvailable": "dostupné aktualizace",
"updatesAvailableNotifDescription": "Upozorňuje uživatele, že jsou k dispozici aktualizace pro jednu nebo více aplikací sledovaných Obtainium",
"noNewUpdates": "Žádné nové aktualizace.",
"xHasAnUpdate":"{} má aktualizaci.",
"appsUpdated": "Aplikace aktualizovány",
"appsUpdatedNotifDescription": "Upozorňuje uživatele, že byly provedeny aktualizace jedné nebo více aplikací na pozadí",
"xWasUpdatedToY":"{} byl aktualizován na {}",
"errorCheckingUpdates": "Chybová kontrola aktualizací",
"errorCheckingUpdatesNotifDescription": "Oznámení zobrazené při neúspěšné kontrole aktualizací na pozadí",
"appsRemoved": "Odstraněné aplikace",
"appsRemovedNotifDescription": "Oznámení uživateli, že jedna nebo více aplikací byly odstraněny z důvodu chyb při načítání",
"xWasRemovedDueToErrorY":"{} byla odstraněna z důvodu následující chyby: {}",
"completeAppInstallation": "Dokončit instalaci aplikace",
"obtainiumMustBeOpenToInstallApps": "Obtainium musí být otevřeno, aby bylo možné instalovat aplikace",
"completeAppInstallationNotifDescription": "Vyzvat uživatele k návratu do Obtainium pro dokončení instalace aplikací",
"checkingForUpdates": "Zkontrolovat aktualizace",
"checkingForUpdatesNotifDescription": "Dočasné oznámení zobrazené při kontrole aktualizací",
"pleaseAllowInstallPerm": "Povolte prosím Obtainium instalovat aplikace",
"trackOnly": "Jen sledovat",
"errorWithHttpStatusCode": "error {}",
"versionCorrectionDisabled": "Oprava verze zakázána (zásuvný modul zřejmě nefunguje)",
"unknown": "Unknown",
"none": "None",
"never": "Nikdy",
"latestVersionX": "Nejnovější verze: {}",
"installedVersionX": "Nainstalovaná verze: {}",
"lastUpdateCheckX": "Poslední kontrola aktualizace: {}",
"remove": "Odebrat",
"yesMarkUpdated": "Ano, označit jako aktualizované",
"fdroid": "F-Droid Official",
"appIdOrName": "App ID or Name",
"appId": "App ID",
"appWithIdOrNameNotFound": "Žádná aplikace s tímto ID nebo názvem nebyla nalezena",
"reposHaveMultipleApps": "Repozitáře mohou obsahovat více aplikací",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"steam": "Steam",
"steamMobile": "Steam Mobile",
"steamChat": "Steam Chat",
"install": "Install",
"markInstalled": "Označit jako nainstalovaný",
"update": "Aktualizovat",
"markUpdated": "Označit jako aktuální",
"additionalOptions": "Additional Options",
"disableVersionDetection": "Zakázat detekci verze",
"noVersionDetectionExplanation": "Tato volba by měla být použita pouze u aplikací, kde detekce verzí nefunguje správně.",
"downloadingX": "download {}",
"downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace",
"noAPKFound": "Žádná APK nebyla nalezena",
"noVersionDetection": "Žádná detekce verze",
"categorize": "Kategorizovat",
"categories": "Kategorie",
"category": "kategorie",
"noCategory": "Žádná kategorie",
"noCategories": "Žádné kategorie",
"deleteCategoriesQuestion": "Smazat kategorie?",
"categoryDeleteWarning": "Všechny aplikace v odstraněných kategoriích budou nastaveny na nekategorizované.",
"addCategory": "přidat kategorii",
"label": "štítek",
"language": "Jazyk",
"copiedToClipboard": "zkopírováno do schránky",
"storagePermissionDenied": "povolení k ukládání odepřeno",
"selectedCategorizeWarning": "Toto nahradí všechna stávající nastavení kategorií pro vybrané aplikace.",
"filterAPKsByRegEx": "Filtrovat APK podle regulárního výrazu",
"removeFromObtainium": "Odebrat z Obtainium",
"uninstallFromDevice": "Odinstalovat ze zařízení",
"onlyWorksWithNonVersionDetectApps": "Funguje pouze pro aplikace s vypnutou detekcí verze.",
"releaseDateAsVersion": "Použít datum vydání jako verzi",
"releaseDateAsVersionExplanation": "Tato možnost by měla být použita pouze u aplikací, u kterých detekce verze nefunguje správně, ale je k dispozici datum vydání.",
"changes": "Změny",
"releaseDate": "datum vydání",
"importFromURLsInFile": "Importovat adresy URL ze souboru (např. OPML)",
"versionDetection": "detekce verze",
"standardVersionDetection": "standardní detekce verze",
"groupByCategory": "Seskupit podle kategorie",
"autoApkFilterByArch": "Pokud je to možné, pokuste se filtrovat soubory APK podle architektury procesoru",
"overrideSource": "Přepsat zdroj",
"dontShowAgain": "Nezobrazovat znovu",
"dontShowTrackOnlyWarnings": "Nezobrazovat varování pro 'Track Only'",
"dontShowAPKOriginWarnings": "Nezobrazovat varování pro původ APK",
"moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
"gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)",
"about": "About",
"requiresCredentialsInSettings": "Vyžaduje další pověření (v nastavení)",
"checkOnStart": "Zkontrolovat jednou při spuštění",
"tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu",
"removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace",
"pickHighestVersionCode": "Automaticky vybrat APK s kódem nejvyšší verze",
"checkUpdateOnDetailPage": "Zkontrolovat aktualizace při otevření stránky s podrobnostmi aplikace",
"disablePageTransitions": "Zakázat animace pro přechody stránek",
"reversePageTransitions": "Obrátit animace pro přechody stránek",
"minStarCount": "Minimální počet hvězdiček",
"addInfoBelow": "Přidat tuto informaci na konec stránky",
"addInfoInSettings": "Přidat tuto informaci do nastavení.",
"githubSourceNote": "Omezení rychlosti GitHub lze obejít pomocí klíče API.",
"gitlabSourceNote": "Extrakce GitLab APK nemusí fungovat bez klíče API",
"sortByFileNamesNotLinks": "Řadit podle názvů souborů místo celých odkazů",
"filterReleaseNotesByRegEx": "Filtrovat poznámky k vydání podle regulárního výrazu",
"customLinkFilterRegex": "Vlastní filtr odkazů APK podle regulárního výrazu (výchozí '.apk$')",
"appsPossiblyUpdated": "Byly provedeny pokusy o aktualizaci aplikací",
"appsPossiblyUpdatedNotifDescription": "Upozorňuje uživatele, že na pozadí mohly být provedeny aktualizace jedné nebo více aplikací",
"xWasPossiblyUpdatedToY":"{} mohlo být aktualizováno na {}.",
"enableBackgroundUpdates": "Povolit aktualizace na pozadí",
"backgroundUpdateReqsExplanation": "Aktualizace na pozadí nemusí být možné pro všechny aplikace.",
"backgroundUpdateLimitsExplanation": "Úspěšnost instalace na pozadí lze určit pouze v případě, že je otevřen Obtainium.",
"verifyLatestTag": "Ověřit značku 'latest'",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
"intermediateLinkNotFound": "Intermediate link not found",
"exemptFromBackgroundUpdates": "Vyloučit aktualizace na pozadí (pokud jsou povoleny)",
"bgUpdatesOnWiFiOnly": "Zakázat aktualizace na pozadí, pokud není přítomna Wi-Fi",
"autoSelectHighestVersionCode": "Automatický výběr nejvyššího kódu verze APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku",
"pickExportDir": "Vybrat adresář pro export",
"autoExportOnChanges": "Automatický export při změnách",
"filterVersionsByRegEx": "Filtrovat verze podle regulárního výrazu",
"trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovaný kód verze APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": {
"one": "Odstranit Apku?",
"other": "Odstranit Apky?"
},
"tooManyRequestsTryAgainInMinutes": {
"one": "Příliš mnoho požadavků (omezená rychlost) - zkuste to znovu za {} minutu",
"other": "Příliš mnoho požadavků (omezená rychlost) - zkuste to znovu za {} minut"
},
"bgUpdateGotErrorRetryInMinutes": {
"one": "Při kontrole aktualizace na pozadí byla zjištěna chyba {}, opakování pokusu bude naplánováno za {} minut",
"other": "Během kontroly aktualizace na pozadí byla zjištěna chyba {}, opakování bude naplánováno za {} minut"
},
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "Při kontrole aktualizací na pozadí nalezena {}aktualizace - v případě potřeby upozorní uživatele",
"other": "Kontrola aktualizací na pozadí našla {} aktualizací - v případě potřeby upozorní uživatele"
},
"apps": {
"one":"{} App",
"other":"{} apps"
},
"url": {
"jedna": "{} URL",
"other": "{} URLs"
},
"minute": {
"one":"{} minute",
"other": "{} minutes"
},
"hour": {
"jedna": "{} hodina",
"other": "{} hours"
},
"day": {
"jedna": "{} den",
"other": "{} dny"
},
"clearedNLogsBeforeXAfterY": {
"one":"{n} log vymazán (před = {před}, po = {po})",
"other": "{n} logů vymazáno (před = {před}, po = {po})"
},
"xAndNMoreUpdatesAvailable": {
"one":"{} a 1 další aplikace mají aktualizace.",
"other":"{} a {} další aplikace mají aktualizace."
},
"xAndNMoreUpdatesInstalled": {
"one":"{} a {} další aplikace mají aktualizace.",
"další":"{} a {} další aplikace byly aktualizovány."
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one":"{} a {} další aplikace byly možná aktualizovány",
"other":"{} a {} další aplikace mohly být aktualizovány."
}
}

View File

@ -12,8 +12,6 @@
"ok": "Okay", "ok": "Okay",
"and": "und", "and": "und",
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)", "githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
"githubPATFormat": "Benutzername:Token",
"includePrereleases": "Vorabversionen einbeziehen", "includePrereleases": "Vorabversionen einbeziehen",
"fallbackToOlderReleases": "Fallback auf ältere Versionen", "fallbackToOlderReleases": "Fallback auf ältere Versionen",
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern", "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
@ -30,8 +28,8 @@
"xIsTrackOnly": "{} ist nur zur Nachverfolgung", "xIsTrackOnly": "{} ist nur zur Nachverfolgung",
"source": "Quelle", "source": "Quelle",
"app": "App", "app": "App",
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind 'Nur Nachverfolgen'.", "appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zum Nachverfolgen.",
"youPickedTrackOnly": "Sie haben die Option 'Nur Nachverfolgen' gewählt.", "youPickedTrackOnly": "Sie haben die Option Nur Nachverfolgen gewählt.",
"trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.", "trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
"cancelled": "Abgebrochen", "cancelled": "Abgebrochen",
"appAlreadyAdded": "App bereits hinzugefügt", "appAlreadyAdded": "App bereits hinzugefügt",
@ -40,10 +38,10 @@
"appSourceURL": "Quell-URL der App", "appSourceURL": "Quell-URL der App",
"error": "Fehler", "error": "Fehler",
"add": "Hinzufügen", "add": "Hinzufügen",
"searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)", "searchSomeSourcesLabel": "Suche (nur für bestimmte Quellen)",
"search": "Suchen", "search": "Suchen",
"additionalOptsFor": "Zusatzoptionen für {}", "additionalOptsFor": "Zusatzoptionen für {}",
"supportedSourcesBelow": "Unterstützte Quellen:", "supportedSources": "Unterstützte Quellen",
"trackOnlyInBrackets": "(Nur Nachverfolgen)", "trackOnlyInBrackets": "(Nur Nachverfolgen)",
"searchableInBrackets": "(Durchsuchbar)", "searchableInBrackets": "(Durchsuchbar)",
"appsString": "Apps", "appsString": "Apps",
@ -72,7 +70,7 @@
"yes": "Ja", "yes": "Ja",
"markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell", "markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell",
"pinToTop": "Oben anheften", "pinToTop": "Oben anheften",
"unpinFromTop": "'Oben anheften' aufheben", "unpinFromTop": "Oben anheften aufheben",
"resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?", "resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?",
"installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.", "installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.",
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen", "shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
@ -177,7 +175,7 @@
"appId": "App ID", "appId": "App ID",
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden", "appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten", "reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo", "fdroidThirdPartyRepo": "F-Droid Drittparteienrepo",
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
@ -240,15 +238,28 @@
"gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel", "gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel",
"sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links", "sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links",
"filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern", "filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern",
"customLinkFilterRegex": "Benutzerdefinierter Link Filter nach Regulärem Ausdruck (Standard '.apk$')", "customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted", "appsPossiblyUpdated": "App Aktualisierungen wurden versucht",
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", "appsPossiblyUpdatedNotifDescription": "Benachrichtigt den Benutzer, dass Updates für eine oder mehrere Apps möglicherweise im Hintergrund durchgeführt wurden",
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.", "xWasPossiblyUpdatedToY": "{} wurde möglicherweise aktualisiert auf {}.",
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", "backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.",
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Überprüfe das „latest“ Tag",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
"autoSelectHighestVersionCode": "Automatisch höchste APK-Code-Version auswählen",
"versionExtractionRegEx": "Versions-Extraktion RegEx",
"matchGroupToUse": "Zu verwendende Gruppe abgleichen",
"highlightTouchTargets": "Weniger offensichtliche Ziele hervorheben",
"pickExportDir": "Export-Verzeichnis wählen",
"autoExportOnChanges": "Automatischer Export bei Änderung",
"filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
"trySelectingSuggestedVersionCode": "Versuchen, die vorgeschlagene APK-Code-Version auszuwählen",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "App entfernen?", "one": "App entfernen?",
"other": "Apps entfernen?" "other": "Apps entfernen?"
@ -298,7 +309,7 @@
"other": "{} und {} weitere Anwendungen wurden aktualisiert." "other": "{} und {} weitere Anwendungen wurden aktualisiert."
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.", "one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.",
"other": "{} and {} more apps may have been updated." "other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert."
} }
} }

View File

@ -12,8 +12,6 @@
"ok": "Okay", "ok": "Okay",
"and": "and", "and": "and",
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)", "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
"githubPATHint": "PAT must be in this format: username:token",
"githubPATFormat": "username:token",
"includePrereleases": "Include prereleases", "includePrereleases": "Include prereleases",
"fallbackToOlderReleases": "Fallback to older releases", "fallbackToOlderReleases": "Fallback to older releases",
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Search (Some Sources Only)", "searchSomeSourcesLabel": "Search (Some Sources Only)",
"search": "Search", "search": "Search",
"additionalOptsFor": "Additional Options for {}", "additionalOptsFor": "Additional Options for {}",
"supportedSourcesBelow": "Supported Sources:", "supportedSources": "Supported Sources",
"trackOnlyInBrackets": "(Track-Only)", "trackOnlyInBrackets": "(Track-Only)",
"searchableInBrackets": "(Searchable)", "searchableInBrackets": "(Searchable)",
"appsString": "Apps", "appsString": "Apps",
@ -252,6 +250,19 @@
"intermediateLinkNotFound": "Intermediate link not found", "intermediateLinkNotFound": "Intermediate link not found",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remove App?", "one": "Remove App?",
"other": "Remove Apps?" "other": "Remove Apps?"

View File

@ -12,8 +12,6 @@
"ok": "Correcto", "ok": "Correcto",
"and": "y", "and": "y",
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)", "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
"githubPATFormat": "nombre_de_usuario:token",
"includePrereleases": "Incluir versiones preliminares", "includePrereleases": "Incluir versiones preliminares",
"fallbackToOlderReleases": "Retorceder a versiones previas", "fallbackToOlderReleases": "Retorceder a versiones previas",
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)", "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
"search": "Buscar", "search": "Buscar",
"additionalOptsFor": "Opciones Adicionales para {}", "additionalOptsFor": "Opciones Adicionales para {}",
"supportedSourcesBelow": "Fuentes Soportadas:", "supportedSources": "Fuentes Soportadas",
"trackOnlyInBrackets": "(Solo Seguimiento)", "trackOnlyInBrackets": "(Solo Seguimiento)",
"searchableInBrackets": "(Soporta Búsquedas)", "searchableInBrackets": "(Soporta Búsquedas)",
"appsString": "Aplicaciones", "appsString": "Aplicaciones",
@ -249,6 +247,19 @@
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Verify the 'latest' tag",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "¿Eliminar Aplicación?", "one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?" "other": "¿Eliminar Aplicaciones?"

View File

@ -12,8 +12,6 @@
"ok": "باشه", "ok": "باشه",
"and": "و", "and": "و",
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
"githubPATHint": "PAT باید در این قالب باشد: username:token",
"githubPATFormat": "username:token",
"includePrereleases": "شامل نسخه های اولیه", "includePrereleases": "شامل نسخه های اولیه",
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر", "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید", "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "جستجو (فقط برخی منابع)", "searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
"search": "جستجو کردن", "search": "جستجو کردن",
"additionalOptsFor": "گزینه های اضافی برای {}", "additionalOptsFor": "گزینه های اضافی برای {}",
"supportedSourcesBelow": "منابع پشتیبانی شده:", "supportedSources": "منابع پشتیبانی شده",
"trackOnlyInBrackets": "«فقط ردیابی»", "trackOnlyInBrackets": "«فقط ردیابی»",
"searchableInBrackets": "(قابل جستجو)", "searchableInBrackets": "(قابل جستجو)",
"appsString": "برنامه ها", "appsString": "برنامه ها",
@ -249,6 +247,19 @@
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Verify the 'latest' tag",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "برنامه حذف شود؟", "one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟" "other": "برنامه ها حذف شوند؟"

View File

@ -12,8 +12,6 @@
"ok": "Okay", "ok": "Okay",
"and": "et", "and": "et",
"githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)", "githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)",
"githubPATHint": "Le JAP doit être dans ce format : username:token",
"githubPATFormat": "username:token",
"includePrereleases": "Inclure les avant-premières", "includePrereleases": "Inclure les avant-premières",
"fallbackToOlderReleases": "Retour aux anciennes versions", "fallbackToOlderReleases": "Retour aux anciennes versions",
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière", "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)", "searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)",
"search": "Rechercher", "search": "Rechercher",
"additionalOptsFor": "Options supplémentaires pour {}", "additionalOptsFor": "Options supplémentaires pour {}",
"supportedSourcesBelow": "Sources prises en charge :", "supportedSources": "Sources prises en charge ",
"trackOnlyInBrackets": "(Suivi uniquement)", "trackOnlyInBrackets": "(Suivi uniquement)",
"searchableInBrackets": "(Recherchable)", "searchableInBrackets": "(Recherchable)",
"appsString": "Applications", "appsString": "Applications",
@ -249,6 +247,19 @@
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Verify the 'latest' tag",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Supprimer l'application ?", "one": "Supprimer l'application ?",
"other": "Supprimer les applications ?" "other": "Supprimer les applications ?"

View File

@ -12,8 +12,6 @@
"ok": "Oké", "ok": "Oké",
"and": "és", "and": "és",
"githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)", "githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)",
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
"githubPATFormat": "felhasználónév:token",
"includePrereleases": "Tartalmazza az előzetes kiadásokat", "includePrereleases": "Tartalmazza az előzetes kiadásokat",
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz", "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel", "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Keresés (csak egyes források)", "searchSomeSourcesLabel": "Keresés (csak egyes források)",
"search": "Keresés", "search": "Keresés",
"additionalOptsFor": "További lehetőségek a következőhöz: {}", "additionalOptsFor": "További lehetőségek a következőhöz: {}",
"supportedSourcesBelow": "Támogatott források:", "supportedSources": "Támogatott források",
"trackOnlyInBrackets": "(Csak nyomonkövetés)", "trackOnlyInBrackets": "(Csak nyomonkövetés)",
"searchableInBrackets": "(Kereshető)", "searchableInBrackets": "(Kereshető)",
"appsString": "Appok", "appsString": "Appok",
@ -87,7 +85,7 @@
"author": "Szerző", "author": "Szerző",
"upToDateApps": "Naprakész appok", "upToDateApps": "Naprakész appok",
"nonInstalledApps": "Nem telepített appok", "nonInstalledApps": "Nem telepített appok",
"importExport": "Importálás/Exportálás", "importExport": "Import/Export",
"settings": "Beállítások", "settings": "Beállítások",
"exportedTo": "Exportálva ide {}", "exportedTo": "Exportálva ide {}",
"obtainiumExport": "Obtainium Adat Exportálás", "obtainiumExport": "Obtainium Adat Exportálás",
@ -137,7 +135,7 @@
"share": "Megosztás", "share": "Megosztás",
"appNotFound": "App nem található", "appNotFound": "App nem található",
"obtainiumExportHyphenatedLowercase": "obtainium-export", "obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "Válasszon egy APK-ot", "pickAnAPK": "Válasszon egy APK-t",
"appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:", "appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:",
"deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.", "deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.",
"deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:", "deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:",
@ -204,7 +202,7 @@
"copiedToClipboard": "Másolva a vágólapra", "copiedToClipboard": "Másolva a vágólapra",
"storagePermissionDenied": "Tárhely engedély megtagadva", "storagePermissionDenied": "Tárhely engedély megtagadva",
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
"filterAPKsByRegEx": "Az APK-ok szűrése reguláris kifejezéssel", "filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
"removeFromObtainium": "Eltávolítás az Obtainiumból", "removeFromObtainium": "Eltávolítás az Obtainiumból",
"uninstallFromDevice": "Eltávolítás a készülékrő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.", "onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.",
@ -239,15 +237,29 @@
"gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.", "gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.",
"sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett", "sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett",
"filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel", "filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel",
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", "customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted", "appsPossiblyUpdated": "App frissítési kísérlet",
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", "appsPossiblyUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több alkalmazás frissítése lehetséges a háttérben",
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.", "xWasPossiblyUpdatedToY": "{} frissítve lehet erre {}.",
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", "backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", "backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
"versionExtractionRegEx": "Verzió kibontása reguláris kifejezéssel",
"matchGroupToUse": "Párosítsa a csoportot a használathoz",
"highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
"pickExportDir": "Válassza az Exportálási könyvtárat",
"autoExportOnChanges": "Auto-exportálás a változások után",
"filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
"dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",
"reverseSort": "Fordított rendezés",
"debugMenu": "Hibakereső menü",
"bgTaskStarted": "A háttérfeladat elindult ellenőrizze a naplókat.",
"enableBackgroundUpdates": "Frissítések a háttérben",
"runBgCheckNow": "Futtassa a Háttérben frissítés ellenőrzését most",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?", "one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?" "other": "Eltávolítja az alkalmazást?"
@ -297,7 +309,7 @@
"other": "{} és {} további alkalmazás frissítve." "other": "{} és {} további alkalmazás frissítve."
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.", "one": "{} és 1 további alkalmazás is frissült.",
"other": "{} and {} more apps may have been updated." "other": "{} és {} további alkalmazás is frissült."
} }
} }

View File

@ -12,8 +12,6 @@
"ok": "Va bene", "ok": "Va bene",
"and": "e", "and": "e",
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)", "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
"githubPATHint": "PAT deve seguire questo formato: nomeutente:token",
"githubPATFormat": "nomeutente:token",
"includePrereleases": "Includi prerelease", "includePrereleases": "Includi prerelease",
"fallbackToOlderReleases": "Ripiega su release precedenti", "fallbackToOlderReleases": "Ripiega su release precedenti",
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari", "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Cerca (solo per alcune fonti)", "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
"search": "Cerca", "search": "Cerca",
"additionalOptsFor": "Opzioni aggiuntive per {}", "additionalOptsFor": "Opzioni aggiuntive per {}",
"supportedSourcesBelow": "Fonti supportate:", "supportedSources": "Fonti supportate",
"trackOnlyInBrackets": "(Solo-Monitoraggio)", "trackOnlyInBrackets": "(Solo-Monitoraggio)",
"searchableInBrackets": "(ricercabile)", "searchableInBrackets": "(ricercabile)",
"appsString": "App", "appsString": "App",
@ -249,6 +247,19 @@
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Verify the 'latest' tag",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Rimuovere l'app?", "one": "Rimuovere l'app?",
"other": "Rimuovere le app?" "other": "Rimuovere le app?"

View File

@ -12,8 +12,6 @@
"ok": "OK", "ok": "OK",
"and": "と", "and": "と",
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
"githubPATFormat": "ユーザー名:トークン",
"includePrereleases": "プレリリースを含む", "includePrereleases": "プレリリースを含む",
"fallbackToOlderReleases": "旧リリースへのフォールバック", "fallbackToOlderReleases": "旧リリースへのフォールバック",
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする", "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "検索 (一部ソースのみ)", "searchSomeSourcesLabel": "検索 (一部ソースのみ)",
"search": "検索", "search": "検索",
"additionalOptsFor": "{}の追加オプション", "additionalOptsFor": "{}の追加オプション",
"supportedSourcesBelow": "対応するソース:", "supportedSources": "対応するソース",
"trackOnlyInBrackets": "(追跡のみ)", "trackOnlyInBrackets": "(追跡のみ)",
"searchableInBrackets": "(検索可能)", "searchableInBrackets": "(検索可能)",
"appsString": "アプリ", "appsString": "アプリ",
@ -146,7 +144,7 @@
"updatesAvailable": "アップデートが利用可能", "updatesAvailable": "アップデートが利用可能",
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する", "updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
"noNewUpdates": "新しいアップデートはありません", "noNewUpdates": "新しいアップデートはありません",
"xHasAnUpdate": "{} のアップデートが利用可能です", "xHasAnUpdate": "{} のアップデートが利用可能です",
"appsUpdated": "アプリをアップデートしました", "appsUpdated": "アプリをアップデートしました",
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する", "appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
"xWasUpdatedToY": "{} が {} にアップデートされました", "xWasUpdatedToY": "{} が {} にアップデートされました",
@ -243,13 +241,28 @@
"customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')", "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
"appsPossiblyUpdated": "アプリのアップデートを試行", "appsPossiblyUpdated": "アプリのアップデートを試行",
"appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する", "appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する",
"xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります", "xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります",
"enableBackgroundUpdates": "バックグラウンドアップデートを有効化する", "enableBackgroundUpdates": "バックグラウンドアップデートを有効化する",
"backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。", "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
"backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。", "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
"verifyLatestTag": "'latest'タグを確認する", "verifyLatestTag": "'latest'タグを確認する",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "intermediateLinkNotFound": "中間リンクが見つかりませんでした",
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合バックグラウンドアップデートを無効にする",
"autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
"versionExtractionRegEx": "バージョン抽出の正規表現",
"matchGroupToUse": "使用するマッチしたグループ",
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
"pickExportDir": "エクスポートディレクトリを選択",
"autoExportOnChanges": "変更があった際に自動でエクスポートする",
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "アプリを削除しますか?", "one": "アプリを削除しますか?",
"other": "アプリを削除しますか?" "other": "アプリを削除しますか?"
@ -291,15 +304,15 @@
"other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})" "other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})"
}, },
"xAndNMoreUpdatesAvailable": { "xAndNMoreUpdatesAvailable": {
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です", "one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です" "other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
}, },
"xAndNMoreUpdatesInstalled": { "xAndNMoreUpdatesInstalled": {
"one": "{} とさらに {} 個のアプリがアップデートされました", "one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{} とさらに {} 個のアプリがアップデートされました" "other": "{} とさらに {} 個のアプリがアップデートされました"
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります", "one": "{} とさらに 1 個のアプリがアップデートされた可能性があります",
"other": "{} とさらに {} 個のアプリがアップデートされた化膿性があります" "other": "{} とさらに {} 個のアプリがアップデートされた可能性があります"
} }
} }

View File

@ -22,8 +22,6 @@
"ok": "Okej", "ok": "Okej",
"and": "i", "and": "i",
"githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)", "githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)",
"githubPATHint": "Wymagany format: użytkownik:token",
"githubPATFormat": "użytkownik:token",
"includePrereleases": "Uwzględnij wersje wstępne", "includePrereleases": "Uwzględnij wersje wstępne",
"fallbackToOlderReleases": "Powracaj do starszych wersji", "fallbackToOlderReleases": "Powracaj do starszych wersji",
"filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych", "filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych",
@ -36,10 +34,10 @@
"githubStarredRepos": "Repozytoria GitHub oznaczone gwiazdką", "githubStarredRepos": "Repozytoria GitHub oznaczone gwiazdką",
"uname": "Nazwa użytkownika", "uname": "Nazwa użytkownika",
"wrongArgNum": "Nieprawidłowa liczba podanych argumentów", "wrongArgNum": "Nieprawidłowa liczba podanych argumentów",
"xIsTrackOnly": "{} jest tylko obserwowana", "xIsTrackOnly": "{} jest tylko obserwowane",
"source": "Źródło", "source": "Źródło",
"app": "Aplikacja", "app": "Aplikacja",
"appsFromSourceAreTrackOnly": "Aplikacje z tego źródła są „Obserwowane”.", "appsFromSourceAreTrackOnly": "Aplikacje z tego źródła są „tylko obserwowane”.",
"youPickedTrackOnly": "Wybrano opcję „Tylko obserwuj”.", "youPickedTrackOnly": "Wybrano opcję „Tylko obserwuj”.",
"trackOnlyAppDescription": "Aplikacja będzie obserwowana pod kątem aktualizacji, ale Obtainium nie będzie w stanie jej pobrać ani zainstalować.", "trackOnlyAppDescription": "Aplikacja będzie obserwowana pod kątem aktualizacji, ale Obtainium nie będzie w stanie jej pobrać ani zainstalować.",
"cancelled": "Anulowano", "cancelled": "Anulowano",
@ -52,9 +50,9 @@
"searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)", "searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)",
"search": "Szukaj", "search": "Szukaj",
"additionalOptsFor": "Dodatkowe opcje dla {}", "additionalOptsFor": "Dodatkowe opcje dla {}",
"supportedSourcesBelow": "Obsługiwane źródła:", "supportedSources": "Obsługiwane źródła",
"trackOnlyInBrackets": "(tylko obserwowane)", "trackOnlyInBrackets": "(tylko obserwowane)",
"searchableInBrackets": "(Wyszukiwalne)", "searchableInBrackets": "(wyszukiwalne)",
"appsString": "Aplikacje", "appsString": "Aplikacje",
"noApps": "Brak aplikacji", "noApps": "Brak aplikacji",
"noAppsForFilter": "Brak aplikacji dla filtra", "noAppsForFilter": "Brak aplikacji dla filtra",
@ -72,7 +70,7 @@
"removeSelectedApps": "Usuń wybrane aplikacje", "removeSelectedApps": "Usuń wybrane aplikacje",
"updateX": "Zaktualizuj {}", "updateX": "Zaktualizuj {}",
"installX": "Zainstaluj {}", "installX": "Zainstaluj {}",
"markXTrackOnlyAsUpdated": "Oznacz {}\n(Tylko obserwowana)\njako zaktualizowaną", "markXTrackOnlyAsUpdated": "Oznacz {}\n(tylko obserwowana)\njako zaktualizowaną",
"changeX": "Zmień {}", "changeX": "Zmień {}",
"installUpdateApps": "Instaluj/aktualizuj aplikacje", "installUpdateApps": "Instaluj/aktualizuj aplikacje",
"installUpdateSelectedApps": "Zainstaluj/zaktualizuj wybrane aplikacje", "installUpdateSelectedApps": "Zainstaluj/zaktualizuj wybrane aplikacje",
@ -246,15 +244,28 @@
"gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.", "gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.",
"sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków", "sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków",
"filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego", "filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego",
"customLinkFilterRegex": "Niestandardowy filtr linków wg. wyrażenia regularnego (domyślnie \".apk$\")", "customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
"appsPossiblyUpdated": "Informuj o próbach aktualizacji", "appsPossiblyUpdated": "Informuj o próbach aktualizacji",
"appsPossiblyUpdatedNotifDescription": "Powiadamiaj o potencjalnym zastosowaniu w tle aktualizacji jednej lub większej ilości aplikacji", "appsPossiblyUpdatedNotifDescription": "Powiadamiaj o potencjalnym zastosowaniu w tle aktualizacji jednej lub większej ilości aplikacji",
"xWasPossiblyUpdatedToY": "{} być może zaktualizowano do {}.", "xWasPossiblyUpdatedToY": "{} być może zaktualizowano do {}.",
"backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.", "backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.",
"backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.", "backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.",
"verifyLatestTag": "Zweryfikuj najnowszy tag", "verifyLatestTag": "Zweryfikuj najnowszy tag",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
"autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
"versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję",
"matchGroupToUse": "Dopasuj grupę do użycia",
"highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
"pickExportDir": "Wybierz katalog eksportu",
"autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
"filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
"trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
"dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API",
"reverseSort": "Odwrotne sortowanie",
"debugMenu": "Menu debugowania",
"bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.",
"runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Usunąć aplikację?", "one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?", "few": "Usunąć aplikacje?",
@ -327,5 +338,7 @@
"many": "{} i {} innych apek mogło zostać zaktualizowanych.", "many": "{} i {} innych apek mogło zostać zaktualizowanych.",
"other": "{} i {} inne apki mogły zostać zaktualizowane." "other": "{} i {} inne apki mogły zostać zaktualizowane."
}, },
"enableBackgroundUpdates": "Włącz aktualizacje w tle" "enableBackgroundUpdates": "Włącz aktualizacje w tle",
"intermediateLinkRegex": "Filtr linków \"pośrednich\" do odwiedzenia w pierwszej kolejności",
"intermediateLinkNotFound": "Nie znaleziono linku pośredniego"
} }

View File

@ -12,8 +12,6 @@
"ok": "Ok", "ok": "Ok",
"and": "e", "and": "e",
"githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)", "githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)",
"githubPATHint": "O TAP deve estar nesse formato: usuario:token",
"githubPATFormat": "usuario:token",
"includePrereleases": "Incluir pré-lançamentos", "includePrereleases": "Incluir pré-lançamentos",
"fallbackToOlderReleases": "Retornar para versões anteriores", "fallbackToOlderReleases": "Retornar para versões anteriores",
"filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular", "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)", "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)",
"search": "Procurar", "search": "Procurar",
"additionalOptsFor": "Opções Adicionais para {}", "additionalOptsFor": "Opções Adicionais para {}",
"supportedSourcesBelow": "Fontes Compatíveis:", "supportedSources": "Fontes Compatíveis",
"trackOnlyInBrackets": "(Apenas Seguir)", "trackOnlyInBrackets": "(Apenas Seguir)",
"searchableInBrackets": "(Pesquisável)", "searchableInBrackets": "(Pesquisável)",
"appsString": "Apps", "appsString": "Apps",
@ -130,7 +128,7 @@
"pinUpdates": "Fixar atualizações no topo da visão de Apps", "pinUpdates": "Fixar atualizações no topo da visão de Apps",
"updates": "Atualizações", "updates": "Atualizações",
"sourceSpecific": "Específico a fonte", "sourceSpecific": "Específico a fonte",
"appSource": "Fonte de Apps", "appSource": "Fonte do App",
"noLogs": "Sem Logs", "noLogs": "Sem Logs",
"appLogs": "Logs do App", "appLogs": "Logs do App",
"close": "Fechar", "close": "Fechar",
@ -252,6 +250,19 @@
"intermediateLinkNotFound": "Link intermediário não encontrado", "intermediateLinkNotFound": "Link intermediário não encontrado",
"exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)", "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
"bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi", "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
"autoSelectHighestVersionCode": "Auto-selecionar o maior codigo de versão",
"versionExtractionRegEx": "RegEx para Extração de Versão",
"matchGroupToUse": "Grupo de Seleção para Usar",
"highlightTouchTargets": "Destaque areas de toque menos óbvias",
"pickExportDir": "Escolher Diretorio de Exportação",
"autoExportOnChanges": "Auto-exportar em mudanças",
"filterVersionsByRegEx": "Filtrar Versões por Expressão Regular",
"trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida",
"dontSortReleasesList": "Reter a ordem de lançamento da API",
"reverseSort": "Ordenação reversa",
"debugMenu": "Menu Debug",
"bgTaskStarted": "Tarefa em segundo plano iniciada - verifique os logs.",
"runBgCheckNow": "Execute a verificação de atualização em segundo plano agora",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remover App?", "one": "Remover App?",
"other": "Remover Apps?" "other": "Remover Apps?"

View File

@ -12,8 +12,6 @@
"ok": "Окей", "ok": "Окей",
"and": "и", "and": "и",
"githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
"githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен",
"githubPATFormat": "имя_пользователя:токен",
"includePrereleases": "Включить предварительные релизы", "includePrereleases": "Включить предварительные релизы",
"fallbackToOlderReleases": "Откатиться к более старым версиям", "fallbackToOlderReleases": "Откатиться к более старым версиям",
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения", "filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Поиск (только в некоторых источниках)", "searchSomeSourcesLabel": "Поиск (только в некоторых источниках)",
"search": "Поиск", "search": "Поиск",
"additionalOptsFor": "Дополнительные опции для {}", "additionalOptsFor": "Дополнительные опции для {}",
"supportedSourcesBelow": "Поддерживаемые источники:", "supportedSources": "Поддерживаемые источники",
"trackOnlyInBrackets": "(Только для отслеживания)", "trackOnlyInBrackets": "(Только для отслеживания)",
"searchableInBrackets": "(Поиск)", "searchableInBrackets": "(Поиск)",
"appsString": "Приложения", "appsString": "Приложения",
@ -238,17 +236,33 @@
"addInfoInSettings": "Добавьте эту информацию в Настройки.", "addInfoInSettings": "Добавьте эту информацию в Настройки.",
"githubSourceNote": "Лимит запросов GitHub можно обойти, используя ключ API.", "githubSourceNote": "Лимит запросов GitHub можно обойти, используя ключ API.",
"gitlabSourceNote": "Извлечение APK из GitLab может не работать без ключа API.", "gitlabSourceNote": "Извлечение APK из GitLab может не работать без ключа API.",
"sortByFileNamesNotLinks": "Sort by file names instead of full links", "sortByFileNamesNotLinks": "Сортировать по именам файлов, а не по полным ссылкам",
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", "filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску по регулярному выражению",
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", "customLinkFilterRegex": "Пользовательский фильтр ссылок APK по регулярному выражению (по умолчанию '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted", "appsPossiblyUpdated": "Попытки обновления приложений",
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", "appsPossiblyUpdatedNotifDescription": "Уведомляет пользователя о возможных обновлениях одного или нескольких приложений в фоновом режиме",
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.", "xWasPossiblyUpdatedToY": "{} возможно был обновлен до {}.",
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", "enableBackgroundUpdates": "Включить обновления в фоне",
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", "backgroundUpdateReqsExplanation": "Фоновые обновления могут быть невозможны для всех приложений.",
"verifyLatestTag": "Verify the 'latest' tag", "backgroundUpdateLimitsExplanation": "Успех фоновой установки можно определить только после открытия Obtainium.",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "verifyLatestTag": "Проверьте тег 'последний'",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "intermediateLinkRegex": "Фильтр ссылок 'промежуточного' типа для приоритетного посещения",
"intermediateLinkNotFound": "Промежуточная ссылка не найдена",
"exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
"bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет соединения с Wi-Fi",
"autoSelectHighestVersionCode": "Автоматически выбирать APK с наивысшим кодом версии",
"versionExtractionRegEx": "Регулярное выражение для извлечения версии",
"matchGroupToUse": "Выберите группу для использования",
"highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
"pickExportDir": "Выбрать каталог для экспорта",
"autoExportOnChanges": "Автоэкспорт при изменениях",
"filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
"trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
"dontSortReleasesList": "Сохранить порядок выпусков от API",
"reverseSort": "Обратная сортировка",
"debugMenu": "Меню Отладки",
"bgTaskStarted": "Фоновая задача начата - проверьте журналы.",
"runBgCheckNow": "Запустить проверку фонового обновления сейчас",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Удалить приложение?", "one": "Удалить приложение?",
"other": "Удалить приложения?" "other": "Удалить приложения?"
@ -294,11 +308,11 @@
"other": "У {} и ещё {} приложений есть обновления." "other": "У {} и ещё {} приложений есть обновления."
}, },
"xAndNMoreUpdatesInstalled": { "xAndNMoreUpdatesInstalled": {
"one": "{} и еще 1 приложение были обновлены.", "one": "{} и ещё 1 приложение были обновлены.",
"other": "{} и еще {} приложений были обновлены." "other": "{} и ещё {} приложений были обновлены."
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.", "one": "{} и ещё 1 приложение могли быть обновлены.",
"other": "{} and {} more apps may have been updated." "other": "{} и ещё {} приложений могли быть обновлены."
} }
} }

View File

@ -12,8 +12,6 @@
"ok": "好的", "ok": "好的",
"and": "和", "and": "和",
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
"githubPATFormat": "username:token",
"includePrereleases": "包含预发行版", "includePrereleases": "包含预发行版",
"fallbackToOlderReleases": "将旧发行版作为备选", "fallbackToOlderReleases": "将旧发行版作为备选",
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题", "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "搜索(仅支持部分来源)", "searchSomeSourcesLabel": "搜索(仅支持部分来源)",
"search": "搜索", "search": "搜索",
"additionalOptsFor": "{} 的更多选项", "additionalOptsFor": "{} 的更多选项",
"supportedSourcesBelow": "支持的来源", "supportedSources": "支持的来源",
"trackOnlyInBrackets": "(仅追踪)", "trackOnlyInBrackets": "(仅追踪)",
"searchableInBrackets": "(可搜索)", "searchableInBrackets": "(可搜索)",
"appsString": "应用列表", "appsString": "应用列表",
@ -53,9 +51,9 @@
"percentProgress": "进度:{}%", "percentProgress": "进度:{}%",
"pleaseWait": "请稍候", "pleaseWait": "请稍候",
"updateAvailable": "更新可用", "updateAvailable": "更新可用",
"estimateInBracketsShort": "(推测)", "estimateInBracketsShort": "推测",
"notInstalled": "未安装", "notInstalled": "未安装",
"estimateInBrackets": "(推测)", "estimateInBrackets": "推测",
"selectAll": "全选", "selectAll": "全选",
"deselectN": "取消选择 {}", "deselectN": "取消选择 {}",
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。", "xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
@ -248,8 +246,23 @@
"backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。", "backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。",
"backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。", "backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。",
"verifyLatestTag": "验证“Latest”标签", "verifyLatestTag": "验证“Latest”标签",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "intermediateLinkRegex": "首先访问“中间”链接的过滤器",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "intermediateLinkNotFound": "中间链接未找到",
"exemptFromBackgroundUpdates": "禁用后台更新(如果全局设置启用)",
"bgUpdatesOnWiFiOnly": "不在连接 WiFi 时禁用后台更新",
"autoSelectHighestVersionCode": "自动选择最高版本号 APK",
"versionExtractionRegEx": "版本提取正则表达式",
"matchGroupToUse": "匹配要使用的组",
"highlightTouchTargets": "突出显示不明显的触摸目标",
"pickExportDir": "选择导出目录",
"autoExportOnChanges": "修改时自动导出",
"filterVersionsByRegEx": "使用正则表达式筛选版本",
"trySelectingSuggestedVersionCode": "尝试选择推荐版本 APK",
"dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting",
"debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now",
"removeAppQuestion": { "removeAppQuestion": {
"one": "是否删除应用?", "one": "是否删除应用?",
"other": "是否删除应用?" "other": "是否删除应用?"

View File

@ -4,7 +4,9 @@
CURR_DIR="$(pwd)" CURR_DIR="$(pwd)"
trap "cd "$CURR_DIR"" EXIT trap "cd "$CURR_DIR"" EXIT
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date if [ -z "$1" ]; then
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
fi
rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any
flutter build apk && flutter build apk --split-per-abi # Build (both split and combined APKs) flutter build apk && flutter build apk --split-per-abi # Build (both split and combined APKs)
for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures

View File

@ -25,12 +25,16 @@ class APKCombo extends AppSource {
} }
@override @override
Map<String, String> get requestHeaders => { Future<Map<String, String>?> getRequestHeaders(
"User-Agent": "curl/8.0.1", {Map<String, dynamic> additionalSettings = const <String, dynamic>{},
"Accept": "*/*", bool forAPKDownload = false}) async {
"Connection": "keep-alive", return {
"Host": "$host" "User-Agent": "curl/8.0.1",
}; "Accept": "*/*",
"Connection": "keep-alive",
"Host": "$host"
};
}
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async { Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
var res = await sourceRequest('$standardUrl/download/apk'); var res = await sourceRequest('$standardUrl/download/apk');

View File

@ -3,19 +3,37 @@ import 'package:html/parser.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
parseDateTimeMMMddCommayyyy(String? dateString) {
DateTime? releaseDate;
try {
releaseDate = dateString != null
? DateFormat('MMM dd, yyyy').parse(dateString)
: null;
releaseDate = dateString != null && releaseDate == null
? DateFormat('MMMM dd, yyyy').parse(dateString)
: releaseDate;
} catch (err) {
// ignore
}
return releaseDate;
}
class APKPure extends AppSource { class APKPure extends AppSource {
APKPure() { APKPure() {
host = 'apkpure.com'; host = 'apkpure.com';
allowSubDomains = true;
} }
@override @override
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegExB = RegExp('^https?://m.$host/+[^/]+/+[^/]+'); RegExp standardUrlRegExB =
RegExp('^https?://m.$host/+[^/]+/+[^/]+(/+[^/]+)?');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
if (match != null) { if (match != null) {
url = 'https://$host/${Uri.parse(url).path}'; url = 'https://$host${Uri.parse(url).path}';
} }
RegExp standardUrlRegExA = RegExp('^https?://$host/+[^/]+/+[^/]+'); RegExp standardUrlRegExA =
RegExp('^https?://$host/+[^/]+/+[^/]+(/+[^/]+)?');
match = standardUrlRegExA.firstMatch(url.toLowerCase()); match = standardUrlRegExA.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(name); throw InvalidURLError(name);
@ -47,17 +65,7 @@ class APKPure extends AppSource {
} }
String? dateString = String? dateString =
html.querySelector('span.info-other span.date')?.text.trim(); html.querySelector('span.info-other span.date')?.text.trim();
DateTime? releaseDate; DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
try {
releaseDate = dateString != null
? DateFormat('MMM dd, yyyy').parse(dateString)
: null;
releaseDate = dateString != null && releaseDate == null
? DateFormat('MMMM dd, yyyy').parse(dateString)
: null;
} catch (err) {
// ignore
}
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
List<MapEntry<String, String>> apkUrls = [ List<MapEntry<String, String>> apkUrls = [
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
@ -70,11 +78,13 @@ class APKPure extends AppSource {
Uri.parse(standardUrl).pathSegments.reversed.last; Uri.parse(standardUrl).pathSegments.reversed.last;
String appName = String appName =
html.querySelector('h1.info-title')?.text.trim() ?? appId; html.querySelector('h1.info-title')?.text.trim() ?? appId;
String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml String? changeLog = htmlChangelog
.trim().replaceAll("<br>", " \n"); .querySelector("div.whats-new-info p:not(.date)")
?.innerHtml
.trim()
.replaceAll("<br>", " \n");
return APKDetails(version, apkUrls, AppNames(author, appName), return APKDetails(version, apkUrls, AppNames(author, appName),
releaseDate: releaseDate, releaseDate: releaseDate, changeLog: changeLog);
changeLog: changeLog);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@ -0,0 +1,76 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class Aptoide extends AppSource {
Aptoide() {
host = 'aptoide.com';
name = tr('Aptoide');
allowSubDomains = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
Future<String?> tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) async {
return (await getAppDetailsJSON(standardUrl))['package'];
}
Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async {
var res = await sourceRequest(standardUrl);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body);
String? id;
if (idMatch != null) {
id = res.body.substring(idMatch.start + 12, idMatch.end);
} else {
throw NoReleasesError();
}
var res2 =
await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id');
if (res2.statusCode != 200) {
throw getObtainiumHttpError(res);
}
return jsonDecode(res2.body)?['nodes']?['meta']?['data'];
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var appDetails = await getAppDetailsJSON(standardUrl);
String appName = appDetails['name'] ?? tr('app');
String author = appDetails['developer']?['name'] ?? name;
String? dateStr = appDetails['updated'];
String? version = appDetails['file']?['vername'];
String? apkUrl = appDetails['file']?['path'];
if (version == null) {
throw NoVersionError();
}
if (apkUrl == null) {
throw NoAPKError();
}
DateTime? relDate;
if (dateStr != null) {
relDate = DateTime.parse(dateStr);
}
return APKDetails(
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
releaseDate: relDate);
}
}

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@ -11,6 +12,26 @@ class FDroid extends AppSource {
host = 'f-droid.org'; host = 'f-droid.org';
name = tr('fdroid'); name = tr('fdroid');
canSearch = true; canSearch = true;
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormTextField('filterVersionsByRegEx',
label: tr('filterVersionsByRegEx'),
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
],
[
GeneratedFormSwitch('trySelectingSuggestedVersionCode',
label: tr('trySelectingSuggestedVersionCode'))
],
[
GeneratedFormSwitch('autoSelectHighestVersionCode',
label: tr('autoSelectHighestVersionCode'))
],
];
} }
@override @override
@ -36,28 +57,6 @@ class FDroid extends AppSource {
return Uri.parse(standardUrl).pathSegments.last; return Uri.parse(standardUrl).pathSegments.last;
} }
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Response res, String apkUrlPrefix, String standardUrl) {
if (res.statusCode == 200) {
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
if (releases.isEmpty) {
throw NoReleasesError();
}
String? latestVersion = releases[0]['versionName'];
if (latestVersion == null) {
throw NoVersionError();
}
List<String> apkUrls = releases
.where((element) => element['versionName'] == latestVersion)
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList();
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
} else {
throw getObtainiumHttpError(res);
}
}
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, String standardUrl,
@ -68,7 +67,18 @@ class FDroid extends AppSource {
return getAPKUrlsFromFDroidPackagesAPIResponse( return getAPKUrlsFromFDroidPackagesAPIResponse(
await sourceRequest('https://$host/api/v1/packages/$appId'), await sourceRequest('https://$host/api/v1/packages/$appId'),
'https://$host/repo/$appId', 'https://$host/repo/$appId',
standardUrl); standardUrl,
name,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx:
(additionalSettings['filterVersionsByRegEx'] as String?)
?.isNotEmpty ==
true
? additionalSettings['filterVersionsByRegEx']
: null);
} }
@override @override
@ -101,3 +111,78 @@ class FDroid extends AppSource {
} }
} }
} }
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Response res, String apkUrlPrefix, String standardUrl, String sourceName,
{bool autoSelectHighestVersionCode = false,
bool trySelectingSuggestedVersionCode = false,
String? filterVersionsByRegEx}) {
if (res.statusCode == 200) {
var response = jsonDecode(res.body);
List<dynamic> releases = response['packages'] ?? [];
if (releases.isEmpty) {
throw NoReleasesError();
}
String? version;
Iterable<dynamic> releaseChoices = [];
// Grab the versionCode suggested if the user chose to do that
// Only do so at this stage if the user has no release filter
if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null &&
filterVersionsByRegEx == null) {
var suggestedReleases = releases.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
version = suggestedReleases.first['versionName'];
}
}
// Apply the release filter if any
if (filterVersionsByRegEx != null) {
version = null;
releaseChoices = [];
for (var i = 0; i < releases.length; i++) {
if (RegExp(filterVersionsByRegEx)
.hasMatch(releases[i]['versionName'])) {
version = releases[i]['versionName'];
}
}
if (version == null) {
throw NoVersionError();
}
}
// Default to the highest version
version ??= releases[0]['versionName'];
if (version == null) {
throw NoVersionError();
}
// If a suggested release was not already picked, pick all those with the selected version
if (releaseChoices.isEmpty) {
releaseChoices =
releases.where((element) => element['versionName'] == version);
}
// For the remaining releases, use the toggles to auto-select one if possible
if (releaseChoices.length > 1) {
if (autoSelectHighestVersionCode) {
releaseChoices = [releaseChoices.first];
} else if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null) {
var suggestedReleases = releaseChoices.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
}
}
}
if (releaseChoices.isEmpty) {
throw NoReleasesError();
}
List<String> apkUrls = releaseChoices
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList();
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
} else {
throw getObtainiumHttpError(res);
}
}

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
@ -21,21 +22,6 @@ class GitHub extends AppSource {
label: tr('githubPATLabel'), label: tr('githubPATLabel'),
password: true, password: true,
required: false, required: false,
additionalValidators: [
(value) {
if (value != null && value.trim().isNotEmpty) {
if (value
.split(':')
.where((element) => element.trim().isNotEmpty)
.length !=
2) {
return tr('githubPATHint');
}
}
return null;
}
],
hint: tr('githubPATFormat'),
belowWidgets: [ belowWidgets: [
const SizedBox( const SizedBox(
height: 4, height: 4,
@ -86,9 +72,10 @@ class GitHub extends AppSource {
} }
]) ])
], ],
[GeneratedFormSwitch('verifyLatestTag', label: tr('verifyLatestTag'))],
[ [
GeneratedFormSwitch('verifyLatestTag', GeneratedFormSwitch('dontSortReleasesList',
label: tr('verifyLatestTag'), defaultValue: false) label: tr('dontSortReleasesList'))
] ]
]; ];
@ -169,26 +156,53 @@ class GitHub extends AppSource {
return url.substring(0, match.end); return url.substring(0, match.end);
} }
Future<String> getCredentialPrefixIfAny( @override
Map<String, dynamic> additionalSettings) async { Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
var token = await getTokenIfAny(additionalSettings);
var headers = <String, String>{};
if (token != null) {
headers[HttpHeaders.authorizationHeader] = 'Token $token';
}
if (forAPKDownload == true) {
headers[HttpHeaders.acceptHeader] = 'application/octet-stream';
}
if (headers.isNotEmpty) {
return headers;
} else {
return null;
}
}
Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
SettingsProvider settingsProvider = SettingsProvider(); SettingsProvider settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings(); await settingsProvider.initializeSettings();
var sourceConfig = var sourceConfig =
await getSourceConfigValues(additionalSettings, settingsProvider); await getSourceConfigValues(additionalSettings, settingsProvider);
String? creds = sourceConfig['github-creds']; String? creds = sourceConfig['github-creds'];
return creds != null && creds.isNotEmpty ? '$creds@' : ''; if (creds != null) {
var userNameEndIndex = creds.indexOf(':');
if (userNameEndIndex > 0) {
creds = creds.substring(
userNameEndIndex + 1); // For old username-included token inputs
}
return creds;
} else {
return null;
}
} }
@override @override
Future<String?> getSourceNote() async { Future<String?> getSourceNote() async {
if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) { if (!hostChanged && (await getTokenIfAny({})) == null) {
return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}'; return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
} }
return null; return null;
} }
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async => Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
'https://${await getCredentialPrefixIfAny(additionalSettings)}api.$host'; 'https://api.$host';
Future<String> convertStandardUrlToAPIUrl( Future<String> convertStandardUrlToAPIUrl(
String standardUrl, Map<String, dynamic> additionalSettings) async => String standardUrl, Map<String, dynamic> additionalSettings) async =>
@ -217,6 +231,8 @@ class GitHub extends AppSource {
? additionalSettings['filterReleaseNotesByRegEx'] ? additionalSettings['filterReleaseNotesByRegEx']
: null; : null;
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true; bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
bool dontSortReleasesList =
additionalSettings['dontSortReleasesList'] == true;
String? latestTag; String? latestTag;
if (verifyLatestTag) { if (verifyLatestTag) {
var temp = requestUrl.split('?'); var temp = requestUrl.split('?');
@ -238,9 +254,10 @@ class GitHub extends AppSource {
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?) (release['assets'] as List<dynamic>?)
?.map((e) { ?.map((e) {
return e['name'] != null && e['browser_download_url'] != null return (e['name'] != null) &&
((e['url'] ?? e['browser_download_url']) != null)
? MapEntry(e['name'] as String, ? MapEntry(e['name'] as String,
e['browser_download_url'] as String) (e['url'] ?? e['browser_download_url']) as String)
: const MapEntry('', ''); : const MapEntry('', '');
}) })
.where((element) => element.key.toLowerCase().endsWith('.apk')) .where((element) => element.key.toLowerCase().endsWith('.apk'))
@ -251,32 +268,36 @@ class GitHub extends AppSource {
rel?['published_at'] != null rel?['published_at'] != null
? DateTime.parse(rel['published_at']) ? DateTime.parse(rel['published_at'])
: null; : null;
releases.sort((a, b) { if (dontSortReleasesList) {
// See #478 and #534 releases = releases.reversed.toList();
if (a == b) { } else {
return 0; releases.sort((a, b) {
} else if (a == null) { // See #478 and #534
return -1; if (a == b) {
} else if (b == null) { return 0;
return 1; } else if (a == null) {
} else { return -1;
var nameA = a['tag_name'] ?? a['name']; } else if (b == null) {
var nameB = b['tag_name'] ?? b['name']; return 1;
var stdFormats = findStandardFormatsForVersion(nameA, true)
.intersection(findStandardFormatsForVersion(nameB, true));
if (stdFormats.isNotEmpty) {
var reg = RegExp(stdFormats.first);
var matchA = reg.firstMatch(nameA);
var matchB = reg.firstMatch(nameB);
return compareAlphaNumeric(
(nameA as String).substring(matchA!.start, matchA.end),
(nameB as String).substring(matchB!.start, matchB.end));
} else { } else {
return (getReleaseDateFromRelease(a) ?? DateTime(1)) var nameA = a['tag_name'] ?? a['name'];
.compareTo(getReleaseDateFromRelease(b) ?? DateTime(0)); var nameB = b['tag_name'] ?? b['name'];
var stdFormats = findStandardFormatsForVersion(nameA, true)
.intersection(findStandardFormatsForVersion(nameB, true));
if (stdFormats.isNotEmpty) {
var reg = RegExp(stdFormats.first);
var matchA = reg.firstMatch(nameA);
var matchB = reg.firstMatch(nameB);
return compareAlphaNumeric(
(nameA as String).substring(matchA!.start, matchA.end),
(nameB as String).substring(matchB!.start, matchB.end));
} else {
return (getReleaseDateFromRelease(a) ?? DateTime(1))
.compareTo(getReleaseDateFromRelease(b) ?? DateTime(0));
}
} }
} });
}); }
if (latestTag != null && if (latestTag != null &&
releases.isNotEmpty && releases.isNotEmpty &&
latestTag != latestTag !=

View File

@ -1,4 +1,5 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
@ -18,7 +19,7 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
.toList(); .toList();
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) { if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
return '${referenceAbsoluteUrl.origin}/$ambiguousUrl'; return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
} else if (ambiguousUrl.split('/').length == 1) { } else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) {
return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl'; return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
} else { } else {
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl'; return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
@ -92,6 +93,7 @@ class HTML extends AppSource {
GeneratedFormSwitch('sortByFileNamesNotLinks', GeneratedFormSwitch('sortByFileNamesNotLinks',
label: tr('sortByFileNamesNotLinks')) label: tr('sortByFileNamesNotLinks'))
], ],
[GeneratedFormSwitch('reverseSort', label: tr('reverseSort'))],
[ [
GeneratedFormTextField('customLinkFilterRegex', GeneratedFormTextField('customLinkFilterRegex',
label: tr('customLinkFilterRegex'), label: tr('customLinkFilterRegex'),
@ -106,9 +108,29 @@ class HTML extends AppSource {
[ [
GeneratedFormTextField('intermediateLinkRegex', GeneratedFormTextField('intermediateLinkRegex',
label: tr('intermediateLinkRegex'), label: tr('intermediateLinkRegex'),
hint: '([0-9]+\.)*[0-9]+/\$', hint: '([0-9]+.)*[0-9]+/\$',
required: false, required: false,
additionalValidators: [(value) => regExValidator(value)]) additionalValidators: [(value) => regExValidator(value)])
],
[
GeneratedFormTextField('versionExtractionRegEx',
label: tr('versionExtractionRegEx'),
required: false,
additionalValidators: [(value) => regExValidator(value)]),
GeneratedFormTextField('matchGroupToUse',
label: tr('matchGroupToUse'),
required: false,
hint: '1',
textInputType: const TextInputType.numberWithOptions(),
additionalValidators: [
(value) {
if (value?.isEmpty == true) {
value = null;
}
value ??= '1';
return intValidator(value);
}
])
] ]
]; ];
overrideVersionDetectionFormDefault('noVersionDetection', overrideVersionDetectionFormDefault('noVersionDetection',
@ -116,11 +138,14 @@ class HTML extends AppSource {
} }
@override @override
// TODO: implement requestHeaders choice, hardcoded for now Future<Map<String, String>?> getRequestHeaders(
Map<String, String>? get requestHeaders => { {Map<String, dynamic> additionalSettings = const <String, dynamic>{},
"User-Agent": bool forAPKDownload = false}) async {
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" return {
}; "User-Agent":
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
};
}
@override @override
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
@ -171,6 +196,9 @@ class HTML extends AppSource {
? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last, ? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last,
b.split('/').where((e) => e.isNotEmpty).last) b.split('/').where((e) => e.isNotEmpty).last)
: compareAlphaNumeric(a, b)); : compareAlphaNumeric(a, b));
if (additionalSettings['reverseSort'] == true) {
links = links.reversed.toList();
}
if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty ==
true) { true) {
var reg = RegExp(additionalSettings['apkFilterRegEx']); var reg = RegExp(additionalSettings['apkFilterRegEx']);
@ -180,10 +208,23 @@ class HTML extends AppSource {
throw NoReleasesError(); throw NoReleasesError();
} }
var rel = links.last; var rel = links.last;
var version = rel.hashCode.toString(); String? version = rel.hashCode.toString();
var versionExtractionRegEx =
additionalSettings['versionExtractionRegEx'] as String?;
if (versionExtractionRegEx?.isNotEmpty == true) {
var match = RegExp(versionExtractionRegEx!).allMatches(rel);
if (match.isEmpty) {
throw NoVersionError();
}
version = match.last
.group(int.parse(additionalSettings['matchGroupToUse'] as String));
if (version?.isEmpty == true) {
throw NoVersionError();
}
}
List<String> apkUrls = List<String> apkUrls =
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
return APKDetails(version, apkUrls.map((e) => MapEntry(e, e)).toList(), return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(),
AppNames(uri.host, tr('app'))); AppNames(uri.host, tr('app')));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);

View File

@ -3,14 +3,25 @@ import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
class IzzyOnDroid extends AppSource { class IzzyOnDroid extends AppSource {
late FDroid fd;
IzzyOnDroid() { IzzyOnDroid() {
host = 'android.izzysoft.de'; host = 'izzysoft.de';
fd = FDroid();
additionalSourceAppSpecificSettingFormItems =
fd.additionalSourceAppSpecificSettingFormItems;
allowSubDomains = true;
} }
@override @override
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+'); RegExp standardUrlRegExA = RegExp('^https?://android.$host/repo/apk/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegExA.firstMatch(url.toLowerCase());
if (match == null) {
RegExp standardUrlRegExB =
RegExp('^https?://apt.$host/fdroid/index/apk/[^/]+');
match = standardUrlRegExB.firstMatch(url.toLowerCase());
}
if (match == null) { if (match == null) {
throw InvalidURLError(name); throw InvalidURLError(name);
} }
@ -20,7 +31,7 @@ class IzzyOnDroid extends AppSource {
@override @override
Future<String?> tryInferringAppId(String standardUrl, Future<String?> tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) async { {Map<String, dynamic> additionalSettings = const {}}) async {
return FDroid().tryInferringAppId(standardUrl); return fd.tryInferringAppId(standardUrl);
} }
@override @override
@ -29,10 +40,16 @@ class IzzyOnDroid extends AppSource {
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
String? appId = await tryInferringAppId(standardUrl); String? appId = await tryInferringAppId(standardUrl);
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( return getAPKUrlsFromFDroidPackagesAPIResponse(
await sourceRequest( await sourceRequest(
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'), 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
'https://android.izzysoft.de/frepo/$appId', 'https://android.izzysoft.de/frepo/$appId',
standardUrl); standardUrl,
name,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx: additionalSettings['filterVersionsByRegEx']);
} }
} }

View File

@ -0,0 +1,82 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:obtainium/app_sources/apkpure.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class Uptodown extends AppSource {
Uptodown() {
host = 'uptodown.com';
allowSubDomains = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return '${url.substring(0, match.end)}/android/download';
}
@override
Future<String?> tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) async {
return (await getAppDetailsFromPage(standardUrl))['appId'];
}
Future<Map<String, String?>> getAppDetailsFromPage(String standardUrl) async {
var res = await sourceRequest(standardUrl);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var html = parse(res.body);
String? version = html.querySelector('div.version')?.innerHtml;
String? apkUrl =
html.querySelector('#detail-download-button')?.attributes['data-url'];
String? name = html.querySelector('#detail-app-name')?.innerHtml.trim();
String? author = html.querySelector('#author-link')?.innerHtml.trim();
var detailElements = html.querySelectorAll('#technical-information td');
String? appId = (detailElements.elementAtOrNull(2))?.innerHtml.trim();
String? dateStr = (detailElements.elementAtOrNull(29))?.innerHtml.trim();
return Map.fromEntries([
MapEntry('version', version),
MapEntry('apkUrl', apkUrl),
MapEntry('appId', appId),
MapEntry('name', name),
MapEntry('author', author),
MapEntry('dateStr', dateStr)
]);
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var appDetails = await getAppDetailsFromPage(standardUrl);
var version = appDetails['version'];
var apkUrl = appDetails['apkUrl'];
var appId = appDetails['appId'];
if (version == null) {
throw NoVersionError();
}
if (apkUrl == null) {
throw NoAPKError();
}
if (appId == null) {
throw NoReleasesError();
}
String appName = appDetails['name'] ?? tr('app');
String author = appDetails['author'] ?? name;
String? dateStr = appDetails['dateStr'];
DateTime? relDate;
if (dateStr != null) {
relDate = parseDateTimeMMMddCommayyyy(dateStr);
}
return APKDetails(
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
releaseDate: relDate);
}
}

View File

@ -12,7 +12,12 @@ class VLC extends AppSource {
get dwUrlBase => 'https://get.$host/vlc-android/'; get dwUrlBase => 'https://get.$host/vlc-android/';
@override @override
Map<String, String>? get requestHeaders => HTML().requestHeaders; Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) =>
HTML().getRequestHeaders(
additionalSettings: additionalSettings,
forAPKDownload: forAPKDownload);
@override @override
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
@ -60,36 +65,10 @@ class VLC extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
String? targetUrl = 'https://$dwUrlBase/$version/'; String? targetUrl = 'https://$dwUrlBase/$version/';
Response res2 = await get(Uri.parse(targetUrl)); var apkUrls = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64']
List<String> apkUrls = []; .map((e) => '${targetUrl}VLC-Android-$version-$e.apk')
if (res2.statusCode == 200) { .toList();
apkUrls = parse(res2.body)
.querySelectorAll('a')
.map((e) => e.attributes['href']?.split('/').last)
.where((h) =>
h != null && h.isNotEmpty && h.toLowerCase().endsWith('.apk'))
.map((e) => targetUrl + e!)
.toList();
} else if (res2.statusCode == 500 &&
res2.body.toLowerCase().indexOf('mirror') > 0) {
var html = parse(res2.body);
var err = '';
html.body?.nodes.forEach((element) {
if (element.text != null) {
err += '${element.text}\n';
}
});
err = err.trim();
if (err.isEmpty) {
err = tr('err');
}
throw ObtainiumError(err);
} else {
throw getObtainiumHttpError(res2);
}
return APKDetails( return APKDetails(
version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC')); version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC'));
} else { } else {
@ -108,6 +87,20 @@ class VLC extends AppSource {
throw NoAPKError(); throw NoAPKError();
} }
return apkUrl; return apkUrl;
} else if (res.statusCode == 500 &&
res.body.toLowerCase().indexOf('mirror') > 0) {
var html = parse(res.body);
var err = '';
html.body?.nodes.forEach((element) {
if (element.text != null) {
err += '${element.text}\n';
}
});
err = err.trim();
if (err.isEmpty) {
err = tr('err');
}
throw ObtainiumError(err);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@ -25,6 +25,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
late int max; late int max;
late String? hint; late String? hint;
late bool password; late bool password;
late TextInputType? textInputType;
GeneratedFormTextField(String key, GeneratedFormTextField(String key,
{String label = 'Input', {String label = 'Input',
@ -34,7 +35,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
this.required = true, this.required = true,
this.max = 1, this.max = 1,
this.hint, this.hint,
this.password = false}) this.password = false,
this.textInputType})
: super(key, : super(key,
label: label, label: label,
belowWidgets: belowWidgets, belowWidgets: belowWidgets,
@ -144,7 +146,8 @@ Color generateRandomLightColor() {
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70 // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]); final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
// Map RBG values from 0-1 to 0-255: // Map RBG values from 0-1 to 0-255:
final List<int> rgbValues = rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); final List<int> rgbValues =
rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]); return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
} }
@ -190,6 +193,7 @@ 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(
keyboardType: formItem.textInputType,
obscureText: formItem.password, obscureText: formItem.password,
autocorrect: !formItem.password, autocorrect: !formItem.password,
enableSuggestions: !formItem.password, enableSuggestions: !formItem.password,
@ -370,34 +374,37 @@ class _GeneratedFormState extends State<GeneratedForm> {
}) ?? }) ??
[const SizedBox.shrink()], [const SizedBox.shrink()],
(values[widget.items[r][e].key] (values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?) as Map<String, MapEntry<int, bool>>?)
?.values ?.values
.where((e) => e.value) .where((e) => e.value)
.length == 1 .length ==
1
? Padding( ? Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
var temp = values[widget.items[r][e].key] var temp = values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>; as Map<String, MapEntry<int, bool>>;
// get selected category str where bool is true // get selected category str where bool is true
final oldEntry = temp.entries.firstWhere((entry) => entry.value.value); final oldEntry = temp.entries
// generate new color, ensure it is not the same .firstWhere((entry) => entry.value.value);
int newColor = oldEntry.value.key; // generate new color, ensure it is not the same
while(oldEntry.value.key == newColor) { int newColor = oldEntry.value.key;
newColor = generateRandomLightColor().value; while (oldEntry.value.key == newColor) {
} newColor = generateRandomLightColor().value;
// Update entry with new color, remain selected }
temp.update(oldEntry.key, (old) => MapEntry(newColor, old.value)); // Update entry with new color, remain selected
values[widget.items[r][e].key] = temp; temp.update(oldEntry.key,
someValueChanged(); (old) => MapEntry(newColor, old.value));
}); values[widget.items[r][e].key] = temp;
}, someValueChanged();
icon: const Icon(Icons.format_color_fill_rounded), });
visualDensity: VisualDensity.compact, },
tooltip: tr('colour'), icon: const Icon(Icons.format_color_fill_rounded),
)) visualDensity: VisualDensity.compact,
tooltip: tr('colour'),
))
: const SizedBox.shrink(), : const SizedBox.shrink(),
(values[widget.items[r][e].key] (values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?) as Map<String, MapEntry<int, bool>>?)

View File

@ -19,7 +19,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.14.3'; const String currentVersion = '0.14.21';
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
@ -27,7 +27,7 @@ const int bgUpdateCheckAlarmId = 666;
List<MapEntry<Locale, String>> supportedLocales = const [ List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('en'), 'English'), MapEntry(Locale('en'), 'English'),
MapEntry(Locale('zh'), '汉语'), MapEntry(Locale('zh'), '简体中文'),
MapEntry(Locale('it'), 'Italiano'), MapEntry(Locale('it'), 'Italiano'),
MapEntry(Locale('ja'), '日本語'), MapEntry(Locale('ja'), '日本語'),
MapEntry(Locale('hu'), 'Magyar'), MapEntry(Locale('hu'), 'Magyar'),
@ -38,7 +38,8 @@ List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('pl'), 'Polski'), MapEntry(Locale('pl'), 'Polski'),
MapEntry(Locale('ru'), 'Русский язык'), MapEntry(Locale('ru'), 'Русский язык'),
MapEntry(Locale('bs'), 'Bosanski'), MapEntry(Locale('bs'), 'Bosanski'),
MapEntry(Locale('br'), 'Brasileiro'), MapEntry(Locale('pt'), 'Brasileiro'),
MapEntry(Locale('cs'), 'Česky'),
]; ];
const fallbackLocale = Locale('en'); const fallbackLocale = Locale('en');
const localeDir = 'assets/translations'; const localeDir = 'assets/translations';

View File

@ -15,8 +15,10 @@ class GitHubStars implements MassAppUrlSource {
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
String username, int page) async { String username, int page) async {
Response res = await get(Uri.parse( Response res = await get(
'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page')); Uri.parse(
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
headers: await GitHub().getRequestHeaders());
if (res.statusCode == 200) { if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {}; Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body) as List<dynamic>)) { for (var e in (jsonDecode(res.body) as List<dynamic>)) {

View File

@ -11,6 +11,7 @@ import 'package:obtainium/pages/app.dart';
import 'package:obtainium/pages/import_export.dart'; import 'package:obtainium/pages/import_export.dart';
import 'package:obtainium/pages/settings.dart'; import 'package:obtainium/pages/settings.dart';
import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/notifications_provider.dart';
import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -42,6 +43,8 @@ class _AddAppPageState extends State<AddAppPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppsProvider appsProvider = context.read<AppsProvider>(); AppsProvider appsProvider = context.read<AppsProvider>();
SettingsProvider settingsProvider = context.watch<SettingsProvider>(); SettingsProvider settingsProvider = context.watch<SettingsProvider>();
NotificationsProvider notificationsProvider =
context.read<NotificationsProvider>();
bool doingSomething = gettingAppInfo || searching; bool doingSomething = gettingAppInfo || searching;
@ -145,7 +148,7 @@ class _AddAppPageState extends State<AddAppPage> {
userPickedTrackOnly))) { userPickedTrackOnly))) {
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
app = await sourceProvider.getApp( app = await sourceProvider.getApp(
pickedSource!, userInput, additionalSettings, pickedSource!, userInput.trim(), additionalSettings,
trackOnlyOverride: trackOnly, trackOnlyOverride: trackOnly,
overrideSource: pickedSourceOverride, overrideSource: pickedSourceOverride,
inferAppIdIfOptional: inferAppIdIfOptional); inferAppIdIfOptional: inferAppIdIfOptional);
@ -161,7 +164,8 @@ class _AddAppPageState extends State<AddAppPage> {
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
var downloadedArtifact = await appsProvider.downloadApp( var downloadedArtifact = await appsProvider.downloadApp(
app, globalNavigatorKey.currentContext); app, globalNavigatorKey.currentContext,
notificationsProvider: notificationsProvider);
DownloadedApk? downloadedFile; DownloadedApk? downloadedFile;
DownloadedXApkDir? downloadedDir; DownloadedXApkDir? downloadedDir;
if (downloadedArtifact is DownloadedApk) { if (downloadedArtifact is DownloadedApk) {
@ -459,14 +463,12 @@ class _AddAppPageState extends State<AddAppPage> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(
height: 48,
),
Text( Text(
tr('supportedSourcesBelow'), tr('supportedSources'),
style: const TextStyle(fontWeight: FontWeight.bold),
), ),
const SizedBox( const SizedBox(
height: 8, height: 16,
), ),
...sourceProvider.sources ...sourceProvider.sources
.map((e) => GestureDetector( .map((e) => GestureDetector(
@ -520,15 +522,17 @@ class _AddAppPageState extends State<AddAppPage> {
: const SizedBox(); : const SizedBox();
}, },
future: pickedSource?.getSourceNote()), future: pickedSource?.getSourceNote()),
const SizedBox( SizedBox(
height: 16, height: pickedSource != null ? 16 : 96,
), ),
if (pickedSource != null) if (pickedSource != null) getAdditionalOptsCol(),
getAdditionalOptsCol() if (pickedSource == null)
else const Divider(
getSourcesListWidget(), height: 48,
const SizedBox( ),
height: 8, if (pickedSource == null) getSourcesListWidget(),
SizedBox(
height: pickedSource != null ? 8 : 2,
), ),
])), ])),
) )

View File

@ -152,7 +152,7 @@ class _AppPageState extends State<AppPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 125), const SizedBox(height: 20),
app?.icon != null app?.icon != null
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Image.memory( Image.memory(
@ -338,9 +338,9 @@ class _AppPageState extends State<AppPage> {
try { try {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
var res = await appsProvider.downloadAndInstallLatestApps( var res = await appsProvider.downloadAndInstallLatestApps(
app?.app.id != null ? [app!.app.id] : [], app?.app.id != null ? [app!.app.id] : [],
globalNavigatorKey.currentContext, globalNavigatorKey.currentContext,
settingsProvider); );
if (app?.app.installedVersion != null && !trackOnly) { if (app?.app.installedVersion != null && !trackOnly) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
showError(tr('appsUpdated'), context); showError(tr('appsUpdated'), context);
@ -463,9 +463,18 @@ class _AppPageState extends State<AppPage> {
: null)) : null))
], ],
)); ));
appScreenAppBar() => AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
);
return Scaffold( return Scaffold(
appBar: settingsProvider.showAppWebpage ? AppBar() : null, appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: RefreshIndicator( body: RefreshIndicator(
child: settingsProvider.showAppWebpage child: settingsProvider.showAppWebpage

View File

@ -381,8 +381,7 @@ class AppsPageState extends State<AppsPage> {
: () { : () {
appsProvider.downloadAndInstallLatestApps( appsProvider.downloadAndInstallLatestApps(
[listedApps[appIndex].app.id], [listedApps[appIndex].app.id],
globalNavigatorKey.currentContext, globalNavigatorKey.currentContext).catchError((e) {
settingsProvider).catchError((e) {
showError(e, context); showError(e, context);
return <String>[]; return <String>[];
}); });
@ -449,33 +448,48 @@ class AppsPageState extends State<AppsPage> {
: const SizedBox.shrink(), : const SizedBox.shrink(),
GestureDetector( GestureDetector(
onTap: showChangesFn, onTap: showChangesFn,
child: Column( child: Container(
mainAxisAlignment: MainAxisAlignment.center, decoration: BoxDecoration(
crossAxisAlignment: CrossAxisAlignment.end, borderRadius: BorderRadius.circular(12),
children: [ color: settingsProvider.highlightTouchTargets &&
Row(mainAxisSize: MainAxisSize.min, children: [ showChangesFn != null
Container( ? (Theme.of(context).brightness == Brightness.light
constraints: BoxConstraints( ? Theme.of(context).primaryColor
maxWidth: MediaQuery.of(context).size.width / 4), : Theme.of(context).primaryColorLight)
child: Text(getVersionText(index), .withAlpha(20)
overflow: TextOverflow.ellipsis, : null),
textAlign: TextAlign.end)), padding: settingsProvider.highlightTouchTargets
]), ? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
Row( : const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
mainAxisSize: MainAxisSize.min, child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Row(mainAxisSize: MainAxisSize.min, children: [
getChangesButtonString(index, showChangesFn != null), Container(
style: TextStyle( constraints: BoxConstraints(
fontStyle: FontStyle.italic, maxWidth:
decoration: showChangesFn != null MediaQuery.of(context).size.width / 4),
? TextDecoration.underline child: Text(getVersionText(index),
: TextDecoration.none), overflow: TextOverflow.ellipsis,
) textAlign: TextAlign.end)),
]),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
getChangesButtonString(
index, showChangesFn != null),
style: TextStyle(
fontStyle: FontStyle.italic,
decoration: showChangesFn != null
? TextDecoration.underline
: TextDecoration.none),
)
],
),
], ],
), )))
],
))
], ],
); );
@ -684,8 +698,8 @@ class AppsPageState extends State<AppsPage> {
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
} }
appsProvider appsProvider
.downloadAndInstallLatestApps(toInstall, .downloadAndInstallLatestApps(
globalNavigatorKey.currentContext, settingsProvider) toInstall, globalNavigatorKey.currentContext)
.catchError((e) { .catchError((e) {
showError(e, context); showError(e, context);
return <String>[]; return <String>[];

View File

@ -28,8 +28,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>(); var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>(); var settingsProvider = context.watch<SettingsProvider>();
var outlineButtonStyle = ButtonStyle( var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
@ -102,10 +102,17 @@ class _ImportExportPageState extends State<ImportExportPage> {
}); });
} }
runObtainiumExport() { runObtainiumExport({bool pickOnly = false}) async {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
appsProvider.exportApps().then((String path) { appsProvider
showError(tr('exportedTo', args: [path]), context); .exportApps(
pickOnly:
pickOnly || (await settingsProvider.getExportDir()) == null,
sp: settingsProvider)
.then((String? result) {
if (result != null) {
showError(tr('exportedTo', args: [result]), context);
}
}).catchError((e) { }).catchError((e) {
showError(e, context); showError(e, context);
}); });
@ -301,27 +308,85 @@ class _ImportExportPageState extends State<ImportExportPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( FutureBuilder(
children: [ future: settingsProvider.getExportDir(),
Expanded( builder: (context, snapshot) {
child: TextButton( return Column(
style: outlineButtonStyle, children: [
onPressed: appsProvider.apps.isEmpty || Row(
importInProgress children: [
? null Expanded(
: runObtainiumExport, child: TextButton(
child: Text(tr('obtainiumExport')))), style: outlineButtonStyle,
const SizedBox( onPressed: appsProvider.apps.isEmpty ||
width: 16, importInProgress
), ? null
Expanded( : () {
child: TextButton( runObtainiumExport(pickOnly: true);
style: outlineButtonStyle, },
onPressed: importInProgress child: Text(tr('pickExportDir')),
? null )),
: runObtainiumImport, const SizedBox(
child: Text(tr('obtainiumImport')))) width: 16,
], ),
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: appsProvider.apps.isEmpty ||
importInProgress ||
snapshot.data == null
? null
: runObtainiumExport,
child: Text(tr('obtainiumExport')),
)),
],
),
const SizedBox(
height: 8,
),
Row(
children: [
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: importInProgress
? null
: runObtainiumImport,
child: Text(tr('obtainiumImport')))),
],
),
if (snapshot.data != null)
Column(
children: [
const SizedBox(height: 16),
GeneratedForm(
items: [
[
GeneratedFormSwitch(
'autoExportOnChanges',
label: tr('autoExportOnChanges'),
defaultValue: settingsProvider
.autoExportOnChanges,
)
]
],
onValueChanges:
(value, valid, isBuilding) {
if (valid && !isBuilding) {
if (value['autoExportOnChanges'] !=
null) {
settingsProvider
.autoExportOnChanges = value[
'autoExportOnChanges'] ==
true;
}
}
}),
],
),
],
);
},
), ),
if (importInProgress) if (importInProgress)
const Column( const Column(
@ -399,7 +464,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
fontStyle: FontStyle.italic, fontSize: 12)), fontStyle: FontStyle.italic, fontSize: 12)),
const SizedBox( const SizedBox(
height: 8, height: 8,
) ),
], ],
))) )))
])); ]));

View File

@ -484,6 +484,21 @@ class _SettingsPageState extends State<SettingsPage> {
}) })
], ],
), ),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(tr('highlightTouchTargets'))),
Switch(
value:
settingsProvider.highlightTouchTargets,
onChanged: (value) {
settingsProvider.highlightTouchTargets =
value;
})
],
),
height32, height32,
Text( Text(
tr('categories'), tr('categories'),
@ -543,7 +558,7 @@ class _SettingsPageState extends State<SettingsPage> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Flexible(child: Text('Debug Menu')), Flexible(child: Text(tr('debugMenu'))),
Switch( Switch(
value: settingsProvider.showDebugOpts, value: settingsProvider.showDebugOpts,
onChanged: (value) { onChanged: (value) {
@ -562,12 +577,9 @@ class _SettingsPageState extends State<SettingsPage> {
const Duration(seconds: 0), const Duration(seconds: 0),
bgUpdateCheckAlarmId + 200, bgUpdateCheckAlarmId + 200,
bgUpdateCheck); bgUpdateCheck);
showError( showError(tr('bgTaskStarted'), context);
'Background task started - check logs.',
context);
}, },
child: child: Text(tr('runBgCheckNow')))
const Text('Run Background Update Check Now'))
], ],
), ),
]), ]),

View File

@ -4,6 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:android_intent_plus/flag.dart'; import 'package:android_intent_plus/flag.dart';
@ -29,6 +31,7 @@ import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart'; import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart'; import 'package:flutter_archive/flutter_archive.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
final pm = AndroidPackageManager(); final pm = AndroidPackageManager();
@ -116,16 +119,19 @@ moveStrToEnd(List<String> arr, String str, {String? strB}) {
return arr; return arr;
} }
moveStrToEndMapEntryWithCount( List<MapEntry<String, int>> moveStrToEndMapEntryWithCount(
List<MapEntry<String, int>> arr, MapEntry<String, int> str, List<MapEntry<String, int>> arr, MapEntry<String, int> str,
{MapEntry<String, int>? strB}) { {MapEntry<String, int>? strB}) {
MapEntry<String, int>? temp; MapEntry<String, int>? temp;
arr.removeWhere((element) { arr.removeWhere((element) {
bool res = element.key == str.key || element.key == strB?.key; bool resA = element.key == str.key;
if (res) { bool resB = element.key == strB?.key;
temp = element; if (resA) {
temp = str;
} else if (resB) {
temp = strB;
} }
return res; return resA || resB;
}); });
if (temp != null) { if (temp != null) {
arr = [...arr, temp!]; arr = [...arr, temp!];
@ -145,6 +151,7 @@ class AppsProvider with ChangeNotifier {
late Stream<FGBGType>? foregroundStream; late Stream<FGBGType>? foregroundStream;
late StreamSubscription<FGBGType>? foregroundSubscription; late StreamSubscription<FGBGType>? foregroundSubscription;
late Directory APKDir; late Directory APKDir;
late SettingsProvider settingsProvider = SettingsProvider();
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy()); Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
@ -156,6 +163,7 @@ class AppsProvider with ChangeNotifier {
if (isForeground) await loadApps(); if (isForeground) await loadApps();
}); });
() async { () async {
await settingsProvider.initializeSettings();
var cacheDirs = await getExternalCacheDirectories(); var cacheDirs = await getExternalCacheDirectories();
if (cacheDirs?.isNotEmpty ?? false) { if (cacheDirs?.isNotEmpty ?? false) {
APKDir = cacheDirs!.first; APKDir = cacheDirs!.first;
@ -211,7 +219,7 @@ class AppsProvider with ChangeNotifier {
if (headers != null) { if (headers != null) {
req.headers.addAll(headers); req.headers.addAll(headers);
} }
var client = Client(); var client = http.Client();
StreamedResponse response = await client.send(req); StreamedResponse response = await client.send(req);
String ext = String ext =
response.headers['content-disposition']?.split('.').last ?? 'apk'; response.headers['content-disposition']?.split('.').last ?? 'apk';
@ -294,9 +302,11 @@ class AppsProvider with ChangeNotifier {
notificationsProvider?.cancel(notif.id); notificationsProvider?.cancel(notif.id);
int? prevProg; int? prevProg;
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
var headers = await source.getRequestHeaders(
additionalSettings: app.additionalSettings, forAPKDownload: true);
var downloadedFile = await downloadFileWithRetry( var downloadedFile = await downloadFileWithRetry(
downloadUrl, fileNameNoExt, headers: source.requestHeaders, downloadUrl, fileNameNoExt,
(double? progress) { headers: headers, (double? progress) {
int? prog = progress?.ceil(); int? prog = progress?.ceil();
if (apps[app.id] != null) { if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = progress; apps[app.id]!.downloadProgress = progress;
@ -362,8 +372,10 @@ class AppsProvider with ChangeNotifier {
.where((element) => element.downloadProgress != null) .where((element) => element.downloadProgress != null)
.isNotEmpty; .isNotEmpty;
Future<bool> canInstallSilently( Future<bool> canInstallSilently(App app) async {
App app, SettingsProvider settingsProvider) async { if (app.id == obtainiumId) {
return false;
}
if (!settingsProvider.enableBackgroundUpdates) { if (!settingsProvider.enableBackgroundUpdates) {
return false; return false;
} }
@ -393,7 +405,7 @@ class AppsProvider with ChangeNotifier {
(await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion; (await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion;
// The OS must also be new enough and the APK should target a new enough API // The OS must also be new enough and the APK should target a new enough API
return osInfo.version.sdkInt >= 30 && return osInfo.version.sdkInt >= 31 &&
targetSDK != null && targetSDK != null &&
targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int) targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
(osInfo.version.sdkInt - 3); (osInfo.version.sdkInt - 3);
@ -418,7 +430,8 @@ class AppsProvider with ChangeNotifier {
zipFile: File(filePath), destinationDir: Directory(destinationPath)); zipFile: File(filePath), destinationDir: Directory(destinationPath));
} }
Future<void> installXApkDir(DownloadedXApkDir dir) async { Future<void> installXApkDir(DownloadedXApkDir dir,
{bool needsBGWorkaround = false}) async {
// We don't know which APKs in an XAPK are supported by the user's device // We don't know which APKs in an XAPK are supported by the user's device
// So we try installing all of them and assume success if at least one installed // So we try installing all of them and assume success if at least one installed
// If 0 APKs installed, throw the first install error encountered // If 0 APKs installed, throw the first install error encountered
@ -431,7 +444,8 @@ class AppsProvider with ChangeNotifier {
if (file.path.toLowerCase().endsWith('.apk')) { if (file.path.toLowerCase().endsWith('.apk')) {
try { try {
somethingInstalled = somethingInstalled || somethingInstalled = somethingInstalled ||
await installApk(DownloadedApk(dir.appId, file)); await installApk(DownloadedApk(dir.appId, file),
needsBGWorkaround: needsBGWorkaround);
} catch (e) { } catch (e) {
logs.add( logs.add(
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}'); 'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
@ -451,7 +465,8 @@ class AppsProvider with ChangeNotifier {
} }
} }
Future<bool> installApk(DownloadedApk file) async { Future<bool> installApk(DownloadedApk file,
{bool needsBGWorkaround = false}) async {
var newInfo = var newInfo =
await pm.getPackageArchiveInfo(archiveFilePath: file.file.path); await pm.getPackageArchiveInfo(archiveFilePath: file.file.path);
PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id);
@ -460,6 +475,17 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) { !(await canDowngradeApps())) {
throw DowngradeError(); throw DowngradeError();
} }
if (needsBGWorkaround) {
// The below 'await' will never return if we are in a background process
// To work around this, we should assume the install will be successful
// So we update the app's installed version first as we will never get to the later code
// We can't conditionally get rid of the 'await' as this causes install fails (BG process times out) - see #896
// TODO: When fixed, update this function and the calls to it accordingly
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
await saveApps([apps[file.appId]!.app],
attemptToCorrectInstallStatus: false);
}
int? code = int? code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
bool installed = false; bool installed = false;
@ -529,7 +555,6 @@ class AppsProvider with ChangeNotifier {
getHost(apkUrl.value) != getHost(app.url) && getHost(apkUrl.value) != getHost(app.url) &&
context != null) { context != null) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
var settingsProvider = context.read<SettingsProvider>();
if (!(settingsProvider.hideAPKOriginWarning) && if (!(settingsProvider.hideAPKOriginWarning) &&
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
await showDialog( await showDialog(
@ -550,8 +575,8 @@ class AppsProvider with ChangeNotifier {
// If no BuildContext is provided, apps that require user interaction are ignored // If no BuildContext is provided, apps that require user interaction are ignored
// If user input is needed and the App is in the background, a notification is sent to get the user's attention // If user input is needed and the App is in the background, a notification is sent to get the user's attention
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
Future<List<String>> downloadAndInstallLatestApps(List<String> appIds, Future<List<String>> downloadAndInstallLatestApps(
BuildContext? context, SettingsProvider settingsProvider, List<String> appIds, BuildContext? context,
{NotificationsProvider? notificationsProvider}) async { {NotificationsProvider? notificationsProvider}) async {
notificationsProvider = notificationsProvider =
notificationsProvider ?? context?.read<NotificationsProvider>(); notificationsProvider ?? context?.read<NotificationsProvider>();
@ -580,8 +605,7 @@ class AppsProvider with ChangeNotifier {
apps[id]!.app.preferredApkIndex = urlInd; apps[id]!.app.preferredApkIndex = urlInd;
await saveApps([apps[id]!.app]); await saveApps([apps[id]!.app]);
} }
if (context != null || if (context != null || await canInstallSilently(apps[id]!.app)) {
await canInstallSilently(apps[id]!.app, settingsProvider)) {
appsToInstall.add(id); appsToInstall.add(id);
} }
} }
@ -618,8 +642,7 @@ class AppsProvider with ChangeNotifier {
downloadedDir = downloadedArtifact as DownloadedXApkDir; downloadedDir = downloadedArtifact as DownloadedXApkDir;
} }
var appId = downloadedFile?.appId ?? downloadedDir!.appId; var appId = downloadedFile?.appId ?? downloadedDir!.appId;
bool willBeSilent = bool willBeSilent = await canInstallSilently(apps[appId]!.app);
await canInstallSilently(apps[appId]!.app, settingsProvider);
if (!(await settingsProvider.getInstallPermission(enforce: false))) { if (!(await settingsProvider.getInstallPermission(enforce: false))) {
throw ObtainiumError(tr('cancelled')); throw ObtainiumError(tr('cancelled'));
} }
@ -632,15 +655,13 @@ class AppsProvider with ChangeNotifier {
try { try {
if (downloadedFile != null) { if (downloadedFile != null) {
if (willBeSilent && context == null) { if (willBeSilent && context == null) {
// Would await forever - workaround - TODO installApk(downloadedFile, needsBGWorkaround: true);
installApk(downloadedFile);
} else { } else {
await installApk(downloadedFile); await installApk(downloadedFile);
} }
} else { } else {
if (willBeSilent && context == null) { if (willBeSilent && context == null) {
// Would await forever - workaround - TODO installXApkDir(downloadedDir!, needsBGWorkaround: true);
installXApkDir(downloadedDir!);
} else { } else {
await installXApkDir(downloadedDir!); await installXApkDir(downloadedDir!);
} }
@ -668,8 +689,8 @@ class AppsProvider with ChangeNotifier {
} }
Future<Directory> getAppsDir() async { Future<Directory> getAppsDir() async {
Directory appsDir = Directory( Directory appsDir =
'${(await getExternalStorageDirectory())?.path as String}/app_data'); Directory('${(await getExternalStorageDirectory())!.path}/app_data');
if (!appsDir.existsSync()) { if (!appsDir.existsSync()) {
appsDir.createSync(); appsDir.createSync();
} }
@ -869,8 +890,6 @@ class AppsProvider with ChangeNotifier {
.toList(); .toList();
// After reconciliation, delete externally uninstalled Apps if needed // After reconciliation, delete externally uninstalled Apps if needed
if (removedAppIds.isNotEmpty) { if (removedAppIds.isNotEmpty) {
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
if (settingsProvider.removeOnExternalUninstall) { if (settingsProvider.removeOnExternalUninstall) {
await removeApps(removedAppIds); await removeApps(removedAppIds);
} }
@ -909,6 +928,7 @@ class AppsProvider with ChangeNotifier {
} }
} }
notifyListeners(); notifyListeners();
exportApps(isAuto: true);
} }
Future<void> removeApps(List<String> appIds) async { Future<void> removeApps(List<String> appIds) async {
@ -930,6 +950,7 @@ class AppsProvider with ChangeNotifier {
} }
if (appIds.isNotEmpty) { if (appIds.isNotEmpty) {
notifyListeners(); notifyListeners();
exportApps(isAuto: true);
} }
} }
@ -1086,32 +1107,50 @@ class AppsProvider with ChangeNotifier {
return updateAppIds; return updateAppIds;
} }
Future<String> exportApps() async { Future<String?> exportApps(
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
if (await Permission.storage.isDenied) { SettingsProvider settingsProvider = sp ?? this.settingsProvider;
await Permission.storage.request(); var exportDir = await settingsProvider.getExportDir();
if (isAuto) {
if (settingsProvider.autoExportOnChanges != true) {
return null;
} }
if (await Permission.storage.isDenied) { if (exportDir == null) {
throw ObtainiumError(tr('storagePermissionDenied')); logs.add('Skipping auto-export as dir is not set.');
return null;
}
var files = await saf
.listFiles(exportDir, columns: [saf.DocumentFileColumn.id])
.where((f) => f.uri.pathSegments.last.endsWith('-auto.json'))
.toList();
if (files.isNotEmpty) {
for (var f in files) {
saf.delete(f.uri);
}
} }
} }
Directory? exportDir = Directory('/storage/emulated/0/Download'); if (exportDir == null || pickOnly) {
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided await settingsProvider.pickExportDir();
var downloadsAccessible = false; exportDir = await settingsProvider.getExportDir();
try {
downloadsAccessible = exportDir.existsSync();
} catch (e) {
logs.add('Error accessing Downloads (will use fallback): $e');
} }
if (!downloadsAccessible) { if (exportDir == null) {
exportDir = await getExternalStorageDirectory(); return null;
path = exportDir!.path;
} }
File export = File( String? returnPath;
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); if (!pickOnly) {
export.writeAsStringSync( var result = await saf.createFile(exportDir,
jsonEncode(apps.values.map((e) => e.app.toJson()).toList())); displayName:
return path; '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
mimeType: 'application/json',
bytes: Uint8List.fromList(utf8.encode(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()))));
if (result == null) {
throw ObtainiumError(tr('unexpectedError'));
}
returnPath =
exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/');
}
return returnPath;
} }
Future<int> importApps(String appsJSON) async { Future<int> importApps(String appsJSON) async {
@ -1288,14 +1327,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
NotificationsProvider notificationsProvider = NotificationsProvider(); NotificationsProvider notificationsProvider = NotificationsProvider();
AppsProvider appsProvider = AppsProvider(isBg: true); AppsProvider appsProvider = AppsProvider(isBg: true);
await appsProvider.loadApps(); await appsProvider.loadApps();
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
int maxAttempts = 4; int maxAttempts = 4;
params ??= {}; params ??= {};
if (params['toCheck'] == null) { if (params['toCheck'] == null) {
settingsProvider.lastBGCheckTime = DateTime.now(); appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
} }
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[ List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
...(params['toCheck'] ...(params['toCheck']
@ -1325,73 +1362,80 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
var didCompleteChecking = false; var didCompleteChecking = false;
CheckingUpdatesNotification? notif; CheckingUpdatesNotification? notif;
var networkRestricted = false; var networkRestricted = false;
if (settingsProvider.bgUpdatesOnWiFiOnly) { if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
var netResult = await (Connectivity().checkConnectivity()); var netResult = await (Connectivity().checkConnectivity());
networkRestricted = (netResult != ConnectivityResult.wifi) && networkRestricted = (netResult != ConnectivityResult.wifi) &&
(netResult != ConnectivityResult.ethernet); (netResult != ConnectivityResult.ethernet);
} }
// Loop through all updates and check each // Loop through all updates and check each
for (int i = 0; i < toCheck.length; i++) { List<App> toNotify = [];
var appId = toCheck[i].key; try {
var retryCount = toCheck[i].value; for (int i = 0; i < toCheck.length; i++) {
AppInMemory? app = appsProvider.apps[appId]; var appId = toCheck[i].key;
if (app?.app.installedVersion != null) { var attemptCount = toCheck[i].value + 1;
try { AppInMemory? app = appsProvider.apps[appId];
notificationsProvider.notify( if (app?.app.installedVersion != null) {
notif = CheckingUpdatesNotification(app?.name ?? appId), try {
cancelExisting: true); notificationsProvider.notify(
App? newApp = await appsProvider.checkUpdate(appId); notif = CheckingUpdatesNotification(app?.name ?? appId),
if (newApp != null) { cancelExisting: true);
if (networkRestricted || App? newApp = await appsProvider.checkUpdate(appId);
!(await appsProvider.canInstallSilently( if (newApp != null) {
app!.app, settingsProvider))) { if (networkRestricted ||
notificationsProvider.notify( !(await appsProvider.canInstallSilently(app!.app))) {
UpdateNotification([newApp], id: newApp.id.hashCode - 1)); toNotify.add(newApp);
} else {
toInstall.add(MapEntry(appId, 0));
}
}
if (i == (toCheck.length - 1)) {
didCompleteChecking = true;
}
} catch (e) {
// If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue checking shortly
logs.add(
'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.');
if (attemptCount < maxAttempts) {
var remainingSeconds = e is RateLimitError
? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60))
: e is ClientException
? (15 * 60)
: pow(attemptCount, 2).toInt();
logs.add(
'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
var remainingToCheck = moveStrToEndMapEntryWithCount(
toCheck.sublist(i), MapEntry(appId, attemptCount));
AndroidAlarmManager.oneShot(Duration(seconds: remainingSeconds),
taskId + 1, bgUpdateCheck,
params: {
'toCheck': remainingToCheck
.map(
(entry) => {'key': entry.key, 'value': entry.value})
.toList(),
'toInstall': toInstall
.map(
(entry) => {'key': entry.key, 'value': entry.value})
.toList(),
});
break;
} else { } else {
toInstall.add(MapEntry(appId, 0)); // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue)
toCheck.removeAt(i);
i--;
notificationsProvider
.notify(ErrorCheckingUpdatesNotification(e.toString()));
}
} finally {
if (notif != null) {
notificationsProvider.cancel(notif.id);
} }
} }
if (i == (toCheck.length - 1)) {
didCompleteChecking = true;
}
} catch (e) {
// If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue checking shortly
logs.add(
'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.');
if (retryCount < maxAttempts) {
var remainingSeconds = e is RateLimitError
? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60))
: e is ClientException
? (15 * 60)
: (retryCount ^ 2);
logs.add(
'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
var remainingToCheck = moveStrToEndMapEntryWithCount(
toCheck.sublist(i), MapEntry(appId, retryCount + 1));
AndroidAlarmManager.oneShot(
Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck,
params: {
'toCheck': remainingToCheck
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
});
break;
} else {
// If the offender has reached its fail limit, notify the user and remove it from the list (task can continue)
toCheck.removeAt(i);
i--;
notificationsProvider
.notify(ErrorCheckingUpdatesNotification(e.toString()));
}
} finally {
if (notif != null) {
notificationsProvider.cancel(notif.id);
}
} }
} }
} finally {
if (toNotify.isNotEmpty) {
notificationsProvider.notify(UpdateNotification(toNotify));
}
} }
// If you're done checking and found some silently installable updates, schedule another task which will run in install mode // If you're done checking and found some silently installable updates, schedule another task which will run in install mode
if (didCompleteChecking && toInstall.isNotEmpty) { if (didCompleteChecking && toInstall.isNotEmpty) {
@ -1424,8 +1468,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
try { try {
logs.add( logs.add(
'BG install task $taskId: Attempting to update $appId in the background.'); 'BG install task $taskId: Attempting to update $appId in the background.');
await appsProvider.downloadAndInstallLatestApps( await appsProvider.downloadAndInstallLatestApps([appId], null,
[appId], null, settingsProvider,
notificationsProvider: notificationsProvider); notificationsProvider: notificationsProvider);
await Future.delayed(const Duration( await Future.delayed(const Duration(
seconds: seconds:

View File

@ -9,8 +9,10 @@ import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/main.dart'; import 'package:obtainium/main.dart';
import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
String obtainiumId = 'dev.imranr.obtainium'; String obtainiumId = 'dev.imranr.obtainium';
@ -35,6 +37,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
class SettingsProvider with ChangeNotifier { class SettingsProvider with ChangeNotifier {
SharedPreferences? prefs; SharedPreferences? prefs;
String? defaultAppDir;
bool justStarted = true; bool justStarted = true;
String sourceUrl = 'https://github.com/ImranR98/Obtainium'; String sourceUrl = 'https://github.com/ImranR98/Obtainium';
@ -42,6 +45,7 @@ class SettingsProvider with ChangeNotifier {
// Not done in constructor as we want to be able to await it // Not done in constructor as we want to be able to await it
Future<void> initializeSettings() async { Future<void> initializeSettings() async {
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
defaultAppDir = (await getExternalStorageDirectory())!.path;
notifyListeners(); notifyListeners();
} }
@ -348,4 +352,58 @@ class SettingsProvider with ChangeNotifier {
prefs?.setBool('showDebugOpts', val); prefs?.setBool('showDebugOpts', val);
notifyListeners(); notifyListeners();
} }
bool get highlightTouchTargets {
return prefs?.getBool('highlightTouchTargets') ?? false;
}
set highlightTouchTargets(bool val) {
prefs?.setBool('highlightTouchTargets', val);
notifyListeners();
}
Future<Uri?> getExportDir() async {
var uriString = prefs?.getString('exportDir');
if (uriString != null) {
Uri? uri = Uri.parse(uriString);
if (!(await saf.canRead(uri) ?? false) ||
!(await saf.canWrite(uri) ?? false)) {
uri = null;
prefs?.remove('exportDir');
notifyListeners();
}
return uri;
} else {
return null;
}
}
Future<void> pickExportDir({bool remove = false}) async {
var existingSAFPerms = (await saf.persistedUriPermissions()) ?? [];
var currentOneWayDataSyncDir = await getExportDir();
Uri? newOneWayDataSyncDir;
if (!remove) {
newOneWayDataSyncDir = (await saf.openDocumentTree());
}
if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) {
if (newOneWayDataSyncDir == null) {
prefs?.remove('exportDir');
} else {
prefs?.setString('exportDir', newOneWayDataSyncDir.toString());
}
notifyListeners();
}
for (var e in existingSAFPerms) {
await saf.releasePersistableUriPermission(e.uri);
}
}
bool get autoExportOnChanges {
return prefs?.getBool('autoExportOnChanges') ?? false;
}
set autoExportOnChanges(bool val) {
prefs?.setBool('autoExportOnChanges', val);
notifyListeners();
}
} }

View File

@ -9,6 +9,7 @@ import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/apkpure.dart'; import 'package:obtainium/app_sources/apkpure.dart';
import 'package:obtainium/app_sources/aptoide.dart';
import 'package:obtainium/app_sources/codeberg.dart'; import 'package:obtainium/app_sources/codeberg.dart';
import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroid.dart';
import 'package:obtainium/app_sources/fdroidrepo.dart'; import 'package:obtainium/app_sources/fdroidrepo.dart';
@ -25,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart';
import 'package:obtainium/app_sources/sourcehut.dart'; import 'package:obtainium/app_sources/sourcehut.dart';
import 'package:obtainium/app_sources/steammobile.dart'; import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/app_sources/telegramapp.dart'; import 'package:obtainium/app_sources/telegramapp.dart';
import 'package:obtainium/app_sources/uptodown.dart';
import 'package:obtainium/app_sources/vlc.dart'; import 'package:obtainium/app_sources/vlc.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';
@ -325,6 +327,7 @@ abstract class AppSource {
bool enforceTrackOnly = false; bool enforceTrackOnly = false;
bool changeLogIfAnyIsMarkDown = true; bool changeLogIfAnyIsMarkDown = true;
bool appIdInferIsOptional = false; bool appIdInferIsOptional = false;
bool allowSubDomains = false;
AppSource() { AppSource() {
name = runtimeType.toString(); name = runtimeType.toString();
@ -361,15 +364,23 @@ abstract class AppSource {
return url; return url;
} }
Map<String, String>? get requestHeaders => null; Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
return null;
}
Future<Response> sourceRequest(String url, Future<Response> sourceRequest(String url,
{bool followRedirects = true}) async { {bool followRedirects = true,
Map<String, dynamic> additionalSettings =
const <String, dynamic>{}}) async {
var requestHeaders =
await getRequestHeaders(additionalSettings: additionalSettings);
if (requestHeaders != null || followRedirects == false) { if (requestHeaders != null || followRedirects == false) {
var req = Request('GET', Uri.parse(url)); var req = Request('GET', Uri.parse(url));
req.followRedirects = followRedirects; req.followRedirects = followRedirects;
if (requestHeaders != null) { if (requestHeaders != null) {
req.headers.addAll(requestHeaders!); req.headers.addAll(requestHeaders);
} }
return Response.fromStream(await Client().send(req)); return Response.fromStream(await Client().send(req));
} else { } else {
@ -510,6 +521,20 @@ regExValidator(String? value) {
return null; return null;
} }
intValidator(String? value, {bool positive = false}) {
if (value == null) {
return tr('invalidInput');
}
var num = int.tryParse(value);
if (num == null) {
return tr('invalidInput');
}
if (positive && num <= 0) {
return tr('invalidInput');
}
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> get sources => [ List<AppSource> get sources => [
@ -517,18 +542,20 @@ class SourceProvider {
GitLab(), GitLab(),
Codeberg(), Codeberg(),
FDroid(), FDroid(),
IzzyOnDroid(),
FDroidRepo(), FDroidRepo(),
Jenkins(), IzzyOnDroid(),
SourceForge(), SourceForge(),
SourceHut(), SourceHut(),
APKMirror(),
APKPure(), APKPure(),
Aptoide(),
Uptodown(),
APKMirror(),
HuaweiAppGallery(), HuaweiAppGallery(),
Jenkins(),
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
Mullvad(), Mullvad(),
Signal(), Signal(),
VLC(), // As of 2023-08-26 this site randomly messes up the 'latest' version (one minute it's 3.5.4, next minute back to 3.5.3) VLC(),
// WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date // WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
TelegramApp(), TelegramApp(),
SteamMobile(), SteamMobile(),
@ -554,7 +581,9 @@ class SourceProvider {
} }
AppSource? source; AppSource? source;
for (var s in sources.where((element) => element.host != null)) { for (var s in sources.where((element) => element.host != null)) {
if (RegExp('://${s.host}(/|\\z)?').hasMatch(url)) { if (RegExp(
'://${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}${s.host}(/|\\z)?')
.hasMatch(url)) {
source = s; source = s;
break; break;
} }

View File

@ -38,18 +38,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: animations name: animations
sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.7" version: "2.0.8"
archive: archive:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" sha256: d4dc11707abb32ef756ab95678c0d6df54003d98277f7c9aeda14c48e7a38c2f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.7" version: "3.4.3"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -142,10 +142,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.3+4" version: "0.3.3+5"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -166,10 +166,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@ -198,18 +198,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dynamic_color name: dynamic_color
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d sha256: "96bff3df72e3d428bda2b874c7a521e8c86f592cae626ea594922fcc8d166e0c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.6" version: "1.6.7"
easy_localization: easy_localization:
dependency: "direct main" dependency: "direct main"
description: description:
name: easy_localization name: easy_localization
sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645" sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.3"
easy_logger: easy_logger:
dependency: transitive dependency: transitive
description: description:
@ -246,10 +246,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: bdfa035a974a0c080576c4c8ed01cdf9d1b406a04c7daa05443ef0383a97bedc sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.3.4" version: "5.5.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -283,10 +283,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
flutter_local_notifications: flutter_local_notifications:
dependency: "direct main" dependency: "direct main"
description: description:
@ -320,18 +320,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_markdown name: flutter_markdown
sha256: "2b206d397dd7836ea60035b2d43825c8a303a76a5098e66f42d55a753e18d431" sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.17+1" version: "0.6.17+3"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.15" version: "2.0.16"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -386,10 +386,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.17" version: "4.1.3"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@ -490,66 +490,66 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.2.0"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
permission_handler: permission_handler:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" sha256: ad65ba9af42a3d067203641de3fd9f547ded1410bad3b84400c2b4899faede70
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.4.3" version: "11.0.0"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" sha256: ace7d15a3d1a4a0b91c041d01e5405df221edb9de9116525efc773c74e6fc790
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.3.3" version: "11.0.5"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
@ -562,10 +562,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.11.3" version: "3.11.5"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
@ -586,18 +586,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "57c07bf82207aee366dfaa3867b3164e4f03a238a461a11b0e8a3a510d51203d" sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.1.6"
pointycastle: pointycastle:
dependency: transitive dependency: transitive
description: description:
@ -634,58 +634,66 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.3" version: "2.3.4"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shared_storage:
dependency: "direct main"
description:
name: shared_storage
sha256: "7c65a9d64f0f5521256be974cfd74010af12196657cec9f9fb7b03b2f11bcaf6"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -783,66 +791,66 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.12" version: "6.1.14"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.38" version: "6.1.0"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "6.1.5"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.6"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.7"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_platform_interface name: url_launcher_platform_interface
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.5"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.18" version: "2.0.20"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.8"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@ -871,58 +879,58 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: webview_flutter name: webview_flutter
sha256: "04a0782fb058b7c71f2048935583488f4d32e9147ca403abc4e58f1de9964629" sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.3" version: "4.2.4"
webview_flutter_android: webview_flutter_android:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: bca797abba472868655b5f1a6029c1132385685ee9db4713cb0e7f33076210c6 sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.3" version: "3.10.1"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
sha256: "0ca3cfcc6781a7de701d580917af4a9efc4e3e129f8ead95a80587f0a749480a" sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0" version: "2.6.0"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
sha256: ed749f94ac9e814d04a258a9255cf69cfa4cc6006ff59542aea7fb4590144972 sha256: "485af05f2c5f83c7f78c20e236b170ad02df7153b299ae9917345be43871d29f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.7.3" version: "3.8.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.7" version: "5.0.9"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
name: win32_registry name: win32_registry
sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.3"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@ -940,5 +948,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.1.0-185.0.dev <4.0.0" dart: ">=3.1.0 <4.0.0"
flutter: ">=3.10.0" flutter: ">=3.13.0"

View File

@ -17,10 +17,10 @@ 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.14.3+195 # When changing this, update the tag in main() accordingly version: 0.14.21+213 # When changing this, update the tag in main() accordingly
environment: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=3.0.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@ -46,7 +46,7 @@ dependencies:
html: ^0.15.0 html: ^0.15.0
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
url_launcher: ^6.1.5 url_launcher: ^6.1.5
permission_handler: ^10.0.0 permission_handler: ^11.0.0
fluttertoast: ^8.0.9 fluttertoast: ^8.0.9
device_info_plus: ^9.0.0 device_info_plus: ^9.0.0
file_picker: ^5.2.10 file_picker: ^5.2.10
@ -65,6 +65,7 @@ dependencies:
flutter_archive: ^5.0.0 flutter_archive: ^5.0.0
hsluv: ^1.1.3 hsluv: ^1.1.3
connectivity_plus: ^4.0.2 connectivity_plus: ^4.0.2
shared_storage: ^0.8.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: