Compare commits
153 Commits
v0.10.1-be
...
v0.11.13-b
Author | SHA1 | Date | |
---|---|---|---|
3bc5837999 | |||
9fbe524818 | |||
c21a9d7292 | |||
9c6068b270 | |||
cd86d6112b | |||
1112c79c14 | |||
08555bac75 | |||
6db31e2b24 | |||
48d2532323 | |||
f1fc43a3e7 | |||
280827d8ec | |||
05ee0f9c48 | |||
ef06ae289e | |||
bd0e322465 | |||
a93a2411fa | |||
26e6eef72e | |||
e49a6e311b | |||
53d3397651 | |||
fe540f5e61 | |||
234374224b | |||
83390f648a | |||
1143b6a546 | |||
0f3e029312 | |||
c0120f4e40 | |||
a0199f0ceb | |||
0528936e5a | |||
4de98b2f36 | |||
dfb5f5596c | |||
2e706aac47 | |||
24a600e595 | |||
1596a44ec5 | |||
9ee2be76ca | |||
83b770294d | |||
2679d5a1aa | |||
e49c09c0ff | |||
c9318ef2b5 | |||
2e88c8eede | |||
8648c1bea7 | |||
b22e2bab0c | |||
57f7bf44c2 | |||
ce526d8d26 | |||
5f3eeb9971 | |||
e67a6b8627 | |||
f8e99bb0cb | |||
09b5dd41d3 | |||
b1bd36408c | |||
54d8dff32f | |||
7b1416e28e | |||
926e7b89ce | |||
43d4f89d61 | |||
2190da162d | |||
f10bb5ac91 | |||
8e52f9666d | |||
a8a47bb153 | |||
728dafcc28 | |||
d53b21906c | |||
d6dcac0f97 | |||
dae5a67652 | |||
508fcccec9 | |||
cc8a4c3760 | |||
814e2b7306 | |||
2e159c9886 | |||
b82d28f2a7 | |||
3c61735706 | |||
a2879f5bfa | |||
b57f023739 | |||
c376a7abec | |||
31c6cc3f6f | |||
8de8438aeb | |||
2b0225dd5b | |||
f6af3a7998 | |||
bd29d7bc10 | |||
ffb3516a4b | |||
6a5e7942ee | |||
859158e84a | |||
435116e10b | |||
a788d9d7cd | |||
4be3478b97 | |||
fe0126095a | |||
d5fdf28a98 | |||
f06d245e20 | |||
2b4f94b407 | |||
5f7e342e6b | |||
191776d0d5 | |||
ea81b0e66e | |||
86131ae3ce | |||
64ded1d720 | |||
a11c2f1d37 | |||
890787f87f | |||
c5ff1de950 | |||
56658abd60 | |||
b60622e2cb | |||
e149f0b225 | |||
d9729f08c0 | |||
eda5c1bac6 | |||
5574ea870b | |||
9f03234ac1 | |||
b2503dd43d | |||
e01ca704bc | |||
6aa4ace8e2 | |||
d762467a31 | |||
b07cce8ecd | |||
8002a946b2 | |||
fd9aebc5b2 | |||
1be38d361f | |||
32c40ae7b3 | |||
07223d81c7 | |||
78baee7265 | |||
348c33dfe9 | |||
c408d70ae6 | |||
3ae4e7cc8a | |||
dab0f2bb72 | |||
4baf6bcd3b | |||
db4517aa13 | |||
55d4d1f978 | |||
f89ac5965f | |||
d5ebaa161f | |||
a4c014a8bf | |||
bbaa42fb01 | |||
4fe311bc03 | |||
ea68b97ff7 | |||
6e0f6b528e | |||
a2c227931e | |||
15ad3bb439 | |||
b03d7fba1a | |||
31c491d7c5 | |||
71c80f11f5 | |||
eef4d33431 | |||
d56342e907 | |||
c72c0fdb57 | |||
ffe29009ed | |||
60e3b68ebd | |||
ee4d0f259f | |||
0ecfbef0a0 | |||
1b60e75ca7 | |||
abcfa389e8 | |||
a64bd67ef1 | |||
4252c2711b | |||
52913b0450 | |||
427b0ed8d2 | |||
a85d6d4f08 | |||
05f712603c | |||
fa2a80e34c | |||
f43e5a2ff1 | |||
b72aa8273e | |||
520f186e4a | |||
e1e97672cf | |||
1494bcd013 | |||
3457a0a12f | |||
b165400a6e | |||
c47bf937f1 | |||
2e19a8c04c | |||
05d4da86ec |
14
README.md
@ -19,9 +19,21 @@ Currently supported App sources:
|
|||||||
- Third Party F-Droid Repos
|
- Third Party F-Droid Repos
|
||||||
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
|
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- [Steam](https://store.steampowered.com/mobile)
|
||||||
|
- [Telegram App](https://telegram.org)
|
||||||
|
- [VLC](https://www.videolan.org/vlc/download-android.html)
|
||||||
|
- [Neutron Code](https://neutroncode.com)
|
||||||
- "HTML" (Fallback)
|
- "HTML" (Fallback)
|
||||||
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
|
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png"
|
||||||
|
alt="Get it on GitHub"
|
||||||
|
height="80">](https://github.com/ImranR98/Obtainium/releases)
|
||||||
|
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
|
||||||
|
alt="Get it on IzzyOnDroid"
|
||||||
|
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
||||||
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
||||||
@ -31,4 +43,4 @@ Currently supported App sources:
|
|||||||
|
|
||||||
| <img src="./assets/screenshots/1.apps.png" alt="Apps Page" /> | <img src="./assets/screenshots/2.dark_theme.png" alt="Dark Theme" /> | <img src="./assets/screenshots/3.material_you.png" alt="Material You" /> |
|
| <img src="./assets/screenshots/1.apps.png" alt="Apps Page" /> | <img src="./assets/screenshots/2.dark_theme.png" alt="Dark Theme" /> | <img src="./assets/screenshots/3.material_you.png" alt="Material You" /> |
|
||||||
| ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||||
| <img src="./assets/screenshots/4.app.png" alt="App Page" /> | <img src="./assets/screenshots/5.apk_picker.png" alt="Multiple APK Support" /> | <img src="./assets/screenshots/6.apk_install.png" alt="App Installation" /> |
|
| <img src="./assets/screenshots/4.app.png" alt="App Page" /> | <img src="./assets/screenshots/5.app_opts.png" alt="App Options" /> | <img src="./assets/screenshots/6.app_webview.png" alt="App Web View" /> |
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
<application
|
<application
|
||||||
android:label="Obtainium"
|
android:label="Obtainium"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@ -51,7 +52,8 @@
|
|||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28"/>
|
android:maxSdkVersion="29"/>
|
||||||
</manifest>
|
</manifest>
|
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 25 KiB |
46
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:viewportWidth="142.129"
|
||||||
|
android:viewportHeight="142.129"
|
||||||
|
android:width="503.6066dp"
|
||||||
|
android:height="503.6066dp">
|
||||||
|
<group
|
||||||
|
android:translateX="-30.39437"
|
||||||
|
android:translateY="-54.68043">
|
||||||
|
<path
|
||||||
|
android:pathData="M109.8808 153.22596c-0.73146 -0.38777 -5.00657 -2.75679 -25.032416 -13.87149 -5.57273 -3.09297 -10.93823 -6.06723 -11.92332 -6.60948 -2.23728 -1.23152 -2.58105 -1.53456 -2.58105 -2.27528 0 -0.3879 0.89293 -2.87231 2.98561 -8.30689 1.64209 -4.2644 3.09426 -8.0014 3.22705 -8.30444 0.3024 -0.69008 0.78972 -1.27621 1.26573 -1.52236 0.44558 -0.23042 11.58052 -4.29685 12.14814 -4.43644 0.61355 -0.1509 1.1428 0.13977 1.45487 0.79901 0.14976 0.31638 0.77213 1.94934 1.38303 3.6288 0.6109 1.67945 1.52036 4.16275 2.02104 5.51844 1.14709 3.10604 1.18992 3.54589 0.3912 4.01771 -0.2117 0.12505 -1.58874 0.66539 -3.06009 1.20075 -1.47136 0.53536 -2.87533 1.08982 -3.11993 1.23213 -0.56422 0.32826 -0.64913 0.83523 -0.20815 1.24273 0.17523 0.16193 3.00434 1.77571 6.28691 3.58618 9.174936 5.06035 8.665596 4.83136 9.277626 4.17097 0.29987 -0.32356 5.78141 -14.266 6.09596 -15.50521 0.1344 -0.5295 0.11969 -0.60308 -0.16695 -0.83519 -0.39165 -0.31714 -0.335 -0.33071 -3.93797 0.9431 -3.56937 1.26192 -3.90926 1.28864 -4.38744 0.34488 -0.25108 -0.49556 -4.095796 -11.05481 -4.334456 -11.90432 -0.15438 -0.5495 0.0344 -1.0717 0.49701 -1.37482 0.19228 -0.12598 2.990116 -1.19935 6.217406 -2.38526 4.78924 -1.75986 6.0081 -2.15842 6.63117 -2.16837 0.8037 -0.0128 0.90917 0.0424 15.64514 8.19599 1.02104 0.56495 1.56579 1.15961 1.56579 1.70925 0 0.21814 -3.6538 9.91011 -8.11957 21.53771 -6.2982 16.39877 -8.19916 21.21114 -8.4744 21.45338 -0.46789 0.41179 -0.8512 0.39392 -1.74794 -0.0815z"
|
||||||
|
android:strokeWidth="0.139">
|
||||||
|
<aapt:attr
|
||||||
|
name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="76.74697"
|
||||||
|
android:startY="113.4246"
|
||||||
|
android:endX="110.6445"
|
||||||
|
android:endY="152.5006"
|
||||||
|
android:tileMode="clamp">
|
||||||
|
<item
|
||||||
|
android:color="#9B58DC"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#321C92"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
<aapt:attr
|
||||||
|
name="android:strokeColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="76.74697"
|
||||||
|
android:startY="113.4246"
|
||||||
|
android:endX="110.6445"
|
||||||
|
android:endY="152.5006"
|
||||||
|
android:tileMode="clamp">
|
||||||
|
<item
|
||||||
|
android:color="#9B58DC"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#321C92"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</group>
|
||||||
|
</vector>
|
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 109 KiB |
78
assets/graphics/icon.svg
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="142.12897mm"
|
||||||
|
height="142.12897mm"
|
||||||
|
viewBox="0 0 142.12897 142.12897"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="icon.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="2.4175295"
|
||||||
|
inkscape:cx="371.03994"
|
||||||
|
inkscape:cy="273.62644"
|
||||||
|
inkscape:window-width="2256"
|
||||||
|
inkscape:window-height="1427"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" /><defs
|
||||||
|
id="defs2"><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient3657"><stop
|
||||||
|
style="stop-color:#9b58dc;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3653" /><stop
|
||||||
|
style="stop-color:#321c92;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3655" /></linearGradient><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient945"><stop
|
||||||
|
style="stop-color:#9b58dc;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop941" /><stop
|
||||||
|
style="stop-color:#321c92;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop943" /></linearGradient><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient945"
|
||||||
|
id="linearGradient947"
|
||||||
|
x1="76.787094"
|
||||||
|
y1="113.40435"
|
||||||
|
x2="110.68458"
|
||||||
|
y2="152.48038"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(-0.04012535,0.02025786)" /><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3657"
|
||||||
|
id="linearGradient3659"
|
||||||
|
x1="76.787094"
|
||||||
|
y1="113.40435"
|
||||||
|
x2="110.68458"
|
||||||
|
y2="152.48038"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(-0.04012535,0.02025786)" /></defs><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-30.394373,-54.680428)"><path
|
||||||
|
style="fill:url(#linearGradient3659);fill-opacity:1;stroke:url(#linearGradient947);stroke-width:0.139;stroke-dasharray:none"
|
||||||
|
d="m 109.8808,153.22596 c -0.73146,-0.38777 -5.00657,-2.75679 -25.032416,-13.87149 -5.57273,-3.09297 -10.93823,-6.06723 -11.92332,-6.60948 -2.23728,-1.23152 -2.58105,-1.53456 -2.58105,-2.27528 0,-0.3879 0.89293,-2.87231 2.98561,-8.30689 1.64209,-4.2644 3.09426,-8.0014 3.22705,-8.30444 0.3024,-0.69008 0.78972,-1.27621 1.26573,-1.52236 0.44558,-0.23042 11.58052,-4.29685 12.14814,-4.43644 0.61355,-0.1509 1.1428,0.13977 1.45487,0.79901 0.14976,0.31638 0.77213,1.94934 1.38303,3.6288 0.6109,1.67945 1.52036,4.16275 2.02104,5.51844 1.14709,3.10604 1.18992,3.54589 0.3912,4.01771 -0.2117,0.12505 -1.58874,0.66539 -3.06009,1.20075 -1.47136,0.53536 -2.87533,1.08982 -3.11993,1.23213 -0.56422,0.32826 -0.64913,0.83523 -0.20815,1.24273 0.17523,0.16193 3.00434,1.77571 6.28691,3.58618 9.174936,5.06035 8.665596,4.83136 9.277626,4.17097 0.29987,-0.32356 5.78141,-14.266 6.09596,-15.50521 0.1344,-0.5295 0.11969,-0.60308 -0.16695,-0.83519 -0.39165,-0.31714 -0.335,-0.33071 -3.93797,0.9431 -3.56937,1.26192 -3.90926,1.28864 -4.38744,0.34488 -0.25108,-0.49556 -4.095796,-11.05481 -4.334456,-11.90432 -0.15438,-0.5495 0.0344,-1.0717 0.49701,-1.37482 0.19228,-0.12598 2.990116,-1.19935 6.217406,-2.38526 4.78924,-1.75986 6.0081,-2.15842 6.63117,-2.16837 0.8037,-0.0128 0.90917,0.0424 15.64514,8.19599 1.02104,0.56495 1.56579,1.15961 1.56579,1.70925 0,0.21814 -3.6538,9.91011 -8.11957,21.53771 -6.2982,16.39877 -8.19916,21.21114 -8.4744,21.45338 -0.46789,0.41179 -0.8512,0.39392 -1.74794,-0.0815 z"
|
||||||
|
id="path239" /></g></svg>
|
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 188 KiB |
BIN
assets/screenshots/5.app_opts.png
Normal file
After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 192 KiB |
BIN
assets/screenshots/6.app_webview.png
Normal file
After Width: | Height: | Size: 262 KiB |
@ -74,7 +74,6 @@
|
|||||||
"changeX": "Ändern {}",
|
"changeX": "Ändern {}",
|
||||||
"installUpdateApps": "Apps installieren/aktualisieren",
|
"installUpdateApps": "Apps installieren/aktualisieren",
|
||||||
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
||||||
"onlyWorksWithNonEVDApps": "Funktioniert nur bei Apps, deren Installationsstatus nicht automatisch erkannt werden kann (ungewöhnlich).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
@ -178,7 +177,6 @@
|
|||||||
"installedVersionX": "Installierte Version: {}",
|
"installedVersionX": "Installierte Version: {}",
|
||||||
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
|
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"removeAppQuestion": "App entfernen?",
|
|
||||||
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "App ID oder Name",
|
"appIdOrName": "App ID oder Name",
|
||||||
@ -209,8 +207,23 @@
|
|||||||
"addCategory": "Kategorie hinzufügen",
|
"addCategory": "Kategorie hinzufügen",
|
||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Speicherberechtigung verweigert",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
|
||||||
|
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
|
||||||
|
"removeFromObtainium": "Aus Obtainium entfernen",
|
||||||
|
"uninstallFromDevice": "Vom Gerät deinstallieren",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Funktioniert nur bei Apps mit deaktivierter Versionserkennung.",
|
||||||
|
"releaseDateAsVersion": "Veröffentlichungsdatum als Version verwenden",
|
||||||
|
"releaseDateAsVersionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert, aber ein Veröffentlichungsdatum verfügbar ist.",
|
||||||
|
"changes": "Änderungen",
|
||||||
|
"releaseDate": "Veröffentlichungsdatum",
|
||||||
|
"importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)",
|
||||||
|
"versionDetection": "Versionserkennung",
|
||||||
|
"standardVersionDetection": "Standardversionserkennung",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "App entfernen?",
|
||||||
|
"other": "App entfernen?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
||||||
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
|
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
|
||||||
|
@ -74,7 +74,6 @@
|
|||||||
"changeX": "Change {}",
|
"changeX": "Change {}",
|
||||||
"installUpdateApps": "Install/Update Apps",
|
"installUpdateApps": "Install/Update Apps",
|
||||||
"installUpdateSelectedApps": "Install/Update Selected Apps",
|
"installUpdateSelectedApps": "Install/Update Selected Apps",
|
||||||
"onlyWorksWithNonEVDApps": "Only works for Apps whose install status cannot be automatically detected (uncommon).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Mark {} Selected Apps as Updated?",
|
"markXSelectedAppsAsUpdated": "Mark {} Selected Apps as Updated?",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
@ -178,7 +177,6 @@
|
|||||||
"installedVersionX": "Installed Version: {}",
|
"installedVersionX": "Installed Version: {}",
|
||||||
"lastUpdateCheckX": "Last Update Check: {}",
|
"lastUpdateCheckX": "Last Update Check: {}",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"removeAppQuestion": "Remove App?",
|
|
||||||
"yesMarkUpdated": "Yes, Mark as Updated",
|
"yesMarkUpdated": "Yes, Mark as Updated",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "App ID or Name",
|
"appIdOrName": "App ID or Name",
|
||||||
@ -211,6 +209,21 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
|
"removeFromObtainium": "Remove from Obtainium",
|
||||||
|
"uninstallFromDevice": "Uninstall from Device",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.",
|
||||||
|
"releaseDateAsVersion": "Use Release Date as Version",
|
||||||
|
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
|
||||||
|
"changes": "Changes",
|
||||||
|
"releaseDate": "Release Date",
|
||||||
|
"importFromURLsInFile": "Import from URLs in File (like OPML)",
|
||||||
|
"versionDetection": "Version Detection",
|
||||||
|
"standardVersionDetection": "Standard version detection",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Remove App?",
|
||||||
|
"other": "Remove Apps?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Too many requests (rate limited) - try again in {} minute",
|
"one": "Too many requests (rate limited) - try again in {} minute",
|
||||||
"other": "Too many requests (rate limited) - try again in {} minutes"
|
"other": "Too many requests (rate limited) - try again in {} minutes"
|
||||||
|
271
assets/translations/fa.json
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "آدرس اینترنتی برنامه {} معتبر نیست",
|
||||||
|
"noReleaseFound": "نسخه مناسبی پیدا نشد",
|
||||||
|
"noVersionFound": "نمی توان نسخه منتشر شده را تعیین کرد",
|
||||||
|
"urlMatchesNoSource": "آدرس اینترنتی با منبع شناخته شده مطابقت ندارد",
|
||||||
|
"cantInstallOlderVersion": "نمی توان نسخه قدیمی یک برنامه را نصب کرد",
|
||||||
|
"appIdMismatch": "شناسه بسته دانلود شده با شناسه برنامه موجود مطابقت ندارد",
|
||||||
|
"functionNotImplemented": "این کلاس این تابع را پیاده سازی نکرده است",
|
||||||
|
"placeholder": "نگهدارنده مکان",
|
||||||
|
"someErrors": "برخی از خطاها رخ داده است",
|
||||||
|
"unexpectedError": "خطای غیرمنتظره",
|
||||||
|
"ok": "باشه",
|
||||||
|
"and": "و",
|
||||||
|
"startedBgUpdateTask": "شروع بررسی بروزرسانی BG",
|
||||||
|
"bgUpdateIgnoreAfterIs": "نادیده گرفتن بروزرسانی BG بعد از {} است",
|
||||||
|
"startedActualBGUpdateCheck": "بررسی بهروزرسانی واقعی BG آغاز شد",
|
||||||
|
"bgUpdateTaskFinished": "کار بررسی بهروزرسانی BG تمام شد",
|
||||||
|
"firstRun": "این اولین اجرای Obtainium است",
|
||||||
|
"settingUpdateCheckIntervalTo": "تنظیم فاصله بهروزرسانی روی {}",
|
||||||
|
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
||||||
|
"githubPATHint": "PAT باید در این قالب باشد: username:token",
|
||||||
|
"githubPATFormat": "username:token",
|
||||||
|
"githubPATLinkText": "درباره گیتهاب PATs",
|
||||||
|
"includePrereleases": "شامل نسخه های اولیه",
|
||||||
|
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
||||||
|
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
||||||
|
"invalidRegEx": "عبارت منظم نامعتبر است",
|
||||||
|
"noDescription": "بدون توضیحات",
|
||||||
|
"cancel": "لغو",
|
||||||
|
"continue": "ادامه دهید",
|
||||||
|
"requiredInBrackets": "(ضروری)",
|
||||||
|
"dropdownNoOptsError": "خطا: کشویی باید حداقل یک گزینه داشته باشد",
|
||||||
|
"colour": "رنگ",
|
||||||
|
"githubStarredRepos": "مخازن ستاره دار گیتهاب",
|
||||||
|
"uname": "نام کاربری",
|
||||||
|
"wrongArgNum": "تعداد آرگومان های ارائه شده اشتباه است",
|
||||||
|
"xIsTrackOnly": "{} فقط ردیابی",
|
||||||
|
"source": "منبع",
|
||||||
|
"app": "برنامه",
|
||||||
|
"appsFromSourceAreTrackOnly": "برنامههای این منبع «فقط ردیابی» هستند",
|
||||||
|
"youPickedTrackOnly": "شما گزینه ی «فقط ردیابی» را انتخاب کرده اید",
|
||||||
|
"trackOnlyAppDescription": "برنامه برای به روز رسانی ها ردیابی می شود، اما Obtainium قادر به دانلود یا نصب آن نخواهد بود.",
|
||||||
|
"cancelled": "لغو شد",
|
||||||
|
"appAlreadyAdded": "برنامه قبلاً اضافه شده است",
|
||||||
|
"alreadyUpToDateQuestion": "برنامه از قبل به روز شده است؟",
|
||||||
|
"addApp": "افزودن برنامه",
|
||||||
|
"appSourceURL": "آدرس اینترنتی منبع برنامه",
|
||||||
|
"error": "خطا",
|
||||||
|
"add": "اضافه کردن",
|
||||||
|
"searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
|
||||||
|
"search": "جستجو کردن",
|
||||||
|
"additionalOptsFor": "گزینه های اضافی برای {}",
|
||||||
|
"supportedSourcesBelow": "منابع پشتیبانی شده:",
|
||||||
|
"trackOnlyInBrackets": "«فقط ردیابی»",
|
||||||
|
"searchableInBrackets": "(قابل جستجو)",
|
||||||
|
"appsString": "برنامه ها",
|
||||||
|
"noApps": "برنامه ای وجود ندارد",
|
||||||
|
"noAppsForFilter": "برنامه ای برای فیلتر کردن وجود ندارد",
|
||||||
|
"byX": "توسط {}",
|
||||||
|
"percentProgress": "پیش رفتن: {}%",
|
||||||
|
"pleaseWait": "لطفا صبر کنید",
|
||||||
|
"updateAvailable": "بروزرسانی در دسترس",
|
||||||
|
"estimateInBracketsShort": "(تخمین)",
|
||||||
|
"notInstalled": "نصب نشده",
|
||||||
|
"estimateInBrackets": "(تخمین زدن)",
|
||||||
|
"selectAll": "انتخاب همه",
|
||||||
|
"deselectN": "لغو انتخاب {}",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف میشود اما روی دستگاه نصب میماند.",
|
||||||
|
"removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟",
|
||||||
|
"removeSelectedApps": "حذف برنامه های انتخاب شده",
|
||||||
|
"updateX": "به روز رسانی {}",
|
||||||
|
"installX": "نصب {}",
|
||||||
|
"markXTrackOnlyAsUpdated": "علامت {}\n(فقط ردیابی)\nبروز شده",
|
||||||
|
"changeX": "تغییر دادن {}",
|
||||||
|
"installUpdateApps": "نصب/بهروزرسانی برنامهها",
|
||||||
|
"installUpdateSelectedApps": "برنامههای انتخابی را نصب/بهروزرسانی کنید",
|
||||||
|
"markXSelectedAppsAsUpdated": "{} برنامه های انتخابی را به عنوان به روز علامت گذاری کنید؟",
|
||||||
|
"no": "خیر",
|
||||||
|
"yes": "بله",
|
||||||
|
"markSelectedAppsUpdated": "برنامه های انتخاب شده را به عنوان به روز علامت گذاری کنید",
|
||||||
|
"pinToTop": "پین به بالا",
|
||||||
|
"unpinFromTop": "برداشتن پین از بالا",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "وضعیت نصب برنامههای انتخابی بازنشانی شود؟",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "وضعیت نصب برنامههای انتخابشده بازنشانی میشود.\n\nاگر نسخه برنامه نشاندادهشده در Obtainium به دلیل بهروزرسانیهای ناموفق یا مشکلات دیگر نادرست باشد، میتواند کمک کند.",
|
||||||
|
"shareSelectedAppURLs": "اشتراک گذاری آدرس اینترنتی برنامه های انتخاب شده",
|
||||||
|
"resetInstallStatus": "بازنشانی وضعیت نصب",
|
||||||
|
"more": "بیشتر",
|
||||||
|
"removeOutdatedFilter": "فیلتر برنامه قدیمی را حذف کنید",
|
||||||
|
"showOutdatedOnly": "فقط برنامه های قدیمی را نشان دهید",
|
||||||
|
"filter": "فیلتر",
|
||||||
|
"filterActive": "فیلتر *",
|
||||||
|
"filterApps": "فیلتر کردن برنامه ها",
|
||||||
|
"appName": "نام برنامه",
|
||||||
|
"author": "سازنده",
|
||||||
|
"upToDateApps": "برنامه های به روز",
|
||||||
|
"nonInstalledApps": "برنامه های نصب نشده",
|
||||||
|
"importExport": "وادر کردن/صادر کردن",
|
||||||
|
"settings": "تنظیمات",
|
||||||
|
"exportedTo": "صادر کردن به{}",
|
||||||
|
"obtainiumExport": "صادرکردن Obtainium",
|
||||||
|
"invalidInput": "ورودی نامعتبر",
|
||||||
|
"importedX": "وارد شده {}",
|
||||||
|
"obtainiumImport": "واردکردن Obtainium",
|
||||||
|
"importFromURLList": "وارد کردن از فهرست آدرس اینترنتی",
|
||||||
|
"searchQuery": "جستجوی سوال",
|
||||||
|
"appURLList": "فهرست آدرس اینترنتی برنامه",
|
||||||
|
"line": "خط",
|
||||||
|
"searchX": "جستجو {}",
|
||||||
|
"noResults": "نتیجه ای پیدا نشد",
|
||||||
|
"importX": "وارد کردن {}",
|
||||||
|
"importedAppsIdDisclaimer": "ممکن است برنامههای وارد شده به اشتباه بهعنوان \"نصب نشده\" نشان داده شوند.\nبرای رفع این مشکل، آنها را دوباره از طریق Obtainium نصب کنید.\nاین نباید روی دادههای برنامه تأثیر بگذارد.\n\nفقط بر روی آدرس اینترنتی و روشهای وارد کردن شخص ثالث تأثیر میگذارد.",
|
||||||
|
"importErrors": "خطاهای وارد کردن",
|
||||||
|
"importedXOfYApps": "{} از {} برنامه وارد شد.",
|
||||||
|
"followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
|
||||||
|
"okay": "باشه",
|
||||||
|
"selectURL": "آدرس اینترنتی انتخاب شده",
|
||||||
|
"selectURLs": "آدرس های اینترنتی انتخاب شده",
|
||||||
|
"pick": "انتخاب",
|
||||||
|
"theme": "تم",
|
||||||
|
"dark": "تاریک",
|
||||||
|
"light": "روشن",
|
||||||
|
"followSystem": "هماهنگ با سیستم",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"appSortBy": "مرتب سازی برنامه بر اساس",
|
||||||
|
"authorName": "سازنده/اسم",
|
||||||
|
"nameAuthor": "اسم/سازنده",
|
||||||
|
"asAdded": "همانطور که اضافه شد",
|
||||||
|
"appSortOrder": "ترتیب مرتب سازی برنامه",
|
||||||
|
"ascending": "صعودی",
|
||||||
|
"descending": "نزولی",
|
||||||
|
"bgUpdateCheckInterval": "فاصله بررسی بهروزرسانی در پسزمینه",
|
||||||
|
"neverManualOnly": "هرگز - فقط دستی",
|
||||||
|
"appearance": "ظاهر",
|
||||||
|
"showWebInAppView": "نمایش صفحه وب منبع در نمای برنامه",
|
||||||
|
"pinUpdates": "بهروزرسانیها را به نمای بالای برنامهها پین کنید",
|
||||||
|
"updates": "به روز رسانی ها",
|
||||||
|
"sourceSpecific": "منبع خاص",
|
||||||
|
"appSource": "منبع برنامه",
|
||||||
|
"noLogs": "بدون گزارش",
|
||||||
|
"appLogs": "گزارش های برنامه",
|
||||||
|
"close": "بستن",
|
||||||
|
"share": "اشتراک گذاری",
|
||||||
|
"appNotFound": "برنامه پیدا نشد",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "صادر کردن-obtainium",
|
||||||
|
"pickAnAPK": "یک APK انتخاب کنید",
|
||||||
|
"appHasMoreThanOnePackage": "{} بیش از یک بسته دارد:",
|
||||||
|
"deviceSupportsXArch": "دستگاه شما از معماری پردازنده {} پشتیبانی میکند",
|
||||||
|
"deviceSupportsFollowingArchs": "دستگاه شما از معماری های پردازنده زیر پشتیبانی می کند:",
|
||||||
|
"warning": "اخطار",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "منبع برنامه \"{}\" است اما بسته انتشار از \"{}\" آمده است. ادامه هید؟",
|
||||||
|
"updatesAvailable": "بروزرسانی در دسترس ",
|
||||||
|
"updatesAvailableNotifDescription": "به کاربر اطلاع می دهد که به روز رسانی برای یک یا چند برنامه ردیابی شده توسط Obtainium در دسترس است",
|
||||||
|
"noNewUpdates": "به روز رسانی جدیدی وجود ندارد.",
|
||||||
|
"xHasAnUpdate": "{} یک به روز رسانی دارد.",
|
||||||
|
"appsUpdated": "برنامه ها به روز شدند",
|
||||||
|
"appsUpdatedNotifDescription": "به کاربر اطلاع می دهد که به روز رسانی یک یا چند برنامه در پس زمینه اعمال شده است",
|
||||||
|
"xWasUpdatedToY": "{} به {} به روز شد.",
|
||||||
|
"errorCheckingUpdates": "خطا در بررسی بهروزرسانیها",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "اعلانی که وقتی بررسی بهروزرسانی پسزمینه ناموفق است نشان میدهد",
|
||||||
|
"appsRemoved": "برنامه ها حذف شدند",
|
||||||
|
"appsRemovedNotifDescription": "به کاربر اطلاع می دهد که یک یا چند برنامه به دلیل خطا در هنگام بارگیری حذف شده است",
|
||||||
|
"xWasRemovedDueToErrorY": "{} به دلیل این خطا حذف شد: {}",
|
||||||
|
"completeAppInstallation": "نصب کامل برنامه",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "Obtainium باید برای نصب برنامه ها باز باشد",
|
||||||
|
"completeAppInstallationNotifDescription": "از کاربر میخواهد برای پایان نصب برنامه به Obtainium برگردد",
|
||||||
|
"checkingForUpdates": "بررسی بهروزرسانیها",
|
||||||
|
"checkingForUpdatesNotifDescription": "اعلان گذرا که هنگام بررسی به روز رسانی ظاهر می شود",
|
||||||
|
"pleaseAllowInstallPerm": "لطفاً به Obtainium اجازه دهید برنامهها را نصب کند",
|
||||||
|
"trackOnly": "فقط ردیابی",
|
||||||
|
"errorWithHttpStatusCode": "خطا {}",
|
||||||
|
"versionCorrectionDisabled": "تصحیح نسخه غیرفعال شد (به نظر می رسد افزونه کار نمی کند)",
|
||||||
|
"unknown": "ناشناخته",
|
||||||
|
"none": "هیچ",
|
||||||
|
"never": "هرگز",
|
||||||
|
"latestVersionX": "آخرین نسخه: {}",
|
||||||
|
"installedVersionX": "نسخه نصب شده: {}",
|
||||||
|
"lastUpdateCheckX": "بررسی آخرین بهروزرسانی: {}",
|
||||||
|
"remove": "حذف",
|
||||||
|
"yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده",
|
||||||
|
"fdroid": "F-Droid",
|
||||||
|
"appIdOrName": "شناسه یا نام برنامه",
|
||||||
|
"appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد",
|
||||||
|
"reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد",
|
||||||
|
"fdroidThirdPartyRepo": "مخازن شخص ثالث F-Droid",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "نصب",
|
||||||
|
"markInstalled": "علامت گذاری به عنوان نصب شده",
|
||||||
|
"update": "به روز رسانی",
|
||||||
|
"markUpdated": "علامت گذاری به روز شد",
|
||||||
|
"additionalOptions": "گزینه های اضافی",
|
||||||
|
"disableVersionDetection": "غیرفعال کردن تشخیص نسخه",
|
||||||
|
"noVersionDetectionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند.",
|
||||||
|
"downloadingX": "در حال دانلود {}",
|
||||||
|
"downloadNotifDescription": "کاربر را از پیشرفت دانلود یک برنامه مطلع می کند",
|
||||||
|
"noAPKFound": "APK پیدا نشد فایل",
|
||||||
|
"noVersionDetection": "بدون تشخیص نسخه",
|
||||||
|
"categorize": "دسته بندی کردن",
|
||||||
|
"categories": "دسته بندی ها",
|
||||||
|
"category": "دسته بندی",
|
||||||
|
"noCategory": "بدون دسته بندی",
|
||||||
|
"noCategories": "بدون دسته بندی ها",
|
||||||
|
"deleteCategoriesQuestion": "دسته بندی ها حذف شوند؟",
|
||||||
|
"categoryDeleteWarning": "همه برنامهها در دستههای حذف شده روی دستهبندی نشده تنظیم میشوند.",
|
||||||
|
"addCategory": "اضافه کردن دسته",
|
||||||
|
"label": "برچسب",
|
||||||
|
"language": "زبان",
|
||||||
|
"storagePermissionDenied": "مجوز ذخیره سازی رد شد",
|
||||||
|
"selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.",
|
||||||
|
"filterAPKsByRegEx": "فایلهای APK را با نظم فیلتر کنید",
|
||||||
|
"removeFromObtainium": "از Obtainium حذف کنید",
|
||||||
|
"uninstallFromDevice": "حذف نصب از دستگاه",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "فقط برای برنامههایی کار میکند که تشخیص نسخه غیرفعال است.",
|
||||||
|
"releaseDateAsVersion": "از تاریخ انتشار به عنوان نسخه استفاده کنید",
|
||||||
|
"releaseDateAsVersionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند، اما تاریخ انتشار در دسترس است.",
|
||||||
|
"changes": "تغییرات",
|
||||||
|
"releaseDate": "تاریخ انتشار",
|
||||||
|
"importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)",
|
||||||
|
"versionDetection": "تشخیص نسخه",
|
||||||
|
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "برنامه حذف شود؟",
|
||||||
|
"other": "برنامه ها حذف شوند؟"
|
||||||
|
},
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "درخواستهای بسیار زیاد (نرخ محدود) - {} دقیقه دیگر دوباره امتحان کنید",
|
||||||
|
"other": "درخواست های بسیار زیاد (نرخ محدود) - بعد از {} دقیقه دوباره امتحان کنید"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "بررسی بهروزرسانی BG با یک {} مواجه شد، یک بررسی مجدد را در {} دقیقه برنامهریزی میکند",
|
||||||
|
"other": "بررسی بهروزرسانی BG با {} مواجه شد، یک بررسی مجدد را در {} دقیقه برنامهریزی میکند"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "بررسی بهروزرسانی BG پیدا شد {} بهروزرسانی - در صورت نیاز به کاربر اطلاع میدهد",
|
||||||
|
"other": "بررسی بهروزرسانی BG {} بهروزرسانیهای یافت شده - در صورت نیاز به کاربر اطلاع میدهد"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "برنامه {}",
|
||||||
|
"other": "{} برنامه ها"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} آدرس اینترنتی",
|
||||||
|
"other": "{} آدرس های اینترنتی"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} دقیقه",
|
||||||
|
"other": "{} دقیقه"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} ساعت",
|
||||||
|
"other": "{} ساعت"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} روز",
|
||||||
|
"other": "{} روز"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "گزارش {n} پاک شد (قبل از = {پیش از}، بعد = {بعد})",
|
||||||
|
"other": "{n} گزارش پاک شد (قبل از = {پیش از}، بعد = {بعد})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "{} و 1 برنامه دیگر بهروزرسانی دارند.",
|
||||||
|
"other": "{} و {} برنامه دیگر به روز رسانی دارند."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "{} و 1 برنامه دیگر به روز شدند.",
|
||||||
|
"other": "{} و {} برنامه دیگر به روز شدند."
|
||||||
|
}
|
||||||
|
}
|
271
assets/translations/fr.json
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "URL d'application {} invalide",
|
||||||
|
"noReleaseFound": "Impossible de trouver une version appropriée",
|
||||||
|
"noVersionFound": "Impossible de déterminer la version de la version",
|
||||||
|
"urlMatchesNoSource": "L'URL ne correspond pas à une source connue",
|
||||||
|
"cantInstallOlderVersion": "Impossible d'installer une ancienne version d'une application",
|
||||||
|
"appIdMismatch": "L'ID de paquet téléchargé ne correspond pas à l'ID de l'application existante",
|
||||||
|
"functionNotImplemented": "Cette classe n'a pas implémenté cette fonction",
|
||||||
|
"placeholder": "Espace réservé",
|
||||||
|
"someErrors": "Des erreurs se sont produites",
|
||||||
|
"unexpectedError": "Erreur inattendue",
|
||||||
|
"ok": "Okay",
|
||||||
|
"and": "et",
|
||||||
|
"startedBgUpdateTask": "Démarrage de la tâche de vérification de mise à jour en arrière-plan",
|
||||||
|
"bgUpdateIgnoreAfterIs": "Mise à jour en arrière-plan est ignoré après {}",
|
||||||
|
"startedActualBGUpdateCheck": "Démarrage de la vérification de la mise à jour en arrière-plan",
|
||||||
|
"bgUpdateTaskFinished": "Tâche de vérification de la mise à jour en arrière-plan terminée",
|
||||||
|
"firstRun": "Il s'agit de la toute première exécution d'Obtainium",
|
||||||
|
"settingUpdateCheckIntervalTo": "Définition de l'intervalle de mise à jour sur {}",
|
||||||
|
"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",
|
||||||
|
"githubPATLinkText": "À propos des JAP GitHub",
|
||||||
|
"includePrereleases": "Inclure les avant-premières",
|
||||||
|
"fallbackToOlderReleases": "Retour aux anciennes versions",
|
||||||
|
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
|
||||||
|
"invalidRegEx": "Expression régulière invalide",
|
||||||
|
"noDescription": "Pas de description",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"continue": "Continuer",
|
||||||
|
"requiredInBrackets": "(Requis)",
|
||||||
|
"dropdownNoOptsError": "ERREUR : LE DÉROULEMENT DOIT AVOIR AU MOINS UNE OPT",
|
||||||
|
"colour": "Couleur",
|
||||||
|
"githubStarredRepos": "Dépôts étoilés GitHub",
|
||||||
|
"uname": "Nom d'utilisateur",
|
||||||
|
"wrongArgNum": "Mauvais nombre d'arguments fournis",
|
||||||
|
"xIsTrackOnly": "{} est en 'Suivi uniquement'",
|
||||||
|
"source": "Source",
|
||||||
|
"app": "Application",
|
||||||
|
"appsFromSourceAreTrackOnly": "Les applications de cette source sont en 'Suivi uniquement'.",
|
||||||
|
"youPickedTrackOnly": "Vous avez sélectionné l'option 'Suivi uniquement'.",
|
||||||
|
"trackOnlyAppDescription": "L'application sera suivie pour les mises à jour, mais Obtainium ne pourra pas la télécharger ou l'installer.",
|
||||||
|
"cancelled": "Annulé",
|
||||||
|
"appAlreadyAdded": "Application déjà ajoutée",
|
||||||
|
"alreadyUpToDateQuestion": "Application déjà à jour ?",
|
||||||
|
"addApp": "Ajouter une application",
|
||||||
|
"appSourceURL": "URL de la source de l'application",
|
||||||
|
"error": "Erreur",
|
||||||
|
"add": "Ajoutée",
|
||||||
|
"searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)",
|
||||||
|
"search": "Rechercher",
|
||||||
|
"additionalOptsFor": "Options supplémentaires pour {}",
|
||||||
|
"supportedSourcesBelow": "Sources prises en charge :",
|
||||||
|
"trackOnlyInBrackets": "(Suivi uniquement)",
|
||||||
|
"searchableInBrackets": "(Recherchable)",
|
||||||
|
"appsString": "Applications",
|
||||||
|
"noApps": "Aucune application",
|
||||||
|
"noAppsForFilter": "Aucune application pour le filtre",
|
||||||
|
"byX": "Par {}",
|
||||||
|
"percentProgress": "Progrès: {}%",
|
||||||
|
"pleaseWait": "Veuillez patienter",
|
||||||
|
"updateAvailable": "Mise à jour disponible",
|
||||||
|
"estimateInBracketsShort": "(Est.)",
|
||||||
|
"notInstalled": "Pas installé",
|
||||||
|
"estimateInBrackets": "(Estimation)",
|
||||||
|
"selectAll": "Tout sélectionner",
|
||||||
|
"deselectN": "Déselectionner {}",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.",
|
||||||
|
"removeSelectedAppsQuestion": "Supprimer les applications sélectionnées ?",
|
||||||
|
"removeSelectedApps": "Supprimer les applications sélectionnées",
|
||||||
|
"updateX": "Mise à jour {}",
|
||||||
|
"installX": "Installer {}",
|
||||||
|
"markXTrackOnlyAsUpdated": "Marquer {}\n(Suivi uniquement)\nas mis à jour",
|
||||||
|
"changeX": "Changer {}",
|
||||||
|
"installUpdateApps": "Installer/Mettre à jour les applications",
|
||||||
|
"installUpdateSelectedApps": "Installer/Mettre à jour les applications sélectionnées",
|
||||||
|
"markXSelectedAppsAsUpdated": "Marquer {} les applications sélectionnées comme mises à jour ?",
|
||||||
|
"no": "Non",
|
||||||
|
"yes": "Oui",
|
||||||
|
"markSelectedAppsUpdated": "Marquer les applications sélectionnées comme mises à jour",
|
||||||
|
"pinToTop": "Épingler en haut",
|
||||||
|
"unpinFromTop": "Détacher du haut",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "Réinitialiser l'état d'installation des applications sélectionnées ?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "L'état d'installation de toutes les applications sélectionnées sera réinitialisé.\n\nCela peut aider lorsque la version de l'application affichée dans Obtainium est incorrecte en raison d'échecs de mises à jour ou d'autres problèmes.",
|
||||||
|
"shareSelectedAppURLs": "Partager les URL d'application sélectionnées",
|
||||||
|
"resetInstallStatus": "Réinitialiser le statut d'installation",
|
||||||
|
"more": "Plus",
|
||||||
|
"removeOutdatedFilter": "Supprimer le filtre d'application obsolète",
|
||||||
|
"showOutdatedOnly": "Afficher uniquement les applications obsolètes",
|
||||||
|
"filter": "Filtre",
|
||||||
|
"filterActive": "Filtre *",
|
||||||
|
"filterApps": "Filtrer les applications",
|
||||||
|
"appName": "Nom de l'application",
|
||||||
|
"author": "Auteur",
|
||||||
|
"upToDateApps": "Applications à jour",
|
||||||
|
"nonInstalledApps": "Applications non installées",
|
||||||
|
"importExport": "Importer/Exporter",
|
||||||
|
"settings": "Paramètres",
|
||||||
|
"exportedTo": "Exporté vers {}",
|
||||||
|
"obtainiumExport": "Exportation d'Obtainium",
|
||||||
|
"invalidInput": "Entrée invalide",
|
||||||
|
"importedX": "Importé {}",
|
||||||
|
"obtainiumImport": "Importation d'Obtainium",
|
||||||
|
"importFromURLList": "Importer à partir de la liste d'URL",
|
||||||
|
"searchQuery": "Requête de recherche",
|
||||||
|
"appURLList": "Liste d'URL d'application",
|
||||||
|
"line": "Queue",
|
||||||
|
"searchX": "Rechercher {}",
|
||||||
|
"noResults": "Aucun résultat trouvé",
|
||||||
|
"importX": "Importer {}",
|
||||||
|
"importedAppsIdDisclaimer": "Les applications importées peuvent s'afficher à tort comme \"Non installées\".\nPour résoudre ce problème, réinstallez-les via Obtainium.\nCela ne devrait pas affecter les données de l'application.\n\nN'affecte que les URL et les méthodes d'importation tierces.",
|
||||||
|
"importErrors": "Erreurs d'importation",
|
||||||
|
"importedXOfYApps": "{} sur {} applications importées.",
|
||||||
|
"followingURLsHadErrors": "Les URL suivantes comportaient des erreurs :",
|
||||||
|
"okay": "Okay",
|
||||||
|
"selectURL": "Sélectionnez l'URL",
|
||||||
|
"selectURLs": "Sélectionnez les URLs",
|
||||||
|
"pick": "Prendre",
|
||||||
|
"theme": "Thème",
|
||||||
|
"dark": "Sombre",
|
||||||
|
"light": "Clair",
|
||||||
|
"followSystem": "Suivre le système",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"appSortBy": "Applications triées par",
|
||||||
|
"authorName": "Auteur/Nom",
|
||||||
|
"nameAuthor": "Nom/Auteur",
|
||||||
|
"asAdded": "Comme ajouté",
|
||||||
|
"appSortOrder": "Ordre de tri des applications",
|
||||||
|
"ascending": "Ascendant",
|
||||||
|
"descending": "Descendanr",
|
||||||
|
"bgUpdateCheckInterval": "Intervalle de vérification des mises à jour en arrière-plan",
|
||||||
|
"neverManualOnly": "Jamais - Manuel uniquement",
|
||||||
|
"appearance": "Apparence",
|
||||||
|
"showWebInAppView": "Afficher la page Web source dans la vue de l'application",
|
||||||
|
"pinUpdates": "Épingler les mises à jour dans la vue Top des applications",
|
||||||
|
"updates": "Mises à jour",
|
||||||
|
"sourceSpecific": "Spécifique à la source",
|
||||||
|
"appSource": "Source de l'application",
|
||||||
|
"noLogs": "Aucun journal",
|
||||||
|
"appLogs": "Journaux d'application",
|
||||||
|
"close": "Fermer",
|
||||||
|
"share": "Partager",
|
||||||
|
"appNotFound": "Application introuvable",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
|
"pickAnAPK": "Choisissez un APK",
|
||||||
|
"appHasMoreThanOnePackage": "{} a plus d'un paquet :",
|
||||||
|
"deviceSupportsXArch": "Votre appareil prend en charge l'architecture de processeur {}.",
|
||||||
|
"deviceSupportsFollowingArchs": "Votre appareil prend en charge les architectures CPU suivantes :",
|
||||||
|
"warning": "Avertissement",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "La source de l'application est '{}' mais le paquet de version provient de '{}'. Continuer?",
|
||||||
|
"updatesAvailable": "Mises à jour disponibles",
|
||||||
|
"updatesAvailableNotifDescription": "Avertit l'utilisateur que des mises à jour sont disponibles pour une ou plusieurs applications suivies par Obtainium",
|
||||||
|
"noNewUpdates": "Aucune nouvelle mise à jour.",
|
||||||
|
"xHasAnUpdate": "{} a une mise à jour.",
|
||||||
|
"appsUpdated": "Applications mises à jour",
|
||||||
|
"appsUpdatedNotifDescription": "Avertit l'utilisateur que les mises à jour d'une ou plusieurs applications ont été appliquées en arrière-plan",
|
||||||
|
"xWasUpdatedToY": "{} a été mis à jour pour {}.",
|
||||||
|
"errorCheckingUpdates": "Erreur lors de la vérification des mises à jour",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "Une notification qui s'affiche lorsque la vérification de la mise à jour en arrière-plan échoue",
|
||||||
|
"appsRemoved": "Applications supprimées",
|
||||||
|
"appsRemovedNotifDescription": "Avertit l'utilisateur qu'une ou plusieurs applications ont été supprimées en raison d'erreurs lors de leur chargement",
|
||||||
|
"xWasRemovedDueToErrorY": "{} a été supprimé en raison de cette erreur : {}",
|
||||||
|
"completeAppInstallation": "Installation complète de l'application",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "Obtainium doit être ouvert pour installer des applications",
|
||||||
|
"completeAppInstallationNotifDescription": "Demande à l'utilisateur de retourner sur Obtainium pour terminer l'installation d'une application",
|
||||||
|
"checkingForUpdates": "Vérification des mises à jour",
|
||||||
|
"checkingForUpdatesNotifDescription": "Notification transitoire qui apparaît lors de la recherche de mises à jour",
|
||||||
|
"pleaseAllowInstallPerm": "Veuillez autoriser Obtainium à installer des applications",
|
||||||
|
"trackOnly": "Suivi uniquement",
|
||||||
|
"errorWithHttpStatusCode": "Erreur {}",
|
||||||
|
"versionCorrectionDisabled": "Correction de version désactivée (le plugin ne semble pas fonctionner)",
|
||||||
|
"unknown": "Inconnu",
|
||||||
|
"none": "Aucun",
|
||||||
|
"never": "Jamais",
|
||||||
|
"latestVersionX": "Dernière version: {}",
|
||||||
|
"installedVersionX": "Version installée : {}",
|
||||||
|
"lastUpdateCheckX": "Vérification de la dernière mise à jour : {}",
|
||||||
|
"remove": "Retirer",
|
||||||
|
"yesMarkUpdated": "Oui, marquer comme mis à jour",
|
||||||
|
"fdroid": "F-Droid",
|
||||||
|
"appIdOrName": "ID ou nom de l'application",
|
||||||
|
"appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
|
||||||
|
"reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications",
|
||||||
|
"fdroidThirdPartyRepo": "Dépôt tiers F-Droid",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "Installer",
|
||||||
|
"markInstalled": "Marquer installée",
|
||||||
|
"update": "Mettre à jour",
|
||||||
|
"markUpdated": "Marquer à jour",
|
||||||
|
"additionalOptions": "Options additionelles",
|
||||||
|
"disableVersionDetection": "Désactiver la détection de version",
|
||||||
|
"noVersionDetectionExplanation": "Cette option ne doit être utilisée que pour les applications où la détection de version ne fonctionne pas correctement.",
|
||||||
|
"downloadingX": "Téléchargement {}",
|
||||||
|
"downloadNotifDescription": "Avertit l'utilisateur de la progression du téléchargement d'une application",
|
||||||
|
"noAPKFound": "Aucun APK trouvé",
|
||||||
|
"noVersionDetection": "Pas de détection de version",
|
||||||
|
"categorize": "Catégoriser",
|
||||||
|
"categories": "Catégories",
|
||||||
|
"category": "Catégorie",
|
||||||
|
"noCategory": "No Category",
|
||||||
|
"noCategories": "Aucune catégorie",
|
||||||
|
"deleteCategoriesQuestion": "Supprimer les catégories ?",
|
||||||
|
"categoryDeleteWarning": "Toutes les applications dans les catégories supprimées seront définies sur non catégorisées.",
|
||||||
|
"addCategory": "Ajouter une catégorie",
|
||||||
|
"label": "Étiquette",
|
||||||
|
"language": "Langue",
|
||||||
|
"storagePermissionDenied": "Autorisation de stockage refusée",
|
||||||
|
"selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.",
|
||||||
|
"filterAPKsByRegEx": "Filtrer les APK par expression régulière",
|
||||||
|
"removeFromObtainium": "Supprimer d'Obtainium",
|
||||||
|
"uninstallFromDevice": "Désinstaller de l'appareil",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Fonctionne uniquement pour les applications avec la détection de version désactivée.",
|
||||||
|
"releaseDateAsVersion": "Utiliser la date de sortie comme version",
|
||||||
|
"releaseDateAsVersionExplanation": "Cette option ne doit être utilisée que pour les applications où la détection de version ne fonctionne pas correctement, mais une date de sortie est disponible.",
|
||||||
|
"changes": "Changements",
|
||||||
|
"releaseDate": "Date de sortie",
|
||||||
|
"importFromURLsInFile": "Importer à partir d'URL dans un fichier (comme OPML)",
|
||||||
|
"versionDetection": "Détection des versions",
|
||||||
|
"standardVersionDetection": "Détection de version standard",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Supprimer l'application ?",
|
||||||
|
"other": "Supprimer les applications ?"
|
||||||
|
},
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "Trop de demandes (taux limité) - réessayez dans {} minute",
|
||||||
|
"other": "Trop de demandes (taux limité) - réessayez dans {} minutes"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "La vérification de la mise à jour en arrière-plan a rencontré un {}, planifiera une nouvelle tentative de vérification dans {} minute",
|
||||||
|
"other": "La vérification de la mise à jour en arrière-plan a rencontré un {}, planifiera une nouvelle tentative de vérification dans {} minutes"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "La vérification des mises à jour en arrière-plan trouvée {} mise à jour - avertira l'utilisateur si nécessaire",
|
||||||
|
"other": "La vérification des mises à jour en arrière-plan a trouvé {} mises à jour - avertira l'utilisateur si nécessaire"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{} Application",
|
||||||
|
"other": "{} Applications"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} URL",
|
||||||
|
"other": "{} URLs"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} Minute",
|
||||||
|
"other": "{} Minutes"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} Heure",
|
||||||
|
"other": "{} Heures"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} Jour",
|
||||||
|
"other": "{} Jours"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "{n} journal effacé (avant = {before}, après = {after})",
|
||||||
|
"other": "{n} journaux effacés (avant = {before}, après = {after})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "{} et 1 autre application ont des mises à jour.",
|
||||||
|
"other": "{} et {} autres applications ont des mises à jour."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "{} et 1 autre application ont été mises à jour.",
|
||||||
|
"other": "{} et {} autres applications ont été mises à jour."
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@
|
|||||||
"githubStarredRepos": "GitHub Csillagos Repo-k",
|
"githubStarredRepos": "GitHub Csillagos Repo-k",
|
||||||
"uname": "Felh.név",
|
"uname": "Felh.név",
|
||||||
"wrongArgNum": "Rossz számú argumentumot adott meg",
|
"wrongArgNum": "Rossz számú argumentumot adott meg",
|
||||||
"xIsTrackOnly": "A(z) {} csak nyomkövethető",
|
"xIsTrackOnly": "A(z) {} csak nyomonkövethető",
|
||||||
"source": "Forrás",
|
"source": "Forrás",
|
||||||
"app": "App",
|
"app": "App",
|
||||||
"appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.",
|
"appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.",
|
||||||
@ -56,7 +56,7 @@
|
|||||||
"appsString": "Appok",
|
"appsString": "Appok",
|
||||||
"noApps": "Nincs App",
|
"noApps": "Nincs App",
|
||||||
"noAppsForFilter": "Nincsenek appok a szűrőhöz",
|
"noAppsForFilter": "Nincsenek appok a szűrőhöz",
|
||||||
"byX": "{} által",
|
"byX": "Fejlesztő: {}",
|
||||||
"percentProgress": "Folyamat: {}%",
|
"percentProgress": "Folyamat: {}%",
|
||||||
"pleaseWait": "Kis türelmet",
|
"pleaseWait": "Kis türelmet",
|
||||||
"updateAvailable": "Frissítés érhető el",
|
"updateAvailable": "Frissítés érhető el",
|
||||||
@ -74,12 +74,11 @@
|
|||||||
"changeX": "Változás {}",
|
"changeX": "Változás {}",
|
||||||
"installUpdateApps": "Appok telepítése/frissítése",
|
"installUpdateApps": "Appok telepítése/frissítése",
|
||||||
"installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat",
|
"installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat",
|
||||||
"onlyWorksWithNonEVDApps": "Csak azoknál az alkalmazásoknál működik, amelyek telepítési állapota nem észlelhető autom. (nem gyakori).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?",
|
"markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?",
|
||||||
"no": "Nem",
|
"no": "Nem",
|
||||||
"yes": "Igen",
|
"yes": "Igen",
|
||||||
"markSelectedAppsUpdated": "Jelölje meg a kiválasztott appokat frissítettként",
|
"markSelectedAppsUpdated": "Jelölje meg a kiválasztott appokat frissítettként",
|
||||||
"pinToTop": "Rögzítés a felülre",
|
"pinToTop": "Rögzítés felülre",
|
||||||
"unpinFromTop": "Eltávolít felülről",
|
"unpinFromTop": "Eltávolít felülről",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Visszaállítja a kiválasztott appok telepítési állapotát?",
|
"resetInstallStatusForSelectedAppsQuestion": "Visszaállítja a kiválasztott appok telepítési állapotát?",
|
||||||
"installStatusOfXWillBeResetExplanation": "A kiválasztott appok telepítési állapota visszaáll.\n\nEz akkor segíthet, ha az Obtainiumban megjelenített app verzió hibás, frissítések vagy egyéb problémák miatt.",
|
"installStatusOfXWillBeResetExplanation": "A kiválasztott appok telepítési állapota visszaáll.\n\nEz akkor segíthet, ha az Obtainiumban megjelenített app verzió hibás, frissítések vagy egyéb problémák miatt.",
|
||||||
@ -178,7 +177,6 @@
|
|||||||
"installedVersionX": "Telepített verzió: {}",
|
"installedVersionX": "Telepített verzió: {}",
|
||||||
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
"removeAppQuestion": "Eltávolítja az alkalmazást?",
|
|
||||||
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "App ID vagy név",
|
"appIdOrName": "App ID vagy név",
|
||||||
@ -207,9 +205,24 @@
|
|||||||
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
|
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
|
||||||
"addCategory": "Új kategória",
|
"addCategory": "Új kategória",
|
||||||
"label": "Címke",
|
"label": "Címke",
|
||||||
"language": "Language",
|
"language": "Nyelv",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Tárhely engedély megtagadva",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
|
||||||
|
"filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
|
||||||
|
"removeFromObtainium": "Eltávolítás az Obtainiumból",
|
||||||
|
"uninstallFromDevice": "Eltávolítás a készülékről",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.",
|
||||||
|
"releaseDateAsVersion": "Használja a Kiadás dátumát, mint verziót",
|
||||||
|
"releaseDateAsVersionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzió érzékelése nem működik megfelelően, de elérhető a kiadás dátuma.",
|
||||||
|
"changes": "Változtatások",
|
||||||
|
"releaseDate": "Kiadás dátuma",
|
||||||
|
"importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)",
|
||||||
|
"versionDetection": "Verzió érzékelés",
|
||||||
|
"standardVersionDetection": "Alapért. verzió érzékelés",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Eltávolítja az alkalmazást?",
|
||||||
|
"other": "Eltávolítja az alkalmazást?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
||||||
"other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
|
"other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
|
||||||
|
@ -56,9 +56,9 @@
|
|||||||
"appsString": "App",
|
"appsString": "App",
|
||||||
"noApps": "Nessuna App",
|
"noApps": "Nessuna App",
|
||||||
"noAppsForFilter": "Nessuna App per i filtri selezionati",
|
"noAppsForFilter": "Nessuna App per i filtri selezionati",
|
||||||
"byX": "Da {}",
|
"byX": "Di {}",
|
||||||
"percentProgress": "Progresso: {}%",
|
"percentProgress": "Progresso: {}%",
|
||||||
"pleaseWait": "Attendere prego",
|
"pleaseWait": "In attesa",
|
||||||
"updateAvailable": "Aggiornamento disponibile",
|
"updateAvailable": "Aggiornamento disponibile",
|
||||||
"estimateInBracketsShort": "(prev.)",
|
"estimateInBracketsShort": "(prev.)",
|
||||||
"notInstalled": "Non installato",
|
"notInstalled": "Non installato",
|
||||||
@ -74,7 +74,6 @@
|
|||||||
"changeX": "Modifica {}",
|
"changeX": "Modifica {}",
|
||||||
"installUpdateApps": "Installa/Aggiorna App",
|
"installUpdateApps": "Installa/Aggiorna App",
|
||||||
"installUpdateSelectedApps": "Installa/Aggiorna le App selezionate",
|
"installUpdateSelectedApps": "Installa/Aggiorna le App selezionate",
|
||||||
"onlyWorksWithNonEVDApps": "Funziona solo per le App il cui stato d'installazione non può essere rilevato automaticamente (inconsueto).",
|
|
||||||
"markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?",
|
"markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"yes": "Sì",
|
"yes": "Sì",
|
||||||
@ -95,7 +94,7 @@
|
|||||||
"author": "Autore",
|
"author": "Autore",
|
||||||
"upToDateApps": "App aggiornate",
|
"upToDateApps": "App aggiornate",
|
||||||
"nonInstalledApps": "App non installate",
|
"nonInstalledApps": "App non installate",
|
||||||
"importExport": "Importa - Esporta",
|
"importExport": "Importa/Esporta",
|
||||||
"settings": "Impostazioni",
|
"settings": "Impostazioni",
|
||||||
"exportedTo": "Esportato in {}",
|
"exportedTo": "Esportato in {}",
|
||||||
"obtainiumExport": "Esporta da Obtainium",
|
"obtainiumExport": "Esporta da Obtainium",
|
||||||
@ -178,7 +177,6 @@
|
|||||||
"installedVersionX": "Versione installata: {}",
|
"installedVersionX": "Versione installata: {}",
|
||||||
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
||||||
"remove": "Rimuovi",
|
"remove": "Rimuovi",
|
||||||
"removeAppQuestion": "Rimuovere l'App?",
|
|
||||||
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "ID o nome dell'App",
|
"appIdOrName": "ID o nome dell'App",
|
||||||
@ -209,8 +207,23 @@
|
|||||||
"addCategory": "Aggiungi categoria",
|
"addCategory": "Aggiungi categoria",
|
||||||
"label": "Etichetta",
|
"label": "Etichetta",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Accesso ai file non autorizzato",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.",
|
||||||
|
"filterAPKsByRegEx": "Filtra file APK con espressioni regolari",
|
||||||
|
"removeFromObtainium": "Rimuovi da Obtainium",
|
||||||
|
"uninstallFromDevice": "Disinstalla dal dispositivo",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Funziona solo per le App con il rilevamento della versione disattivato.",
|
||||||
|
"releaseDateAsVersion": "Usa data di rilascio come versione",
|
||||||
|
"releaseDateAsVersionExplanation": "Questa opzione dovrebbe essere usata solo per le App in cui il rilevamento della versione non funziona correttamente, ma è disponibile una data di rilascio.",
|
||||||
|
"changes": "Novità",
|
||||||
|
"releaseDate": "Data di rilascio",
|
||||||
|
"importFromURLsInFile": "Import from URLs in File (like OPML)",
|
||||||
|
"versionDetection": "Version Detection",
|
||||||
|
"standardVersionDetection": "Standard version detection",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Rimuovere l'App?",
|
||||||
|
"other": "Rimuovere le App?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
||||||
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません",
|
"appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません",
|
||||||
"functionNotImplemented": "このクラスはこの機能を実装していません",
|
"functionNotImplemented": "このクラスはこの機能を実装していません",
|
||||||
"placeholder": "プレースホルダー",
|
"placeholder": "プレースホルダー",
|
||||||
"someErrors": "いくつかのエラーが発生しました",
|
"someErrors": "何らかのエラーが発生しました",
|
||||||
"unexpectedError": "予期せぬエラーが発生しました",
|
"unexpectedError": "予期せぬエラーが発生しました",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"and": "と",
|
"and": "と",
|
||||||
@ -74,7 +74,6 @@
|
|||||||
"changeX": "{} を変更する",
|
"changeX": "{} を変更する",
|
||||||
"installUpdateApps": "アプリのインストール/アップデート",
|
"installUpdateApps": "アプリのインストール/アップデート",
|
||||||
"installUpdateSelectedApps": "選択したアプリのインストール/アップデート",
|
"installUpdateSelectedApps": "選択したアプリのインストール/アップデート",
|
||||||
"onlyWorksWithNonEVDApps": "インストール状況を自動検出できないアプリ(一般的でないもの)のみ動作します。",
|
|
||||||
"markXSelectedAppsAsUpdated": "{}個の選択したアプリをアップデート済みとしてマークしますか?",
|
"markXSelectedAppsAsUpdated": "{}個の選択したアプリをアップデート済みとしてマークしますか?",
|
||||||
"no": "いいえ",
|
"no": "いいえ",
|
||||||
"yes": "はい",
|
"yes": "はい",
|
||||||
@ -82,7 +81,7 @@
|
|||||||
"pinToTop": "トップに固定",
|
"pinToTop": "トップに固定",
|
||||||
"unpinFromTop": "トップから固定解除",
|
"unpinFromTop": "トップから固定解除",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
||||||
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗するなどして、Obtainiumに表示されるアプリのバージョンが正しくない場合に役立ちます。",
|
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。",
|
||||||
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
||||||
"resetInstallStatus": "インストール状態をリセットする",
|
"resetInstallStatus": "インストール状態をリセットする",
|
||||||
"more": "もっと見る",
|
"more": "もっと見る",
|
||||||
@ -108,8 +107,8 @@
|
|||||||
"line": "行",
|
"line": "行",
|
||||||
"searchX": "{}で検索",
|
"searchX": "{}で検索",
|
||||||
"noResults": "結果は見つかりませんでした",
|
"noResults": "結果は見つかりませんでした",
|
||||||
"importX": "{}をインポートする",
|
"importX": "{}をインポート",
|
||||||
"importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティーのインポートメソッドにのみ影響します。",
|
"importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティのインポートメソッドにのみ影響します。",
|
||||||
"importErrors": "インポートエラー",
|
"importErrors": "インポートエラー",
|
||||||
"importedXOfYApps": "{} / {} アプリをインポートしました",
|
"importedXOfYApps": "{} / {} アプリをインポートしました",
|
||||||
"followingURLsHadErrors": "以下のURLでエラーが発生しました:",
|
"followingURLsHadErrors": "以下のURLでエラーが発生しました:",
|
||||||
@ -133,7 +132,7 @@
|
|||||||
"bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔",
|
"bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔",
|
||||||
"neverManualOnly": "手動",
|
"neverManualOnly": "手動",
|
||||||
"appearance": "外観",
|
"appearance": "外観",
|
||||||
"showWebInAppView": "アプリビューにソースウェブページを表示する",
|
"showWebInAppView": "アプリページにソースのWebページを表示する",
|
||||||
"pinUpdates": "アップデートがあるアプリをトップに固定する",
|
"pinUpdates": "アップデートがあるアプリをトップに固定する",
|
||||||
"updates": "アップデート",
|
"updates": "アップデート",
|
||||||
"sourceSpecific": "Github アクセストークン",
|
"sourceSpecific": "Github アクセストークン",
|
||||||
@ -178,13 +177,12 @@
|
|||||||
"installedVersionX": "インストールされたバージョン: {}",
|
"installedVersionX": "インストールされたバージョン: {}",
|
||||||
"lastUpdateCheckX": "最終アップデート確認: {}",
|
"lastUpdateCheckX": "最終アップデート確認: {}",
|
||||||
"remove": "削除",
|
"remove": "削除",
|
||||||
"removeAppQuestion": "アプリを削除しますか?",
|
|
||||||
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "アプリのIDまたは名前",
|
"appIdOrName": "アプリのIDまたは名前",
|
||||||
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
||||||
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
|
||||||
"steam": "Steam",
|
"steam": "Steam",
|
||||||
"steamMobile": "Steam Mobile",
|
"steamMobile": "Steam Mobile",
|
||||||
"steamChat": "Steam Chat",
|
"steamChat": "Steam Chat",
|
||||||
@ -211,6 +209,21 @@
|
|||||||
"language": "言語",
|
"language": "言語",
|
||||||
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
||||||
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
||||||
|
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
|
||||||
|
"removeFromObtainium": "Obtainiumから削除する",
|
||||||
|
"uninstallFromDevice": "デバイスからアンインストールする",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
|
||||||
|
"releaseDateAsVersion": "リリース日をバージョンとして使用する",
|
||||||
|
"releaseDateAsVersionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリで、リリース日が利用可能な場合にのみ使用する必要があります。",
|
||||||
|
"changes": "変更点",
|
||||||
|
"releaseDate": "リリース日",
|
||||||
|
"importFromURLsInFile": "ファイル(OPMLなど)内のURLからインポート",
|
||||||
|
"versionDetection": "バージョン検出",
|
||||||
|
"standardVersionDetection": "標準のバージョン検出",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "アプリを削除しますか?",
|
||||||
|
"other": "アプリを削除しますか?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
||||||
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
||||||
|
@ -178,7 +178,6 @@
|
|||||||
"installedVersionX": "已安装: {}",
|
"installedVersionX": "已安装: {}",
|
||||||
"lastUpdateCheckX": "最后检查: {}",
|
"lastUpdateCheckX": "最后检查: {}",
|
||||||
"remove": "删除",
|
"remove": "删除",
|
||||||
"removeAppQuestion": "删除应用?",
|
|
||||||
"yesMarkUpdated": "'是的,标为已更新",
|
"yesMarkUpdated": "'是的,标为已更新",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "应用 ID 或名称",
|
"appIdOrName": "应用 ID 或名称",
|
||||||
@ -211,6 +210,20 @@
|
|||||||
"language": "语言",
|
"language": "语言",
|
||||||
"storagePermissionDenied": "存储权限已被拒绝",
|
"storagePermissionDenied": "存储权限已被拒绝",
|
||||||
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
|
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
|
||||||
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
|
"removeFromObtainium": "Remove from Obtainium",
|
||||||
|
"uninstallFromDevice": "Uninstall from Device",
|
||||||
|
"releaseDateAsVersion": "Use Release Date as Version",
|
||||||
|
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
|
||||||
|
"changes": "Changes",
|
||||||
|
"releaseDate": "Release Date",
|
||||||
|
"importFromURLsInFile": "Import from URLs in File (like OPML)",
|
||||||
|
"versionDetection": "Version Detection",
|
||||||
|
"standardVersionDetection": "Standard version detection",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "删除应用?",
|
||||||
|
"other": "删除应用?"
|
||||||
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
||||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
@ -7,6 +11,23 @@ class APKMirror extends AppSource {
|
|||||||
APKMirror() {
|
APKMirror() {
|
||||||
host = 'apkmirror.com';
|
host = 'apkmirror.com';
|
||||||
enforceTrackOnly = true;
|
enforceTrackOnly = true;
|
||||||
|
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
||||||
|
label: tr('filterReleaseTitlesByRegEx'),
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
return regExValidator(value);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,12 +49,38 @@ class APKMirror extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
|
bool fallbackToOlderReleases =
|
||||||
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
|
String? regexFilter =
|
||||||
|
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
||||||
|
?.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
|
: null;
|
||||||
Response res = await get(Uri.parse('$standardUrl/feed'));
|
Response res = await get(Uri.parse('$standardUrl/feed'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
String? titleString = parse(res.body)
|
var items = parse(res.body).querySelectorAll('item');
|
||||||
.querySelector('item')
|
dynamic targetRelease;
|
||||||
?.querySelector('title')
|
for (int i = 0; i < items.length; i++) {
|
||||||
?.innerHtml;
|
if (!fallbackToOlderReleases && i > 0) break;
|
||||||
|
String? nameToFilter = items[i].querySelector('title')?.innerHtml;
|
||||||
|
if (regexFilter != null &&
|
||||||
|
nameToFilter != null &&
|
||||||
|
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
targetRelease = items[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
String? titleString = targetRelease?.querySelector('title')?.innerHtml;
|
||||||
|
String? dateString = targetRelease
|
||||||
|
?.querySelector('pubDate')
|
||||||
|
?.innerHtml
|
||||||
|
.split(' ')
|
||||||
|
.sublist(0, 5)
|
||||||
|
.join(' ');
|
||||||
|
DateTime? releaseDate =
|
||||||
|
dateString != null ? HttpDate.parse('$dateString GMT') : null;
|
||||||
String? version = titleString
|
String? version = titleString
|
||||||
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
||||||
RegExp(' by ').firstMatch(titleString)?.start ?? 0)
|
RegExp(' by ').firstMatch(titleString)?.start ?? 0)
|
||||||
@ -44,7 +91,8 @@ class APKMirror extends AppSource {
|
|||||||
if (version == null || version.isEmpty) {
|
if (version == null || version.isEmpty) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, [], getAppNames(standardUrl));
|
return APKDetails(version, [], getAppNames(standardUrl),
|
||||||
|
releaseDate: releaseDate);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -26,15 +26,7 @@ class Codeberg extends AppSource {
|
|||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
if (value == null || value.isEmpty) {
|
return regExValidator(value);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
RegExp(value);
|
|
||||||
} catch (e) {
|
|
||||||
return tr('invalidRegEx');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
@ -62,9 +54,9 @@ class Codeberg extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
bool includePrereleases = additionalSettings['includePrereleases'];
|
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'];
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
String? regexFilter =
|
String? regexFilter =
|
||||||
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
||||||
?.isNotEmpty ==
|
?.isNotEmpty ==
|
||||||
@ -72,7 +64,7 @@ class Codeberg extends AppSource {
|
|||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
@ -99,8 +91,8 @@ class Codeberg extends AppSource {
|
|||||||
if (releases[i]['draft'] == true) {
|
if (releases[i]['draft'] == true) {
|
||||||
// Draft releases not supported
|
// Draft releases not supported
|
||||||
}
|
}
|
||||||
var nameToFilter = releases[i]['name'] as String;
|
var nameToFilter = releases[i]['name'] as String?;
|
||||||
if (nameToFilter.trim().isEmpty) {
|
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
|
||||||
// Some leave titles empty so tag is used
|
// Some leave titles empty so tag is used
|
||||||
nameToFilter = releases[i]['tag_name'] as String;
|
nameToFilter = releases[i]['tag_name'] as String;
|
||||||
}
|
}
|
||||||
@ -120,11 +112,17 @@ class Codeberg extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
String? version = targetRelease['tag_name'];
|
String? version = targetRelease['tag_name'];
|
||||||
|
DateTime? releaseDate = targetRelease['published_at'] != null
|
||||||
|
? DateTime.parse(targetRelease['published_at'])
|
||||||
|
: null;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
|
var changeLog = targetRelease['body'].toString();
|
||||||
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
||||||
getAppNames(standardUrl));
|
getAppNames(standardUrl),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
changeLog: changeLog.isEmpty ? null : changeLog);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -27,9 +27,6 @@ class FDroid extends AppSource {
|
|||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? tryInferringAppId(String standardUrl,
|
String? tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||||
|
@ -69,6 +69,8 @@ class FDroidRepo extends AppSource {
|
|||||||
foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName;
|
foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName;
|
||||||
var releases = foundApps[0].querySelectorAll('package');
|
var releases = foundApps[0].querySelectorAll('package');
|
||||||
String? latestVersion = releases[0].querySelector('version')?.innerHtml;
|
String? latestVersion = releases[0].querySelector('version')?.innerHtml;
|
||||||
|
String? added = releases[0].querySelector('added')?.innerHtml;
|
||||||
|
DateTime? releaseDate = added != null ? DateTime.parse(added) : null;
|
||||||
if (latestVersion == null) {
|
if (latestVersion == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -78,7 +80,8 @@ class FDroidRepo extends AppSource {
|
|||||||
element.querySelector('apkname') != null)
|
element.querySelector('apkname') != null)
|
||||||
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
|
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName));
|
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName),
|
||||||
|
releaseDate: releaseDate);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -65,15 +65,7 @@ class GitHub extends AppSource {
|
|||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
if (value == null || value.isEmpty) {
|
return regExValidator(value);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
RegExp(value);
|
|
||||||
} catch (e) {
|
|
||||||
return tr('invalidRegEx');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
@ -109,9 +101,9 @@ class GitHub extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
bool includePrereleases = additionalSettings['includePrereleases'];
|
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'];
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
String? regexFilter =
|
String? regexFilter =
|
||||||
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
||||||
?.isNotEmpty ==
|
?.isNotEmpty ==
|
||||||
@ -119,7 +111,7 @@ class GitHub extends AppSource {
|
|||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
@ -141,8 +133,8 @@ class GitHub extends AppSource {
|
|||||||
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var nameToFilter = releases[i]['name'] as String;
|
var nameToFilter = releases[i]['name'] as String?;
|
||||||
if (nameToFilter.trim().isEmpty) {
|
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
|
||||||
// Some leave titles empty so tag is used
|
// Some leave titles empty so tag is used
|
||||||
nameToFilter = releases[i]['tag_name'] as String;
|
nameToFilter = releases[i]['tag_name'] as String;
|
||||||
}
|
}
|
||||||
@ -162,11 +154,17 @@ class GitHub extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
String? version = targetRelease['tag_name'];
|
String? version = targetRelease['tag_name'];
|
||||||
|
DateTime? releaseDate = targetRelease['published_at'] != null
|
||||||
|
? DateTime.parse(targetRelease['published_at'])
|
||||||
|
: null;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
|
var changeLog = targetRelease['body'].toString();
|
||||||
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
||||||
getAppNames(standardUrl));
|
getAppNames(standardUrl),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
changeLog: changeLog.isEmpty ? null : changeLog);
|
||||||
} else {
|
} else {
|
||||||
rateLimitErrorCheck(res);
|
rateLimitErrorCheck(res);
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
|
@ -54,10 +54,14 @@ class GitLab extends AppSource {
|
|||||||
var entryId = entry?.querySelector('id')?.innerHtml;
|
var entryId = entry?.querySelector('id')?.innerHtml;
|
||||||
var version =
|
var version =
|
||||||
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
||||||
|
var releaseDateString = entry?.querySelector('updated')?.innerHtml;
|
||||||
|
DateTime? releaseDate =
|
||||||
|
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl));
|
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl),
|
||||||
|
releaseDate: releaseDate);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,6 @@ class HTML extends AppSource {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
@ -27,6 +24,10 @@ class HTML extends AppSource {
|
|||||||
.where((element) => element.toLowerCase().endsWith('.apk'))
|
.where((element) => element.toLowerCase().endsWith('.apk'))
|
||||||
.toList();
|
.toList();
|
||||||
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
|
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
|
||||||
|
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||||
|
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||||
|
links = links.where((element) => reg.hasMatch(element)).toList();
|
||||||
|
}
|
||||||
if (links.isEmpty) {
|
if (links.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
@ -37,7 +38,9 @@ class HTML extends AppSource {
|
|||||||
.map((e) => e.toLowerCase().startsWith('http://') ||
|
.map((e) => e.toLowerCase().startsWith('http://') ||
|
||||||
e.toLowerCase().startsWith('https://')
|
e.toLowerCase().startsWith('https://')
|
||||||
? e
|
? e
|
||||||
: '${uri.origin}/$e')
|
: e.startsWith('/')
|
||||||
|
? '${uri.origin}/$e'
|
||||||
|
: '${uri.origin}/${uri.path}/$e')
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(version, apkUrls, AppNames(uri.host, tr('app')));
|
return APKDetails(version, apkUrls, AppNames(uri.host, tr('app')));
|
||||||
} else {
|
} else {
|
||||||
|
@ -18,9 +18,6 @@ class IzzyOnDroid extends AppSource {
|
|||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? tryInferringAppId(String standardUrl,
|
String? tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||||
|
111
lib/app_sources/neutroncode.dart
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class NeutronCode extends AppSource {
|
||||||
|
NeutronCode() {
|
||||||
|
host = 'neutroncode.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
|
||||||
|
|
||||||
|
String monthNameToNumberString(String s) {
|
||||||
|
switch (s.toLowerCase()) {
|
||||||
|
case 'january':
|
||||||
|
return '01';
|
||||||
|
case 'february':
|
||||||
|
return '02';
|
||||||
|
case 'march':
|
||||||
|
return '03';
|
||||||
|
case 'april':
|
||||||
|
return '04';
|
||||||
|
case 'may':
|
||||||
|
return '05';
|
||||||
|
case 'june':
|
||||||
|
return '06';
|
||||||
|
case 'july':
|
||||||
|
return '07';
|
||||||
|
case 'august':
|
||||||
|
return '08';
|
||||||
|
case 'september':
|
||||||
|
return '09';
|
||||||
|
case 'october':
|
||||||
|
return '10';
|
||||||
|
case 'november':
|
||||||
|
return '11';
|
||||||
|
case 'december':
|
||||||
|
return '12';
|
||||||
|
default:
|
||||||
|
throw ArgumentError('Invalid month name: $s');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customDateParse(String dateString) {
|
||||||
|
List<String> parts = dateString.split(' ');
|
||||||
|
if (parts.length != 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String result = '';
|
||||||
|
for (var s in parts.reversed) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
int.parse(s);
|
||||||
|
result += '$s-';
|
||||||
|
} catch (e) {
|
||||||
|
result += '${monthNameToNumberString(s)}-';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.substring(0, result.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
Response res = await get(Uri.parse(standardUrl));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var http = parse(res.body);
|
||||||
|
var name = http.querySelector('.pd-title')?.innerHtml;
|
||||||
|
var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml;
|
||||||
|
if (filename == null) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
var version =
|
||||||
|
http.querySelector('.pd-version-txt')?.nextElementSibling?.innerHtml;
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? apkUrl = 'https://$host/download/$filename';
|
||||||
|
var dateStringOriginal =
|
||||||
|
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
|
||||||
|
var dateString = dateStringOriginal != null
|
||||||
|
? (customDateParse(dateStringOriginal))
|
||||||
|
: null;
|
||||||
|
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
|
||||||
|
return APKDetails(version, [apkUrl],
|
||||||
|
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
|
||||||
|
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
|
||||||
|
changeLog: changeLogElements.isNotEmpty
|
||||||
|
? changeLogElements.last.innerHtml
|
||||||
|
: null);
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,6 @@ class Signal extends AppSource {
|
|||||||
return 'https://$host';
|
return 'https://$host';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
|
@ -18,9 +18,6 @@ class SourceForge extends AppSource {
|
|||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
|
@ -10,7 +10,10 @@ class SteamMobile extends AppSource {
|
|||||||
host = 'store.steampowered.com';
|
host = 'store.steampowered.com';
|
||||||
name = tr('steam');
|
name = tr('steam');
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[GeneratedFormDropdown('app', apks.entries.toList(), label: tr('app'))]
|
[
|
||||||
|
GeneratedFormDropdown('app', apks.entries.toList(),
|
||||||
|
label: tr('app'), defaultValue: apks.entries.toList()[0].key)
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,9 +24,6 @@ class SteamMobile extends AppSource {
|
|||||||
return 'https://$host';
|
return 'https://$host';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
@ -35,7 +35,8 @@ class SteamMobile extends AppSource {
|
|||||||
if (apkNamePrefix == null) {
|
if (apkNamePrefix == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
String apkInURLRegexPattern = '/$apkNamePrefix-[^/]+\\.apk\$';
|
String apkInURLRegexPattern =
|
||||||
|
'/$apkNamePrefix-([0-9]+\\.)*[0-9]+\\.apk\$';
|
||||||
var links = parse(res.body)
|
var links = parse(res.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((e) => e.attributes['href'] ?? '')
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
|
40
lib/app_sources/telegramapp.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class TelegramApp extends AppSource {
|
||||||
|
TelegramApp() {
|
||||||
|
host = 'telegram.org';
|
||||||
|
name = 'Telegram ${tr('app')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
return 'https://$host';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var http = parse(res.body);
|
||||||
|
var messages =
|
||||||
|
http.querySelectorAll('.tgme_widget_message_text.js-message_text');
|
||||||
|
var version = messages.isNotEmpty
|
||||||
|
? messages.last.innerHtml.split('\n').first.trim().split(' ').first
|
||||||
|
: null;
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? apkUrl = 'https://telegram.org/dl/android/apk';
|
||||||
|
return APKDetails(version, [apkUrl], AppNames('Telegram', 'Telegram'));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
lib/app_sources/vlc.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class VLC extends AppSource {
|
||||||
|
VLC() {
|
||||||
|
host = 'videolan.org';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
return 'https://$host';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
Response res = await get(
|
||||||
|
Uri.parse('https://www.videolan.org/vlc/download-android.html'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var dwUrlBase = 'get.videolan.org/vlc-android';
|
||||||
|
var dwLinks = parse(res.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.where((element) =>
|
||||||
|
element.attributes['href']?.contains(dwUrlBase) ?? false)
|
||||||
|
.toList();
|
||||||
|
String? version = dwLinks.isNotEmpty
|
||||||
|
? dwLinks.first.attributes['href']
|
||||||
|
?.split('/')
|
||||||
|
.where((s) => s.isNotEmpty)
|
||||||
|
.last
|
||||||
|
: null;
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? targetUrl = 'https://$dwUrlBase/$version/';
|
||||||
|
Response res2 = await get(Uri.parse(targetUrl));
|
||||||
|
String mirrorDwBase =
|
||||||
|
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
|
||||||
|
List<String> apkUrls = [];
|
||||||
|
if (res2.statusCode == 200) {
|
||||||
|
apkUrls = parse(res2.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.map((e) => e.attributes['href'])
|
||||||
|
.where((h) =>
|
||||||
|
h != null && h.isNotEmpty && h.toLowerCase().endsWith('.apk'))
|
||||||
|
.map((e) => mirrorDwBase + e!)
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return APKDetails(version, apkUrls, AppNames('VideoLAN', 'VLC'));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
lib/app_sources/whatsapp.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class WhatsApp extends AppSource {
|
||||||
|
WhatsApp() {
|
||||||
|
host = 'whatsapp.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
return 'https://$host';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
||||||
|
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var targetLinks = parse(res.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.map((e) => e.attributes['href'])
|
||||||
|
.where((e) => e != null)
|
||||||
|
.where((e) =>
|
||||||
|
e!.contains('scontent.whatsapp.net') &&
|
||||||
|
e.contains('WhatsApp.apk'))
|
||||||
|
.toList();
|
||||||
|
if (targetLinks.isEmpty) {
|
||||||
|
throw NoAPKError();
|
||||||
|
}
|
||||||
|
return targetLinks[0]!;
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var targetElements = parse(res.body)
|
||||||
|
.querySelectorAll('p')
|
||||||
|
.where((element) => element.innerHtml.contains('Version '))
|
||||||
|
.toList();
|
||||||
|
if (targetElements.isEmpty) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
var vLines = targetElements[0]
|
||||||
|
.innerHtml
|
||||||
|
.split('\n')
|
||||||
|
.where((element) => element.contains('Version '))
|
||||||
|
.toList();
|
||||||
|
if (vLines.isEmpty) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
var versionMatch = RegExp('[0-9]+(\\.[0-9]+)+').firstMatch(vLines[0]);
|
||||||
|
if (versionMatch == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String version =
|
||||||
|
vLines[0].substring(versionMatch.start, versionMatch.end);
|
||||||
|
return APKDetails(
|
||||||
|
version,
|
||||||
|
[
|
||||||
|
'https://www.whatsapp.com/android?v=$version&=thisIsaPlaceholder&a=realURLPrefetchedAtDownloadTime'
|
||||||
|
],
|
||||||
|
AppNames('Meta', 'WhatsApp'));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -150,6 +150,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
Map<String, dynamic> values = {};
|
Map<String, dynamic> values = {};
|
||||||
late List<List<Widget>> formInputs;
|
late List<List<Widget>> formInputs;
|
||||||
List<List<Widget>> rows = [];
|
List<List<Widget>> rows = [];
|
||||||
|
String? initKey;
|
||||||
|
|
||||||
// If any value changes, call this to update the parent with value and validity
|
// If any value changes, call this to update the parent with value and validity
|
||||||
void someValueChanged({bool isBuilding = false}) {
|
void someValueChanged({bool isBuilding = false}) {
|
||||||
@ -169,13 +170,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
widget.onValueChanges(returnValues, valid, isBuilding);
|
widget.onValueChanges(returnValues, valid, isBuilding);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
initForm() {
|
||||||
void initState() {
|
initKey = widget.key.toString();
|
||||||
super.initState();
|
|
||||||
|
|
||||||
// Initialize form values as all empty
|
// Initialize form values as all empty
|
||||||
values.clear();
|
values.clear();
|
||||||
int j = 0;
|
|
||||||
for (var row in widget.items) {
|
for (var row in widget.items) {
|
||||||
for (var e in row) {
|
for (var e in row) {
|
||||||
values[e.key] = e.defaultValue;
|
values[e.key] = e.defaultValue;
|
||||||
@ -245,8 +243,17 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
someValueChanged(isBuilding: true);
|
someValueChanged(isBuilding: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
initForm();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.key.toString() != initKey) {
|
||||||
|
initForm();
|
||||||
|
}
|
||||||
for (var r = 0; r < formInputs.length; r++) {
|
for (var r = 0; r < formInputs.length; r++) {
|
||||||
for (var e = 0; e < formInputs[r].length; e++) {
|
for (var e = 0; e < formInputs[r].length; e++) {
|
||||||
if (widget.items[r][e] is GeneratedFormSwitch) {
|
if (widget.items[r][e] is GeneratedFormSwitch) {
|
||||||
@ -453,10 +460,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
if (rowInputs.key > 0) {
|
if (rowInputs.key > 0) {
|
||||||
rows.add([
|
rows.add([
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: widget.items[rowInputs.key][0] is GeneratedFormSwitch &&
|
height: widget.items[rowInputs.key - 1][0] is GeneratedFormSwitch
|
||||||
widget.items[rowInputs.key - 1][0] is! GeneratedFormSwitch
|
? 8
|
||||||
? 25
|
: 25,
|
||||||
: 8,
|
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -470,6 +476,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
rowItems.add(Expanded(
|
rowItems.add(Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
rowInput.value,
|
rowInput.value,
|
||||||
...widget.items[rowInputs.key][rowInput.key].belowWidgets
|
...widget.items[rowInputs.key][rowInput.key].belowWidgets
|
||||||
|
@ -29,7 +29,7 @@ class NoReleasesError extends ObtainiumError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NoAPKError extends ObtainiumError {
|
class NoAPKError extends ObtainiumError {
|
||||||
NoAPKError() : super(tr('noReleaseFound'));
|
NoAPKError() : super(tr('noAPKFound'));
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoVersionError extends ObtainiumError {
|
class NoVersionError extends ObtainiumError {
|
||||||
|
@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
|
|||||||
// ignore: implementation_imports
|
// ignore: implementation_imports
|
||||||
import 'package:easy_localization/src/localization.dart';
|
import 'package:easy_localization/src/localization.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.10.1';
|
const String currentVersion = '0.11.13';
|
||||||
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
|
||||||
|
|
||||||
@ -33,7 +33,9 @@ const supportedLocales = [
|
|||||||
Locale('it'),
|
Locale('it'),
|
||||||
Locale('ja'),
|
Locale('ja'),
|
||||||
Locale('hu'),
|
Locale('hu'),
|
||||||
Locale('de')
|
Locale('de'),
|
||||||
|
Locale('fa'),
|
||||||
|
Locale('fr')
|
||||||
];
|
];
|
||||||
const fallbackLocale = Locale('en');
|
const fallbackLocale = Locale('en');
|
||||||
const localeDir = 'assets/translations';
|
const localeDir = 'assets/translations';
|
||||||
@ -210,6 +212,14 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
false)
|
false)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
if (!supportedLocales
|
||||||
|
.map((e) => e.languageCode)
|
||||||
|
.contains(context.locale.languageCode) ||
|
||||||
|
settingsProvider.forcedLocale == null &&
|
||||||
|
context.deviceLocale.languageCode !=
|
||||||
|
context.locale.languageCode) {
|
||||||
|
settingsProvider.resetLocaleSafe(context);
|
||||||
|
}
|
||||||
// Register the background update task according to the user's setting
|
// Register the background update task according to the user's setting
|
||||||
if (existingUpdateInterval != settingsProvider.updateInterval) {
|
if (existingUpdateInterval != settingsProvider.updateInterval) {
|
||||||
if (existingUpdateInterval != -1) {
|
if (existingUpdateInterval != -1) {
|
||||||
|
@ -32,6 +32,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
Map<String, dynamic> additionalSettings = {};
|
Map<String, dynamic> additionalSettings = {};
|
||||||
bool additionalSettingsValid = true;
|
bool additionalSettingsValid = true;
|
||||||
List<String> pickedCategories = [];
|
List<String> pickedCategories = [];
|
||||||
|
int searchnum = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -40,10 +41,14 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
|
|
||||||
bool doingSomething = gettingAppInfo || searching;
|
bool doingSomething = gettingAppInfo || searching;
|
||||||
|
|
||||||
changeUserInput(String input, bool valid, bool isBuilding) {
|
changeUserInput(String input, bool valid, bool isBuilding,
|
||||||
|
{bool isSearch = false}) {
|
||||||
userInput = input;
|
userInput = input;
|
||||||
if (!isBuilding) {
|
if (!isBuilding) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (isSearch) {
|
||||||
|
searchnum++;
|
||||||
|
}
|
||||||
var source = valid ? sourceProvider.getSource(userInput) : null;
|
var source = valid ? sourceProvider.getSource(userInput) : null;
|
||||||
if (pickedSource.runtimeType != source.runtimeType) {
|
if (pickedSource.runtimeType != source.runtimeType) {
|
||||||
pickedSource = source;
|
pickedSource = source;
|
||||||
@ -66,10 +71,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
var settingsProvider = context.read<SettingsProvider>();
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
() async {
|
() async {
|
||||||
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
||||||
var userPickedNoVersionDetection =
|
|
||||||
additionalSettings['noVersionDetection'] == true;
|
|
||||||
var cont = true;
|
var cont = true;
|
||||||
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -87,7 +91,22 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
null) {
|
null) {
|
||||||
cont = false;
|
cont = false;
|
||||||
}
|
}
|
||||||
if (userPickedNoVersionDetection &&
|
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: tr('releaseDateAsVersion'),
|
||||||
|
items: const [],
|
||||||
|
message: tr('releaseDateAsVersionExplanation'),
|
||||||
|
);
|
||||||
|
}) ==
|
||||||
|
null) {
|
||||||
|
cont = false;
|
||||||
|
}
|
||||||
|
if (additionalSettings['versionDetection'] == 'noVersionDetection' &&
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -105,13 +124,12 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||||
App app = await sourceProvider.getApp(
|
App app = await sourceProvider.getApp(
|
||||||
pickedSource!, userInput, additionalSettings,
|
pickedSource!, userInput, additionalSettings,
|
||||||
trackOnlyOverride: trackOnly,
|
trackOnlyOverride: trackOnly);
|
||||||
noVersionDetectionOverride: userPickedNoVersionDetection);
|
|
||||||
if (!trackOnly) {
|
if (!trackOnly) {
|
||||||
await settingsProvider.getInstallPermission();
|
await settingsProvider.getInstallPermission();
|
||||||
}
|
}
|
||||||
// Only download the APK here if you need to for the package ID
|
// Only download the APK here if you need to for the package ID
|
||||||
if (sourceProvider.isTempId(app.id) &&
|
if (sourceProvider.isTempId(app) &&
|
||||||
app.additionalSettings['trackOnly'] != true) {
|
app.additionalSettings['trackOnly'] != true) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var apkUrl = await appsProvider.confirmApkUrl(app, context);
|
var apkUrl = await appsProvider.confirmApkUrl(app, context);
|
||||||
@ -167,10 +185,12 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GeneratedForm(
|
child: GeneratedForm(
|
||||||
|
key: Key(searchnum.toString()),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('appSourceURL',
|
GeneratedFormTextField('appSourceURL',
|
||||||
label: tr('appSourceURL'),
|
label: tr('appSourceURL'),
|
||||||
|
defaultValue: userInput,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
try {
|
try {
|
||||||
@ -294,8 +314,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
if (selectedUrls != null &&
|
if (selectedUrls != null &&
|
||||||
selectedUrls.isNotEmpty) {
|
selectedUrls.isNotEmpty) {
|
||||||
changeUserInput(
|
changeUserInput(
|
||||||
selectedUrls[0], true, false);
|
selectedUrls[0], true, false,
|
||||||
addApp(resetUserInputAfter: true);
|
isSearch: true);
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
@ -325,6 +345,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
|
key: Key(pickedSource.runtimeType.toString()),
|
||||||
items: pickedSource!
|
items: pickedSource!
|
||||||
.combinedAppSpecificSettingFormItems,
|
.combinedAppSpecificSettingFormItems,
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
@ -111,7 +111,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 150),
|
const SizedBox(height: 125),
|
||||||
app?.installedInfo != null
|
app?.installedInfo != null
|
||||||
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
Image.memory(
|
Image.memory(
|
||||||
@ -134,6 +134,21 @@ class _AppPageState extends State<AppPage> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
app?.app.id ?? '',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
app?.app.releaseDate == null
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Text(
|
||||||
|
app!.app.releaseDate.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
@ -190,8 +205,10 @@ class _AppPageState extends State<AppPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
if (app?.app.installedVersion != null &&
|
if (app?.app.additionalSettings['versionDetection'] !=
|
||||||
|
'standardVersionDetection' &&
|
||||||
!trackOnly &&
|
!trackOnly &&
|
||||||
|
app?.app.installedVersion != null &&
|
||||||
app?.app.installedVersion != app?.app.latestVersion)
|
app?.app.installedVersion != app?.app.latestVersion)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
@ -203,13 +220,6 @@ class _AppPageState extends State<AppPage> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(tr(
|
title: Text(tr(
|
||||||
'alreadyUpToDateQuestion')),
|
'alreadyUpToDateQuestion')),
|
||||||
content: Text(
|
|
||||||
tr('onlyWorksWithNonEVDApps'),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight:
|
|
||||||
FontWeight.bold,
|
|
||||||
fontStyle:
|
|
||||||
FontStyle.italic)),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -268,28 +278,67 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('additionalOptions'),
|
title: tr('additionalOptions'),
|
||||||
items: items);
|
items: items,
|
||||||
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (app != null && values != null) {
|
if (app != null && values != null) {
|
||||||
var changedApp = app.app;
|
Map<String, dynamic>
|
||||||
changedApp.additionalSettings =
|
originalSettings =
|
||||||
values;
|
app.app.additionalSettings;
|
||||||
|
app.app.additionalSettings = values;
|
||||||
if (source.enforceTrackOnly) {
|
if (source.enforceTrackOnly) {
|
||||||
changedApp.additionalSettings[
|
app.app.additionalSettings[
|
||||||
'trackOnly'] = true;
|
'trackOnly'] = true;
|
||||||
showError(
|
showError(
|
||||||
tr('appsFromSourceAreTrackOnly'),
|
tr('appsFromSourceAreTrackOnly'),
|
||||||
context);
|
context);
|
||||||
}
|
}
|
||||||
appsProvider.saveApps(
|
if (app.app.additionalSettings[
|
||||||
[changedApp]).then((value) {
|
'versionDetection'] ==
|
||||||
getUpdate(changedApp.id);
|
'releaseDateAsVersion') {
|
||||||
|
if (originalSettings[
|
||||||
|
'versionDetection'] !=
|
||||||
|
'releaseDateAsVersion') {
|
||||||
|
if (app.app.releaseDate != null) {
|
||||||
|
bool isUpdated =
|
||||||
|
app.app.installedVersion ==
|
||||||
|
app.app.latestVersion;
|
||||||
|
app.app.latestVersion = app
|
||||||
|
.app
|
||||||
|
.releaseDate!
|
||||||
|
.microsecondsSinceEpoch
|
||||||
|
.toString();
|
||||||
|
if (isUpdated) {
|
||||||
|
app.app.installedVersion =
|
||||||
|
app.app.latestVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (originalSettings[
|
||||||
|
'versionDetection'] ==
|
||||||
|
'releaseDateAsVersion') {
|
||||||
|
app.app.installedVersion = app
|
||||||
|
.installedInfo
|
||||||
|
?.versionName ??
|
||||||
|
app.app.installedVersion;
|
||||||
|
}
|
||||||
|
appsProvider.saveApps([app.app]).then(
|
||||||
|
(value) {
|
||||||
|
getUpdate(app.app.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip: tr('additionalOptions'),
|
tooltip: tr('additionalOptions'),
|
||||||
icon: const Icon(Icons.settings)),
|
icon: const Icon(Icons.edit)),
|
||||||
|
if (app != null && app.installedInfo != null)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
appsProvider.openAppSettings(app.app.id);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
tooltip: tr('settings'),
|
||||||
|
),
|
||||||
if (app != null && settingsProvider.showAppWebpage)
|
if (app != null && settingsProvider.showAppWebpage)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -317,7 +366,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
tooltip: tr('more')),
|
tooltip: tr('more')),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: TextButton(
|
||||||
onPressed: (app?.app.installedVersion == null ||
|
onPressed: (app?.app.installedVersion == null ||
|
||||||
app?.app.installedVersion !=
|
app?.app.installedVersion !=
|
||||||
app?.app.latestVersion) &&
|
app?.app.latestVersion) &&
|
||||||
@ -342,6 +391,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
if (res.isNotEmpty && mounted) {
|
if (res.isNotEmpty && mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
@ -356,43 +407,16 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? tr('update')
|
? tr('update')
|
||||||
: tr('markUpdated')))),
|
: tr('markUpdated')))),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
ElevatedButton(
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog(
|
appsProvider.removeAppsWithModal(
|
||||||
context: context,
|
context, [app!.app]).then((value) {
|
||||||
builder: (BuildContext ctx) {
|
if (value == true) {
|
||||||
return AlertDialog(
|
|
||||||
title: Text(tr('removeAppQuestion')),
|
|
||||||
content: Text(tr(
|
|
||||||
'xWillBeRemovedButRemainInstalled',
|
|
||||||
args: [
|
|
||||||
app?.installedInfo?.name ??
|
|
||||||
app?.app.name ??
|
|
||||||
tr('app')
|
|
||||||
])),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
HapticFeedback
|
|
||||||
.selectionClick();
|
|
||||||
appsProvider.removeApps(
|
|
||||||
[app!.app.id]).then((_) {
|
|
||||||
int count = 0;
|
|
||||||
Navigator.of(context)
|
|
||||||
.popUntil((_) =>
|
|
||||||
count++ >= 2);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(tr('remove'))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
}
|
||||||
child: Text(tr('cancel')))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
@ -401,7 +425,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
surfaceTintColor:
|
surfaceTintColor:
|
||||||
Theme.of(context).colorScheme.error),
|
Theme.of(context).colorScheme.error),
|
||||||
child: Text(tr('remove')),
|
child: Text(tr('remove')),
|
||||||
),
|
)),
|
||||||
])),
|
])),
|
||||||
if (app?.downloadProgress != null)
|
if (app?.downloadProgress != null)
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
@ -14,6 +15,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class AppsPage extends StatefulWidget {
|
class AppsPage extends StatefulWidget {
|
||||||
const AppsPage({super.key});
|
const AppsPage({super.key});
|
||||||
@ -54,12 +56,12 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var appsProvider = context.watch<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
var settingsProvider = context.watch<SettingsProvider>();
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
var sortedApps = appsProvider.apps.values.toList();
|
var listedApps = appsProvider.apps.values.toList();
|
||||||
var currentFilterIsUpdatesOnly =
|
var currentFilterIsUpdatesOnly =
|
||||||
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
||||||
|
|
||||||
selectedApps = selectedApps
|
selectedApps = selectedApps
|
||||||
.where((element) => sortedApps.map((e) => e.app).contains(element))
|
.where((element) => listedApps.map((e) => e.app).contains(element))
|
||||||
.toSet();
|
.toSet();
|
||||||
|
|
||||||
toggleAppSelected(App app) {
|
toggleAppSelected(App app) {
|
||||||
@ -72,7 +74,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sortedApps = sortedApps.where((app) {
|
listedApps = listedApps.where((app) {
|
||||||
if (app.app.installedVersion == app.app.latestVersion &&
|
if (app.app.installedVersion == app.app.latestVersion &&
|
||||||
!(filter.includeUptodate)) {
|
!(filter.includeUptodate)) {
|
||||||
return false;
|
return false;
|
||||||
@ -111,7 +113,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
sortedApps.sort((a, b) {
|
listedApps.sort((a, b) {
|
||||||
var nameA = a.installedInfo?.name ?? a.app.name;
|
var nameA = a.installedInfo?.name ?? a.app.name;
|
||||||
var nameB = b.installedInfo?.name ?? b.app.name;
|
var nameB = b.installedInfo?.name ?? b.app.name;
|
||||||
int result = 0;
|
int result = 0;
|
||||||
@ -119,25 +121,30 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
result = (a.app.author + nameA).compareTo(b.app.author + nameB);
|
result = (a.app.author + nameA).compareTo(b.app.author + nameB);
|
||||||
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
|
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
|
||||||
result = (nameA + a.app.author).compareTo(nameB + b.app.author);
|
result = (nameA + a.app.author).compareTo(nameB + b.app.author);
|
||||||
|
} else if (settingsProvider.sortColumn ==
|
||||||
|
SortColumnSettings.releaseDate) {
|
||||||
|
result = (a.app.releaseDate)?.compareTo(
|
||||||
|
b.app.releaseDate ?? DateTime.fromMicrosecondsSinceEpoch(0)) ??
|
||||||
|
0;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (settingsProvider.sortOrder == SortOrderSettings.descending) {
|
if (settingsProvider.sortOrder == SortOrderSettings.descending) {
|
||||||
sortedApps = sortedApps.reversed.toList();
|
listedApps = listedApps.reversed.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
|
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
|
||||||
|
|
||||||
var existingUpdateIdsAllOrSelected = existingUpdates
|
var existingUpdateIdsAllOrSelected = existingUpdates
|
||||||
.where((element) => selectedApps.isEmpty
|
.where((element) => selectedApps.isEmpty
|
||||||
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
? listedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
: selectedApps.map((e) => e.id).contains(element))
|
: selectedApps.map((e) => e.id).contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
var newInstallIdsAllOrSelected = appsProvider
|
var newInstallIdsAllOrSelected = appsProvider
|
||||||
.findExistingUpdates(nonInstalledOnly: true)
|
.findExistingUpdates(nonInstalledOnly: true)
|
||||||
.where((element) => selectedApps.isEmpty
|
.where((element) => selectedApps.isEmpty
|
||||||
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
? listedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
: selectedApps.map((e) => e.id).contains(element))
|
: selectedApps.map((e) => e.id).contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -159,26 +166,26 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
|
|
||||||
if (settingsProvider.pinUpdates) {
|
if (settingsProvider.pinUpdates) {
|
||||||
var temp = [];
|
var temp = [];
|
||||||
sortedApps = sortedApps.where((sa) {
|
listedApps = listedApps.where((sa) {
|
||||||
if (existingUpdates.contains(sa.app.id)) {
|
if (existingUpdates.contains(sa.app.id)) {
|
||||||
temp.add(sa);
|
temp.add(sa);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
sortedApps = [...temp, ...sortedApps];
|
listedApps = [...temp, ...listedApps];
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempPinned = [];
|
var tempPinned = [];
|
||||||
var tempNotPinned = [];
|
var tempNotPinned = [];
|
||||||
for (var a in sortedApps) {
|
for (var a in listedApps) {
|
||||||
if (a.app.pinned) {
|
if (a.app.pinned) {
|
||||||
tempPinned.add(a);
|
tempPinned.add(a);
|
||||||
} else {
|
} else {
|
||||||
tempNotPinned.add(a);
|
tempNotPinned.add(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sortedApps = [...tempPinned, ...tempNotPinned];
|
listedApps = [...tempPinned, ...tempNotPinned];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@ -198,7 +205,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
},
|
},
|
||||||
child: CustomScrollView(slivers: <Widget>[
|
child: CustomScrollView(slivers: <Widget>[
|
||||||
CustomAppBar(title: tr('appsString')),
|
CustomAppBar(title: tr('appsString')),
|
||||||
if (appsProvider.loadingApps || sortedApps.isEmpty)
|
if (appsProvider.loadingApps || listedApps.isEmpty)
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: appsProvider.loadingApps
|
child: appsProvider.loadingApps
|
||||||
@ -224,147 +231,284 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
String? changesUrl = SourceProvider()
|
AppSource appSource =
|
||||||
.getSource(sortedApps[index].app.url)
|
SourceProvider().getSource(listedApps[index].app.url);
|
||||||
.changeLogPageFromStandardUrl(sortedApps[index].app.url);
|
String? changesUrl = appSource
|
||||||
|
.changeLogPageFromStandardUrl(listedApps[index].app.url);
|
||||||
|
String? changeLog = listedApps[index].app.changeLog;
|
||||||
|
var showChanges = (changeLog == null && changesUrl == null)
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (changeLog != null) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: tr('changes'),
|
||||||
|
items: const [],
|
||||||
|
additionalWidgets: [
|
||||||
|
changesUrl != null
|
||||||
|
? GestureDetector(
|
||||||
|
child: Text(
|
||||||
|
changesUrl,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration:
|
||||||
|
TextDecoration.underline,
|
||||||
|
fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(changesUrl,
|
||||||
|
mode: LaunchMode
|
||||||
|
.externalApplication);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
changesUrl != null
|
||||||
|
? const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
appSource.changeLogIfAnyIsMarkDown
|
||||||
|
? SizedBox(
|
||||||
|
width:
|
||||||
|
MediaQuery.of(context).size.width,
|
||||||
|
height: MediaQuery.of(context)
|
||||||
|
.size
|
||||||
|
.height -
|
||||||
|
350,
|
||||||
|
child: Markdown(
|
||||||
|
data: changeLog,
|
||||||
|
onTapLink: (text, href, title) {
|
||||||
|
if (href != null) {
|
||||||
|
launchUrlString(
|
||||||
|
href.startsWith(
|
||||||
|
'http://') ||
|
||||||
|
href.startsWith(
|
||||||
|
'https://')
|
||||||
|
? href
|
||||||
|
: '${Uri.parse(listedApps[index].app.url).origin}/$href',
|
||||||
|
mode: LaunchMode
|
||||||
|
.externalApplication);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extensionSet: md.ExtensionSet(
|
||||||
|
md.ExtensionSet.gitHubFlavored
|
||||||
|
.blockSyntaxes,
|
||||||
|
[
|
||||||
|
md.EmojiSyntax(),
|
||||||
|
...md
|
||||||
|
.ExtensionSet
|
||||||
|
.gitHubFlavored
|
||||||
|
.inlineSyntaxes
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
: Text(changeLog),
|
||||||
|
],
|
||||||
|
singleNullReturnButton: tr('ok'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
launchUrlString(changesUrl!,
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
};
|
||||||
var transparent = const Color.fromARGB(0, 0, 0, 0).value;
|
var transparent = const Color.fromARGB(0, 0, 0, 0).value;
|
||||||
|
var hasUpdate = listedApps[index].app.installedVersion != null &&
|
||||||
|
listedApps[index].app.installedVersion !=
|
||||||
|
listedApps[index].app.latestVersion;
|
||||||
|
var updateButton = IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
tooltip:
|
||||||
|
listedApps[index].app.additionalSettings['trackOnly'] ==
|
||||||
|
true
|
||||||
|
? tr('markUpdated')
|
||||||
|
: tr('update'),
|
||||||
|
onPressed: appsProvider.areDownloadsRunning()
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
appsProvider.downloadAndInstallLatestApps([
|
||||||
|
listedApps[index].app.id
|
||||||
|
], globalNavigatorKey.currentContext).catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
listedApps[index].app.additionalSettings['trackOnly'] ==
|
||||||
|
true
|
||||||
|
? Icons.check_circle_outline
|
||||||
|
: Icons.install_mobile));
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
vertical: BorderSide(
|
vertical: BorderSide(
|
||||||
width: 4,
|
width: 4,
|
||||||
color: Color(
|
color: Color(
|
||||||
sortedApps[index].app.categories.isNotEmpty
|
listedApps[index].app.categories.isNotEmpty
|
||||||
? settingsProvider.categories[
|
? settingsProvider.categories[
|
||||||
sortedApps[index]
|
listedApps[index]
|
||||||
.app
|
.app
|
||||||
.categories
|
.categories
|
||||||
.first] ??
|
.first] ??
|
||||||
transparent
|
transparent
|
||||||
: transparent)))),
|
: transparent)))),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
tileColor: sortedApps[index].app.pinned
|
tileColor: listedApps[index].app.pinned
|
||||||
? Colors.grey.withOpacity(0.1)
|
? Colors.grey.withOpacity(0.1)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
selectedTileColor: Theme.of(context)
|
selectedTileColor: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.primary
|
.primary
|
||||||
.withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1),
|
.withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1),
|
||||||
selected: selectedApps.contains(sortedApps[index].app),
|
selected: selectedApps.contains(listedApps[index].app),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
toggleAppSelected(sortedApps[index].app);
|
toggleAppSelected(listedApps[index].app);
|
||||||
},
|
},
|
||||||
leading: sortedApps[index].installedInfo != null
|
leading: listedApps[index].installedInfo != null
|
||||||
? Image.memory(
|
? Image.memory(
|
||||||
sortedApps[index].installedInfo!.icon!,
|
listedApps[index].installedInfo!.icon!,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
)
|
)
|
||||||
: null,
|
: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.rotationZ(0.31),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
child: Image(
|
||||||
|
image: const AssetImage(
|
||||||
|
'assets/graphics/icon_small.png'),
|
||||||
|
color: Colors.white.withOpacity(0.1),
|
||||||
|
colorBlendMode: BlendMode.modulate,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
title: Text(
|
title: Text(
|
||||||
sortedApps[index].installedInfo?.name ??
|
maxLines: 1,
|
||||||
sortedApps[index].app.name,
|
listedApps[index].installedInfo?.name ??
|
||||||
|
listedApps[index].app.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: sortedApps[index].app.pinned
|
overflow: TextOverflow.ellipsis,
|
||||||
|
fontWeight: listedApps[index].app.pinned
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal,
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
tr('byX', args: [sortedApps[index].app.author]),
|
tr('byX', args: [listedApps[index].app.author]),
|
||||||
|
maxLines: 1,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: sortedApps[index].app.pinned
|
overflow: TextOverflow.ellipsis,
|
||||||
|
fontWeight: listedApps[index].app.pinned
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal)),
|
: FontWeight.normal)),
|
||||||
trailing: SingleChildScrollView(
|
trailing: listedApps[index].downloadProgress != null
|
||||||
reverse: true,
|
|
||||||
child: sortedApps[index].downloadProgress != null
|
|
||||||
? Text(tr('percentProgress', args: [
|
? Text(tr('percentProgress', args: [
|
||||||
sortedApps[index]
|
listedApps[index]
|
||||||
.downloadProgress
|
.downloadProgress
|
||||||
?.toInt()
|
?.toInt()
|
||||||
.toString() ??
|
.toString() ??
|
||||||
'100'
|
'100'
|
||||||
]))
|
]))
|
||||||
: (Column(
|
: (Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
hasUpdate
|
||||||
|
? updateButton
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
hasUpdate
|
||||||
|
? const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
Row(
|
||||||
width: 100,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 150),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}',
|
'${listedApps[index].app.installedVersion ?? tr('notInstalled')}${listedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}',
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.ellipsis,
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
)),
|
)),
|
||||||
sortedApps[index].app.installedVersion !=
|
]),
|
||||||
null &&
|
Row(
|
||||||
sortedApps[index]
|
mainAxisSize: MainAxisSize.min,
|
||||||
.app
|
children: [
|
||||||
.installedVersion !=
|
GestureDetector(
|
||||||
sortedApps[index]
|
onTap: showChanges,
|
||||||
.app
|
child: Text(
|
||||||
.latestVersion
|
listedApps[index].app.releaseDate ==
|
||||||
? GestureDetector(
|
|
||||||
onTap: changesUrl == null
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
launchUrlString(changesUrl,
|
|
||||||
mode: LaunchMode
|
|
||||||
.externalApplication);
|
|
||||||
},
|
|
||||||
child: appsProvider
|
|
||||||
.areDownloadsRunning()
|
|
||||||
? Text(tr('pleaseWait'))
|
|
||||||
: Text(
|
|
||||||
'${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBracketsShort')}' : ''}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontStyle:
|
|
||||||
FontStyle.italic,
|
|
||||||
decoration: changesUrl ==
|
|
||||||
null
|
null
|
||||||
? TextDecoration.none
|
? showChanges != null
|
||||||
: TextDecoration
|
? tr('changes')
|
||||||
.underline),
|
: ''
|
||||||
|
: DateFormat('yyyy-MM-dd')
|
||||||
|
.format(listedApps[index]
|
||||||
|
.app
|
||||||
|
.releaseDate!),
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
decoration: showChanges != null
|
||||||
|
? TextDecoration.underline
|
||||||
|
: TextDecoration.none),
|
||||||
))
|
))
|
||||||
: const SizedBox(),
|
|
||||||
],
|
],
|
||||||
))),
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (selectedApps.isNotEmpty) {
|
if (selectedApps.isNotEmpty) {
|
||||||
toggleAppSelected(sortedApps[index].app);
|
toggleAppSelected(listedApps[index].app);
|
||||||
} else {
|
} else {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
AppPage(appId: sortedApps[index].app.id)),
|
AppPage(appId: listedApps[index].app.id)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}, childCount: sortedApps.length))
|
}, childCount: listedApps.length))
|
||||||
])),
|
])),
|
||||||
persistentFooterButtons: [
|
persistentFooterButtons: appsProvider.apps.isEmpty
|
||||||
|
? null
|
||||||
|
: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
selectedApps.isEmpty
|
selectedApps.isEmpty
|
||||||
? TextButton.icon(
|
? TextButton.icon(
|
||||||
style:
|
style: const ButtonStyle(
|
||||||
const ButtonStyle(visualDensity: VisualDensity.compact),
|
visualDensity: VisualDensity.compact),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
selectThese(sortedApps.map((e) => e.app).toList());
|
selectThese(listedApps.map((e) => e.app).toList());
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.select_all_outlined,
|
Icons.select_all_outlined,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
label: Text(sortedApps.length.toString()))
|
label: Text(listedApps.length.toString()))
|
||||||
: TextButton.icon(
|
: TextButton.icon(
|
||||||
style:
|
style: const ButtonStyle(
|
||||||
const ButtonStyle(visualDensity: VisualDensity.compact),
|
visualDensity: VisualDensity.compact),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
selectedApps.isEmpty
|
selectedApps.isEmpty
|
||||||
? selectThese(sortedApps.map((e) => e.app).toList())
|
? selectThese(
|
||||||
|
listedApps.map((e) => e.app).toList())
|
||||||
: clearSelected();
|
: clearSelected();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@ -386,42 +530,27 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
onPressed: selectedApps.isEmpty
|
onPressed: selectedApps.isEmpty
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog<Map<String, dynamic>?>(
|
appsProvider.removeAppsWithModal(
|
||||||
context: context,
|
context, selectedApps.toList());
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return GeneratedFormModal(
|
|
||||||
title:
|
|
||||||
tr('removeSelectedAppsQuestion'),
|
|
||||||
items: const [],
|
|
||||||
initValid: true,
|
|
||||||
message: tr(
|
|
||||||
'xWillBeRemovedButRemainInstalled',
|
|
||||||
args: [
|
|
||||||
plural(
|
|
||||||
'apps', selectedApps.length)
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}).then((values) {
|
|
||||||
if (values != null) {
|
|
||||||
appsProvider.removeApps(selectedApps
|
|
||||||
.map((e) => e.id)
|
|
||||||
.toList());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
tooltip: tr('removeSelectedApps'),
|
tooltip: tr('removeSelectedApps'),
|
||||||
icon: const Icon(Icons.delete_outline_outlined),
|
icon: const Icon(Icons.delete_outline_outlined),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: appsProvider.areDownloadsRunning() ||
|
onPressed: appsProvider
|
||||||
(existingUpdateIdsAllOrSelected.isEmpty &&
|
.areDownloadsRunning() ||
|
||||||
newInstallIdsAllOrSelected.isEmpty &&
|
(existingUpdateIdsAllOrSelected
|
||||||
trackOnlyUpdateIdsAllOrSelected.isEmpty)
|
.isEmpty &&
|
||||||
|
newInstallIdsAllOrSelected
|
||||||
|
.isEmpty &&
|
||||||
|
trackOnlyUpdateIdsAllOrSelected
|
||||||
|
.isEmpty)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
List<GeneratedFormItem> formItems = [];
|
List<GeneratedFormItem> formItems =
|
||||||
|
[];
|
||||||
if (existingUpdateIdsAllOrSelected
|
if (existingUpdateIdsAllOrSelected
|
||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
formItems.add(GeneratedFormSwitch(
|
formItems.add(GeneratedFormSwitch(
|
||||||
@ -434,7 +563,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
]),
|
]),
|
||||||
defaultValue: true));
|
defaultValue: true));
|
||||||
}
|
}
|
||||||
if (newInstallIdsAllOrSelected.isNotEmpty) {
|
if (newInstallIdsAllOrSelected
|
||||||
|
.isNotEmpty) {
|
||||||
formItems.add(GeneratedFormSwitch(
|
formItems.add(GeneratedFormSwitch(
|
||||||
'installs',
|
'installs',
|
||||||
label: tr('installX', args: [
|
label: tr('installX', args: [
|
||||||
@ -451,7 +581,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
formItems.add(GeneratedFormSwitch(
|
formItems.add(GeneratedFormSwitch(
|
||||||
'trackonlies',
|
'trackonlies',
|
||||||
label: tr('markXTrackOnlyAsUpdated',
|
label: tr(
|
||||||
|
'markXTrackOnlyAsUpdated',
|
||||||
args: [
|
args: [
|
||||||
plural(
|
plural(
|
||||||
'apps',
|
'apps',
|
||||||
@ -467,8 +598,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
showDialog<Map<String, dynamic>?>(
|
showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
var totalApps =
|
var totalApps = existingUpdateIdsAllOrSelected
|
||||||
existingUpdateIdsAllOrSelected.length +
|
.length +
|
||||||
newInstallIdsAllOrSelected
|
newInstallIdsAllOrSelected
|
||||||
.length +
|
.length +
|
||||||
trackOnlyUpdateIdsAllOrSelected
|
trackOnlyUpdateIdsAllOrSelected
|
||||||
@ -560,7 +691,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
cont = await showDialog<
|
cont = await showDialog<
|
||||||
Map<String, dynamic>?>(
|
Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder:
|
||||||
|
(BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('categorize'),
|
title: tr('categorize'),
|
||||||
items: const [],
|
items: const [],
|
||||||
@ -572,7 +704,9 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
if (cont) {
|
if (cont) {
|
||||||
await showDialog<Map<String, dynamic>?>(
|
// ignore: use_build_context_synchronously
|
||||||
|
await showDialog<
|
||||||
|
Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
@ -586,11 +720,15 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
preselected: !showPrompt
|
preselected: !showPrompt
|
||||||
? preselected ?? {}
|
? preselected ?? {}
|
||||||
: {},
|
: {},
|
||||||
showLabelWhenNotEmpty: false,
|
showLabelWhenNotEmpty:
|
||||||
onSelected: (categories) {
|
false,
|
||||||
|
onSelected:
|
||||||
|
(categories) {
|
||||||
appsProvider.saveApps(
|
appsProvider.saveApps(
|
||||||
selectedApps.map((e) {
|
selectedApps
|
||||||
e.categories = categories;
|
.map((e) {
|
||||||
|
e.categories =
|
||||||
|
categories;
|
||||||
return e;
|
return e;
|
||||||
}).toList());
|
}).toList());
|
||||||
},
|
},
|
||||||
@ -618,7 +756,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
scrollable: true,
|
scrollable: true,
|
||||||
content: Padding(
|
content: Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.only(top: 6),
|
const EdgeInsets.only(
|
||||||
|
top: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment
|
MainAxisAlignment
|
||||||
@ -636,33 +775,26 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
(BuildContext
|
(BuildContext
|
||||||
ctx) {
|
ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(tr(
|
title:
|
||||||
'markXSelectedAppsAsUpdated',
|
Text(tr('markXSelectedAppsAsUpdated', args: [
|
||||||
args: [
|
|
||||||
selectedApps.length.toString()
|
selectedApps.length.toString()
|
||||||
])),
|
])),
|
||||||
content:
|
content:
|
||||||
Text(
|
Text(
|
||||||
tr('onlyWorksWithNonEVDApps'),
|
tr('onlyWorksWithNonVersionDetectApps'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
|
||||||
fontWeight:
|
|
||||||
FontWeight.bold,
|
|
||||||
fontStyle: FontStyle.italic),
|
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: () {
|
||||||
() {
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child:
|
child: Text(tr('no'))),
|
||||||
Text(tr('no'))),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: () {
|
||||||
() {
|
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
appsProvider.saveApps(selectedApps.map((a) {
|
appsProvider.saveApps(selectedApps.map((a) {
|
||||||
if (a.installedVersion != null) {
|
if (a.installedVersion != null && a.additionalSettings['versionDetection'] != 'standardVersionDetection') {
|
||||||
a.installedVersion = a.latestVersion;
|
a.installedVersion = a.latestVersion;
|
||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
@ -670,8 +802,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child:
|
child: Text(tr('yes')))
|
||||||
Text(tr('yes')))
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).whenComplete(() {
|
}).whenComplete(() {
|
||||||
@ -686,29 +817,36 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
Icons.done)),
|
Icons.done)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
var pinStatus =
|
var pinStatus = selectedApps
|
||||||
selectedApps
|
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element
|
element
|
||||||
.pinned)
|
.pinned)
|
||||||
.isEmpty;
|
.isEmpty;
|
||||||
appsProvider.saveApps(
|
appsProvider
|
||||||
selectedApps.map((e) {
|
.saveApps(
|
||||||
e.pinned = pinStatus;
|
selectedApps
|
||||||
|
.map(
|
||||||
|
(e) {
|
||||||
|
e.pinned =
|
||||||
|
pinStatus;
|
||||||
return e;
|
return e;
|
||||||
}).toList());
|
}).toList());
|
||||||
Navigator.of(context)
|
Navigator.of(
|
||||||
|
context)
|
||||||
.pop();
|
.pop();
|
||||||
},
|
},
|
||||||
tooltip: selectedApps
|
tooltip: selectedApps
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element.pinned)
|
element
|
||||||
|
.pinned)
|
||||||
.isEmpty
|
.isEmpty
|
||||||
? tr('pinToTop')
|
? tr('pinToTop')
|
||||||
: tr('unpinFromTop'),
|
: tr(
|
||||||
|
'unpinFromTop'),
|
||||||
icon: Icon(selectedApps
|
icon: Icon(selectedApps
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element.pinned)
|
element
|
||||||
|
.pinned)
|
||||||
.isEmpty
|
.isEmpty
|
||||||
? Icons
|
? Icons
|
||||||
.bookmark_outline_rounded
|
.bookmark_outline_rounded
|
||||||
@ -720,53 +858,63 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
String urls = '';
|
String urls = '';
|
||||||
for (var a
|
for (var a
|
||||||
in selectedApps) {
|
in selectedApps) {
|
||||||
urls += '${a.url}\n';
|
urls +=
|
||||||
|
'${a.url}\n';
|
||||||
}
|
}
|
||||||
urls = urls.substring(
|
urls =
|
||||||
0, urls.length - 1);
|
urls.substring(
|
||||||
|
0,
|
||||||
|
urls.length -
|
||||||
|
1);
|
||||||
Share.share(urls,
|
Share.share(urls,
|
||||||
subject: tr(
|
subject: tr(
|
||||||
'selectedAppURLsFromObtainium'));
|
'selectedAppURLsFromObtainium'));
|
||||||
Navigator.of(context)
|
Navigator.of(
|
||||||
|
context)
|
||||||
.pop();
|
.pop();
|
||||||
},
|
},
|
||||||
tooltip: tr(
|
tooltip: tr(
|
||||||
'shareSelectedAppURLs'),
|
'shareSelectedAppURLs'),
|
||||||
icon:
|
icon: const Icon(
|
||||||
const Icon(Icons.share),
|
Icons.share),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context:
|
||||||
builder: (BuildContext
|
context,
|
||||||
|
builder:
|
||||||
|
(BuildContext
|
||||||
ctx) {
|
ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr(
|
title: tr(
|
||||||
'resetInstallStatusForSelectedAppsQuestion'),
|
'resetInstallStatusForSelectedAppsQuestion'),
|
||||||
items: const [],
|
items: const [],
|
||||||
initValid: true,
|
initValid:
|
||||||
|
true,
|
||||||
message: tr(
|
message: tr(
|
||||||
'installStatusOfXWillBeResetExplanation',
|
'installStatusOfXWillBeResetExplanation',
|
||||||
args: [
|
args: [
|
||||||
plural(
|
plural(
|
||||||
'app',
|
'app',
|
||||||
selectedApps
|
selectedApps.length)
|
||||||
.length)
|
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values !=
|
||||||
|
null) {
|
||||||
appsProvider.saveApps(
|
appsProvider.saveApps(
|
||||||
selectedApps
|
selectedApps
|
||||||
.map((e) {
|
.map(
|
||||||
|
(e) {
|
||||||
e.installedVersion =
|
e.installedVersion =
|
||||||
null;
|
null;
|
||||||
return e;
|
return e;
|
||||||
}).toList());
|
}).toList());
|
||||||
}
|
}
|
||||||
}).whenComplete(() {
|
}).whenComplete(() {
|
||||||
Navigator.of(context)
|
Navigator.of(
|
||||||
|
context)
|
||||||
.pop();
|
.pop();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -807,11 +955,9 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
appsProvider.apps.isEmpty
|
TextButton.icon(
|
||||||
? const SizedBox()
|
style: const ButtonStyle(
|
||||||
: TextButton.icon(
|
visualDensity: VisualDensity.compact),
|
||||||
style:
|
|
||||||
const ButtonStyle(visualDensity: VisualDensity.compact),
|
|
||||||
label: Text(
|
label: Text(
|
||||||
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
||||||
? tr('filter')
|
? tr('filter')
|
||||||
@ -859,7 +1005,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
CategoryEditorSelector(
|
CategoryEditorSelector(
|
||||||
preselected: filter.categoryFilter,
|
preselected: filter.categoryFilter,
|
||||||
onSelected: (categories) {
|
onSelected: (categories) {
|
||||||
filter.categoryFilter = categories.toSet();
|
filter.categoryFilter =
|
||||||
|
categories.toSet();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -63,21 +63,29 @@ class _HomePageState extends State<HomePage> {
|
|||||||
.map((e) =>
|
.map((e) =>
|
||||||
NavigationDestination(icon: Icon(e.icon), label: e.title))
|
NavigationDestination(icon: Icon(e.icon), label: e.title))
|
||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: (int index) {
|
onDestinationSelected: (int index) async {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
setState(() {
|
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
|
while ((pages[0].widget.key as GlobalKey<AppsPageState>)
|
||||||
|
.currentState !=
|
||||||
|
null) {
|
||||||
|
// Avoid duplicate GlobalKey error
|
||||||
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
selectedIndexHistory.clear();
|
selectedIndexHistory.clear();
|
||||||
|
});
|
||||||
} else if (selectedIndexHistory.isEmpty ||
|
} else if (selectedIndexHistory.isEmpty ||
|
||||||
(selectedIndexHistory.isNotEmpty &&
|
(selectedIndexHistory.isNotEmpty &&
|
||||||
selectedIndexHistory.last != index)) {
|
selectedIndexHistory.last != index)) {
|
||||||
|
setState(() {
|
||||||
int existingInd = selectedIndexHistory.indexOf(index);
|
int existingInd = selectedIndexHistory.indexOf(index);
|
||||||
if (existingInd >= 0) {
|
if (existingInd >= 0) {
|
||||||
selectedIndexHistory.removeAt(existingInd);
|
selectedIndexHistory.removeAt(existingInd);
|
||||||
}
|
}
|
||||||
selectedIndexHistory.add(index);
|
selectedIndexHistory.add(index);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
selectedIndex:
|
selectedIndex:
|
||||||
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||||
|
@ -41,6 +41,66 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
urlListImport({String? initValue, bool overrideInitValid = false}) {
|
||||||
|
showDialog<Map<String, dynamic>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
initValid: overrideInitValid,
|
||||||
|
title: tr('importFromURLList'),
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('appURLList',
|
||||||
|
defaultValue: initValue ?? '',
|
||||||
|
label: tr('appURLList'),
|
||||||
|
max: 7,
|
||||||
|
additionalValidators: [
|
||||||
|
(dynamic value) {
|
||||||
|
if (value != null && value.isNotEmpty) {
|
||||||
|
var lines = value.trim().split('\n');
|
||||||
|
for (int i = 0; i < lines.length; i++) {
|
||||||
|
try {
|
||||||
|
sourceProvider.getSource(lines[i]);
|
||||||
|
} catch (e) {
|
||||||
|
return '${tr('line')} ${i + 1}: $e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
var urls = (values['appURLList'] as String).split('\n');
|
||||||
|
setState(() {
|
||||||
|
importInProgress = true;
|
||||||
|
});
|
||||||
|
appsProvider.addAppsByURL(urls).then((errors) {
|
||||||
|
if (errors.isEmpty) {
|
||||||
|
showError(tr('importedX', args: [plural('apps', urls.length)]),
|
||||||
|
context);
|
||||||
|
} else {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return ImportErrorDialog(
|
||||||
|
urlsLength: urls.length, errors: errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@ -150,6 +210,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
const Divider(
|
const Divider(
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
@ -157,81 +219,51 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
onPressed: importInProgress
|
onPressed: importInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog<Map<String, dynamic>?>(
|
urlListImport();
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return GeneratedFormModal(
|
|
||||||
title: tr('importFromURLList'),
|
|
||||||
items: [
|
|
||||||
[
|
|
||||||
GeneratedFormTextField(
|
|
||||||
'appURLList',
|
|
||||||
label: tr('appURLList'),
|
|
||||||
max: 7,
|
|
||||||
additionalValidators: [
|
|
||||||
(dynamic value) {
|
|
||||||
if (value != null &&
|
|
||||||
value.isNotEmpty) {
|
|
||||||
var lines = value
|
|
||||||
.trim()
|
|
||||||
.split('\n');
|
|
||||||
for (int i = 0;
|
|
||||||
i < lines.length;
|
|
||||||
i++) {
|
|
||||||
try {
|
|
||||||
sourceProvider
|
|
||||||
.getSource(
|
|
||||||
lines[i]);
|
|
||||||
} catch (e) {
|
|
||||||
return '${tr('line')} ${i + 1}: $e';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
])
|
|
||||||
]
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}).then((values) {
|
|
||||||
if (values != null) {
|
|
||||||
var urls =
|
|
||||||
(values['appURLList'] as String)
|
|
||||||
.split('\n');
|
|
||||||
setState(() {
|
|
||||||
importInProgress = true;
|
|
||||||
});
|
|
||||||
appsProvider
|
|
||||||
.addAppsByURL(urls)
|
|
||||||
.then((errors) {
|
|
||||||
if (errors.isEmpty) {
|
|
||||||
showError(
|
|
||||||
tr('importedX', args: [
|
|
||||||
plural('apps', urls.length)
|
|
||||||
]),
|
|
||||||
context);
|
|
||||||
} else {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return ImportErrorDialog(
|
|
||||||
urlsLength: urls.length,
|
|
||||||
errors: errors);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catchError((e) {
|
|
||||||
showError(e, context);
|
|
||||||
}).whenComplete(() {
|
|
||||||
setState(() {
|
|
||||||
importInProgress = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('importFromURLList'),
|
tr('importFromURLList'),
|
||||||
)),
|
)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: importInProgress
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
FilePicker.platform
|
||||||
|
.pickFiles()
|
||||||
|
.then((result) {
|
||||||
|
if (result != null) {
|
||||||
|
urlListImport(
|
||||||
|
overrideInitValid: true,
|
||||||
|
initValue:
|
||||||
|
RegExp('https?://[^"]+')
|
||||||
|
.allMatches(File(result
|
||||||
|
.files
|
||||||
|
.single
|
||||||
|
.path!)
|
||||||
|
.readAsStringSync())
|
||||||
|
.map((e) =>
|
||||||
|
e.input.substring(
|
||||||
|
e.start, e.end))
|
||||||
|
.toSet()
|
||||||
|
.toList()
|
||||||
|
.where((url) {
|
||||||
|
try {
|
||||||
|
sourceProvider
|
||||||
|
.getSource(url);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).join('\n'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
tr('importFromURLsInFile'),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
...sourceProvider.sources
|
...sourceProvider.sources
|
||||||
.where((element) => element.canSearch)
|
.where((element) => element.canSearch)
|
||||||
.map((source) => Column(
|
.map((source) => Column(
|
||||||
@ -280,6 +312,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
if (urlsWithDescriptions
|
if (urlsWithDescriptions
|
||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog<
|
await showDialog<
|
||||||
List<
|
List<
|
||||||
String>?>(
|
String>?>(
|
||||||
@ -314,6 +347,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
]),
|
]),
|
||||||
context);
|
context);
|
||||||
} else {
|
} else {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
@ -391,6 +425,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
e.toString())
|
e.toString())
|
||||||
.toList());
|
.toList());
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog<
|
await showDialog<
|
||||||
List<String>?>(
|
List<String>?>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -418,6 +453,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
]),
|
]),
|
||||||
context);
|
context);
|
||||||
} else {
|
} else {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
@ -564,10 +600,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
||||||
return Row(children: [
|
select(bool? value) {
|
||||||
Checkbox(
|
|
||||||
value: urlWithDescriptionSelections[urlWithD],
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
value ??= false;
|
value ??= false;
|
||||||
if (value! && widget.onlyOneSelectionAllowed) {
|
if (value! && widget.onlyOneSelectionAllowed) {
|
||||||
@ -576,6 +609,13 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
urlWithDescriptionSelections[urlWithD] = value!;
|
urlWithDescriptionSelections[urlWithD] = value!;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(children: [
|
||||||
|
Checkbox(
|
||||||
|
value: urlWithDescriptionSelections[urlWithD],
|
||||||
|
onChanged: (value) {
|
||||||
|
select(value);
|
||||||
}),
|
}),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
@ -599,13 +639,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
const TextStyle(decoration: TextDecoration.underline),
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
)),
|
)),
|
||||||
Text(
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
select(!(urlWithDescriptionSelections[urlWithD] ?? false));
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
urlWithD.value.length > 128
|
urlWithD.value.length > 128
|
||||||
? '${urlWithD.value.substring(0, 128)}...'
|
? '${urlWithD.value.substring(0, 128)}...'
|
||||||
: urlWithD.value,
|
: urlWithD.value,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontStyle: FontStyle.italic, fontSize: 12),
|
fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
)
|
)
|
||||||
|
@ -4,10 +4,8 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/components/generated_form_modal.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -89,6 +87,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var sortDropdown = DropdownButtonFormField(
|
var sortDropdown = DropdownButtonFormField(
|
||||||
|
isExpanded: true,
|
||||||
decoration: InputDecoration(labelText: tr('appSortBy')),
|
decoration: InputDecoration(labelText: tr('appSortBy')),
|
||||||
value: settingsProvider.sortColumn,
|
value: settingsProvider.sortColumn,
|
||||||
items: [
|
items: [
|
||||||
@ -103,6 +102,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: SortColumnSettings.added,
|
value: SortColumnSettings.added,
|
||||||
child: Text(tr('asAdded')),
|
child: Text(tr('asAdded')),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: SortColumnSettings.releaseDate,
|
||||||
|
child: Text(tr('releaseDate')),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -112,6 +115,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var orderDropdown = DropdownButtonFormField(
|
var orderDropdown = DropdownButtonFormField(
|
||||||
|
isExpanded: true,
|
||||||
decoration: InputDecoration(labelText: tr('appSortOrder')),
|
decoration: InputDecoration(labelText: tr('appSortOrder')),
|
||||||
value: settingsProvider.sortOrder,
|
value: settingsProvider.sortOrder,
|
||||||
items: [
|
items: [
|
||||||
@ -148,7 +152,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.setLocale(Locale(value));
|
context.setLocale(Locale(value));
|
||||||
} else {
|
} else {
|
||||||
context.resetLocale();
|
settingsProvider.resetLocaleSafe(context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:android_intent_plus/flag.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -12,6 +13,8 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
||||||
import 'package:installed_apps/app_info.dart';
|
import 'package:installed_apps/app_info.dart';
|
||||||
import 'package:installed_apps/installed_apps.dart';
|
import 'package:installed_apps/installed_apps.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
@ -23,6 +26,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
|
|
||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
@ -141,13 +145,19 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
|
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
|
||||||
|
NotificationsProvider? notificationsProvider =
|
||||||
|
context?.read<NotificationsProvider>();
|
||||||
|
var notifId = DownloadNotification(app.name, 0).id;
|
||||||
|
if (apps[app.id] != null) {
|
||||||
|
apps[app.id]!.downloadProgress = 0;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
try {
|
||||||
var fileName =
|
var fileName =
|
||||||
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk';
|
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk';
|
||||||
String downloadUrl = await SourceProvider()
|
String downloadUrl = await SourceProvider()
|
||||||
.getSource(app.url)
|
.getSource(app.url)
|
||||||
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex]);
|
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex]);
|
||||||
NotificationsProvider? notificationsProvider =
|
|
||||||
context?.read<NotificationsProvider>();
|
|
||||||
var notif = DownloadNotification(app.name, 100);
|
var notif = DownloadNotification(app.name, 100);
|
||||||
notificationsProvider?.cancel(notif.id);
|
notificationsProvider?.cancel(notif.id);
|
||||||
int? prevProg;
|
int? prevProg;
|
||||||
@ -164,7 +174,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
prevProg = prog;
|
prevProg = prog;
|
||||||
});
|
});
|
||||||
notificationsProvider?.cancel(notif.id);
|
|
||||||
// Delete older versions of the APK if any
|
// Delete older versions of the APK if any
|
||||||
for (var file in downloadedFile.parent.listSync()) {
|
for (var file in downloadedFile.parent.listSync()) {
|
||||||
var fn = file.path.split('/').last;
|
var fn = file.path.split('/').last;
|
||||||
@ -178,7 +187,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
// The former case should be handled (give the App its real ID), the latter is a security issue
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||||
if (app.id != newInfo.packageName) {
|
if (app.id != newInfo.packageName) {
|
||||||
if (apps[app.id] != null && !SourceProvider().isTempId(app.id)) {
|
if (apps[app.id] != null && !SourceProvider().isTempId(app)) {
|
||||||
throw IDChangedError();
|
throw IDChangedError();
|
||||||
}
|
}
|
||||||
var originalAppId = app.id;
|
var originalAppId = app.id;
|
||||||
@ -191,6 +200,13 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DownloadedApk(app.id, downloadedFile);
|
return DownloadedApk(app.id, downloadedFile);
|
||||||
|
} finally {
|
||||||
|
notificationsProvider?.cancel(notifId);
|
||||||
|
if (apps[app.id] != null) {
|
||||||
|
apps[app.id]!.downloadProgress = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool areDownloadsRunning() => apps.values
|
bool areDownloadsRunning() => apps.values
|
||||||
@ -247,7 +263,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
!(await canDowngradeApps())) {
|
!(await canDowngradeApps())) {
|
||||||
throw DowngradeError();
|
throw DowngradeError();
|
||||||
}
|
}
|
||||||
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
|
await InstallPlugin.installApk(file.file.path, obtainiumId);
|
||||||
|
if (file.appId == obtainiumId) {
|
||||||
|
// Obtainium prompt should be lowest
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
apps[file.appId]!.app.installedVersion =
|
apps[file.appId]!.app.installedVersion =
|
||||||
apps[file.appId]!.app.latestVersion;
|
apps[file.appId]!.app.latestVersion;
|
||||||
// Don't correct install status as installation may not be done yet
|
// Don't correct install status as installation may not be done yet
|
||||||
@ -255,6 +275,15 @@ class AppsProvider with ChangeNotifier {
|
|||||||
attemptToCorrectInstallStatus: false);
|
attemptToCorrectInstallStatus: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uninstallApp(String appId) async {
|
||||||
|
var intent = AndroidIntent(
|
||||||
|
action: 'android.intent.action.DELETE',
|
||||||
|
data: 'package:$appId',
|
||||||
|
flags: <int>[Flag.FLAG_ACTIVITY_NEW_TASK],
|
||||||
|
package: 'vnd.android.package-archive');
|
||||||
|
await intent.launch();
|
||||||
|
}
|
||||||
|
|
||||||
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
||||||
// If the App has more than one APK, the user should pick one (if context provided)
|
// If the App has more than one APK, the user should pick one (if context provided)
|
||||||
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
||||||
@ -262,6 +291,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
|
|
||||||
if (app.apkUrls.length > 1 && context != null) {
|
if (app.apkUrls.length > 1 && context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
apkUrl = await showDialog(
|
apkUrl = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -281,6 +311,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (apkUrl != null &&
|
if (apkUrl != null &&
|
||||||
getHost(apkUrl) != getHost(app.url) &&
|
getHost(apkUrl) != getHost(app.url) &&
|
||||||
context != null) {
|
context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
if (await showDialog(
|
if (await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -438,9 +469,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
if (!res) {
|
|
||||||
logs.add(tr('versionCorrectionDisabled'));
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,8 +479,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
|
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
|
||||||
var modded = false;
|
var modded = false;
|
||||||
var trackOnly = app.additionalSettings['trackOnly'] == true;
|
var trackOnly = app.additionalSettings['trackOnly'] == true;
|
||||||
var noVersionDetection =
|
var noVersionDetection = app.additionalSettings['versionDetection'] !=
|
||||||
app.additionalSettings['noVersionDetection'] == true;
|
'standardVersionDetection';
|
||||||
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
|
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
|
||||||
app.installedVersion = null;
|
app.installedVersion = null;
|
||||||
modded = true;
|
modded = true;
|
||||||
@ -543,7 +571,21 @@ class AppsProvider with ChangeNotifier {
|
|||||||
List<App> newApps = (await getAppsDir())
|
List<App> newApps = (await getAppsDir())
|
||||||
.listSync()
|
.listSync()
|
||||||
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
||||||
.map((e) => App.fromJson(jsonDecode(File(e.path).readAsStringSync())))
|
.map((e) {
|
||||||
|
try {
|
||||||
|
return App.fromJson(jsonDecode(File(e.path).readAsStringSync()));
|
||||||
|
} catch (err) {
|
||||||
|
if (err is FormatException) {
|
||||||
|
logs.add('Corrupt JSON when loading App (will be ignored): $e');
|
||||||
|
e.renameSync('${e.path}.corrupt');
|
||||||
|
return App(
|
||||||
|
'', '', '', '', '', '', [], 0, {}, DateTime.now(), false);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where((element) => element.id.isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
var idsToDelete = apps.values
|
var idsToDelete = apps.values
|
||||||
.map((e) => e.app.id)
|
.map((e) => e.app.id)
|
||||||
@ -619,6 +661,57 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> removeAppsWithModal(BuildContext context, List<App> apps) async {
|
||||||
|
var showUninstallOption =
|
||||||
|
apps.where((a) => a.installedVersion != null).isNotEmpty;
|
||||||
|
var values = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: plural('removeAppQuestion', apps.length),
|
||||||
|
items: !showUninstallOption
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('rmAppEntry',
|
||||||
|
label: tr('removeFromObtainium'), defaultValue: true)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('uninstallApp',
|
||||||
|
label: tr('uninstallFromDevice'))
|
||||||
|
]
|
||||||
|
],
|
||||||
|
initValid: true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (values != null) {
|
||||||
|
bool uninstall = values['uninstallApp'] == true && showUninstallOption;
|
||||||
|
bool remove = values['rmAppEntry'] == true || !showUninstallOption;
|
||||||
|
if (uninstall) {
|
||||||
|
for (var i = 0; i < apps.length; i++) {
|
||||||
|
if (apps[i].installedVersion != null) {
|
||||||
|
uninstallApp(apps[i].id);
|
||||||
|
apps[i].installedVersion = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveApps(apps, attemptToCorrectInstallStatus: false);
|
||||||
|
}
|
||||||
|
if (remove) {
|
||||||
|
await removeApps(apps.map((e) => e.id).toList());
|
||||||
|
}
|
||||||
|
return uninstall || remove;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openAppSettings(String appId) async {
|
||||||
|
final AndroidIntent intent = AndroidIntent(
|
||||||
|
action: 'action_application_details_settings',
|
||||||
|
data: 'package:$appId',
|
||||||
|
);
|
||||||
|
await intent.launch();
|
||||||
|
}
|
||||||
|
|
||||||
Future<App?> checkUpdate(String appId) async {
|
Future<App?> checkUpdate(String appId) async {
|
||||||
App? currentApp = apps[appId]!.app;
|
App? currentApp = apps[appId]!.app;
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
@ -704,7 +797,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
exportDir = await getExternalStorageDirectory();
|
exportDir = await getExternalStorageDirectory();
|
||||||
path = exportDir!.path;
|
path = exportDir!.path;
|
||||||
}
|
}
|
||||||
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 28) {
|
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
|
||||||
if (await Permission.storage.isDenied) {
|
if (await Permission.storage.isDenied) {
|
||||||
await Permission.storage.request();
|
await Permission.storage.request();
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -18,7 +17,7 @@ enum ThemeSettings { system, light, dark }
|
|||||||
|
|
||||||
enum ColourSettings { basic, materialYou }
|
enum ColourSettings { basic, materialYou }
|
||||||
|
|
||||||
enum SortColumnSettings { added, nameAuthor, authorName }
|
enum SortColumnSettings { added, nameAuthor, authorName, releaseDate }
|
||||||
|
|
||||||
enum SortOrderSettings { ascending, descending }
|
enum SortOrderSettings { ascending, descending }
|
||||||
|
|
||||||
@ -179,4 +178,15 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
bool setEqual(Set<String> a, Set<String> b) =>
|
bool setEqual(Set<String> a, Set<String> b) =>
|
||||||
a.length == b.length && a.union(b).length == a.length;
|
a.length == b.length && a.union(b).length == a.length;
|
||||||
|
|
||||||
|
void resetLocaleSafe(BuildContext context) {
|
||||||
|
if (context.supportedLocales
|
||||||
|
.map((e) => e.languageCode)
|
||||||
|
.contains(context.deviceLocale.languageCode)) {
|
||||||
|
context.resetLocale();
|
||||||
|
} else {
|
||||||
|
context.setLocale(context.fallbackLocale!);
|
||||||
|
context.deleteSaveLocale();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,13 @@ import 'package:obtainium/app_sources/gitlab.dart';
|
|||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
import 'package:obtainium/app_sources/mullvad.dart';
|
import 'package:obtainium/app_sources/mullvad.dart';
|
||||||
|
import 'package:obtainium/app_sources/neutroncode.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
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/vlc.dart';
|
||||||
|
import 'package:obtainium/app_sources/whatsapp.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||||
@ -33,8 +37,11 @@ class APKDetails {
|
|||||||
late String version;
|
late String version;
|
||||||
late List<String> apkUrls;
|
late List<String> apkUrls;
|
||||||
late AppNames names;
|
late AppNames names;
|
||||||
|
late DateTime? releaseDate;
|
||||||
|
late String? changeLog;
|
||||||
|
|
||||||
APKDetails(this.version, this.apkUrls, this.names);
|
APKDetails(this.version, this.apkUrls, this.names,
|
||||||
|
{this.releaseDate, this.changeLog});
|
||||||
}
|
}
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
@ -50,6 +57,8 @@ class App {
|
|||||||
late DateTime? lastUpdateCheck;
|
late DateTime? lastUpdateCheck;
|
||||||
bool pinned = false;
|
bool pinned = false;
|
||||||
List<String> categories;
|
List<String> categories;
|
||||||
|
late DateTime? releaseDate;
|
||||||
|
late String? changeLog;
|
||||||
App(
|
App(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
@ -62,7 +71,9 @@ class App {
|
|||||||
this.additionalSettings,
|
this.additionalSettings,
|
||||||
this.lastUpdateCheck,
|
this.lastUpdateCheck,
|
||||||
this.pinned,
|
this.pinned,
|
||||||
{this.categories = const []});
|
{this.categories = const [],
|
||||||
|
this.releaseDate,
|
||||||
|
this.changeLog});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -97,6 +108,20 @@ class App {
|
|||||||
additionalSettings['noVersionDetection'] =
|
additionalSettings['noVersionDetection'] =
|
||||||
json['noVersionDetection'] == 'true' || json['trackOnly'] == true;
|
json['noVersionDetection'] == 'true' || json['trackOnly'] == true;
|
||||||
}
|
}
|
||||||
|
// Convert bool style version detection options to dropdown style
|
||||||
|
if (additionalSettings['noVersionDetection'] == true) {
|
||||||
|
additionalSettings['versionDetection'] = 'noVersionDetection';
|
||||||
|
}
|
||||||
|
if (additionalSettings['releaseDateAsVersion'] == true) {
|
||||||
|
additionalSettings['versionDetection'] = 'releaseDateAsVersion';
|
||||||
|
additionalSettings.remove('releaseDateAsVersion');
|
||||||
|
}
|
||||||
|
if (additionalSettings['noVersionDetection'] != null) {
|
||||||
|
additionalSettings.remove('noVersionDetection');
|
||||||
|
}
|
||||||
|
if (additionalSettings['releaseDateAsVersion'] != null) {
|
||||||
|
additionalSettings.remove('releaseDateAsVersion');
|
||||||
|
}
|
||||||
// Ensure additionalSettings are correctly typed
|
// Ensure additionalSettings are correctly typed
|
||||||
for (var item in formItems) {
|
for (var item in formItems) {
|
||||||
if (additionalSettings[item.key] != null) {
|
if (additionalSettings[item.key] != null) {
|
||||||
@ -134,7 +159,12 @@ class App {
|
|||||||
.toList()
|
.toList()
|
||||||
: json['category'] != null
|
: json['category'] != null
|
||||||
? [json['category'] as String]
|
? [json['category'] as String]
|
||||||
: []);
|
: [],
|
||||||
|
releaseDate: json['releaseDate'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||||
|
changeLog:
|
||||||
|
json['changeLog'] == null ? null : json['changeLog'] as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@ -149,7 +179,9 @@ class App {
|
|||||||
'additionalSettings': jsonEncode(additionalSettings),
|
'additionalSettings': jsonEncode(additionalSettings),
|
||||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||||
'pinned': pinned,
|
'pinned': pinned,
|
||||||
'categories': categories
|
'categories': categories,
|
||||||
|
'releaseDate': releaseDate?.microsecondsSinceEpoch,
|
||||||
|
'changeLog': changeLog
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +230,7 @@ class AppSource {
|
|||||||
String? host;
|
String? host;
|
||||||
late String name;
|
late String name;
|
||||||
bool enforceTrackOnly = false;
|
bool enforceTrackOnly = false;
|
||||||
|
bool changeLogIfAnyIsMarkDown = true;
|
||||||
|
|
||||||
AppSource() {
|
AppSource() {
|
||||||
name = runtimeType.toString();
|
name = runtimeType.toString();
|
||||||
@ -225,7 +258,28 @@ class AppSource {
|
|||||||
label: tr('trackOnly'),
|
label: tr('trackOnly'),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))]
|
[
|
||||||
|
GeneratedFormDropdown(
|
||||||
|
'versionDetection',
|
||||||
|
[
|
||||||
|
MapEntry(
|
||||||
|
'standardVersionDetection', tr('standardVersionDetection')),
|
||||||
|
MapEntry('releaseDateAsVersion', tr('releaseDateAsVersion')),
|
||||||
|
MapEntry('noVersionDetection', tr('noVersionDetection'))
|
||||||
|
],
|
||||||
|
label: tr('versionDetection'),
|
||||||
|
defaultValue: 'standardVersionDetection')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('apkFilterRegEx',
|
||||||
|
label: tr('filterAPKsByRegEx'),
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
return regExValidator(value);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
// Previous 2 variables combined into one at runtime for convenient usage
|
// Previous 2 variables combined into one at runtime for convenient usage
|
||||||
@ -269,6 +323,18 @@ abstract class MassAppUrlSource {
|
|||||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
regExValidator(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RegExp(value);
|
||||||
|
} catch (e) {
|
||||||
|
return tr('invalidRegEx');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
// Add more source classes here so they are available via the service
|
// Add more source classes here so they are available via the service
|
||||||
List<AppSource> sources = [
|
List<AppSource> sources = [
|
||||||
@ -277,12 +343,16 @@ class SourceProvider {
|
|||||||
Codeberg(),
|
Codeberg(),
|
||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
IzzyOnDroid(),
|
||||||
Mullvad(),
|
FDroidRepo(),
|
||||||
Signal(),
|
|
||||||
SourceForge(),
|
SourceForge(),
|
||||||
APKMirror(),
|
APKMirror(),
|
||||||
FDroidRepo(),
|
Mullvad(),
|
||||||
|
Signal(),
|
||||||
|
VLC(),
|
||||||
|
// WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
|
||||||
|
TelegramApp(),
|
||||||
SteamMobile(),
|
SteamMobile(),
|
||||||
|
NeutronCode(),
|
||||||
HTML() // This should ALWAYS be the last option as they are tried in order
|
HTML() // This should ALWAYS be the last option as they are tried in order
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -326,38 +396,34 @@ class SourceProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String generateTempID(AppNames names, AppSource source) =>
|
String generateTempID(
|
||||||
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}';
|
String standardUrl, Map<String, dynamic> additionalSettings) =>
|
||||||
|
(standardUrl + additionalSettings.toString()).hashCode.toString();
|
||||||
|
|
||||||
bool isTempId(String id) {
|
bool isTempId(App app) {
|
||||||
List<String> parts = id.split('_');
|
// return app.id == generateTempID(app.url, app.additionalSettings);
|
||||||
if (parts.length < 3) {
|
return RegExp('^[0-9]+\$').hasMatch(app.id);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < parts.length - 1; i++) {
|
|
||||||
if (RegExp('.*[A-Z].*').hasMatch(parts[i])) {
|
|
||||||
// TODO: Look into RegEx for non-Latin characters
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<App> getApp(
|
Future<App> getApp(
|
||||||
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
||||||
{App? currentApp,
|
{App? currentApp, bool trackOnlyOverride = false}) async {
|
||||||
bool trackOnlyOverride = false,
|
|
||||||
noVersionDetectionOverride = false}) async {
|
|
||||||
if (trackOnlyOverride || source.enforceTrackOnly) {
|
if (trackOnlyOverride || source.enforceTrackOnly) {
|
||||||
additionalSettings['trackOnly'] = true;
|
additionalSettings['trackOnly'] = true;
|
||||||
}
|
}
|
||||||
if (noVersionDetectionOverride) {
|
|
||||||
additionalSettings['noVersionDetection'] = true;
|
|
||||||
}
|
|
||||||
var trackOnly = additionalSettings['trackOnly'] == true;
|
var trackOnly = additionalSettings['trackOnly'] == true;
|
||||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
||||||
|
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
|
||||||
|
apk.releaseDate != null) {
|
||||||
|
apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||||
|
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||||
|
apk.apkUrls =
|
||||||
|
apk.apkUrls.where((element) => reg.hasMatch(element)).toList();
|
||||||
|
}
|
||||||
if (apk.apkUrls.isEmpty && !trackOnly) {
|
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
@ -368,7 +434,7 @@ class SourceProvider {
|
|||||||
currentApp?.id ??
|
currentApp?.id ??
|
||||||
source.tryInferringAppId(standardUrl,
|
source.tryInferringAppId(standardUrl,
|
||||||
additionalSettings: additionalSettings) ??
|
additionalSettings: additionalSettings) ??
|
||||||
generateTempID(apk.names, source),
|
generateTempID(standardUrl, additionalSettings),
|
||||||
standardUrl,
|
standardUrl,
|
||||||
apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
|
apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
|
||||||
name.trim().isNotEmpty
|
name.trim().isNotEmpty
|
||||||
@ -381,7 +447,9 @@ class SourceProvider {
|
|||||||
additionalSettings,
|
additionalSettings,
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
currentApp?.pinned ?? false,
|
currentApp?.pinned ?? false,
|
||||||
categories: currentApp?.categories ?? const []);
|
categories: currentApp?.categories ?? const [],
|
||||||
|
releaseDate: apk.releaseDate,
|
||||||
|
changeLog: apk.changeLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns errors in [results, errors] instead of throwing them
|
// Returns errors in [results, errors] instead of throwing them
|
||||||
|
498
pubspec.lock
12
pubspec.yaml
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.10.1+107 # When changing this, update the tag in main() accordingly
|
version: 0.11.13+134 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
@ -58,12 +58,13 @@ dependencies:
|
|||||||
android_alarm_manager_plus: ^2.1.0
|
android_alarm_manager_plus: ^2.1.0
|
||||||
sqflite: ^2.2.0+3
|
sqflite: ^2.2.0+3
|
||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
|
android_intent_plus: ^3.1.5
|
||||||
|
flutter_markdown: ^0.6.14
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_launcher_icons: ^0.11.0
|
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
@ -72,12 +73,6 @@ dev_dependencies:
|
|||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
|
|
||||||
flutter_icons:
|
|
||||||
android: true
|
|
||||||
image_path: "assets/graphics/icon.png"
|
|
||||||
adaptive_icon_background: "#FFFFFF"
|
|
||||||
adaptive_icon_foreground: "assets/graphics/icon.png"
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
@ -96,6 +91,7 @@ flutter:
|
|||||||
|
|
||||||
assets:
|
assets:
|
||||||
- assets/translations/
|
- assets/translations/
|
||||||
|
- assets/graphics/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|