Compare commits
1187 Commits
v0.11.33-b
...
v1.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d33ca0948f | ||
|
|
f76637a2e1 | ||
|
|
b7de627c7b | ||
|
|
27fc60d437 | ||
|
|
ec240f946e | ||
|
|
ecd80fc371 | ||
|
|
68fa660e6d | ||
|
|
70f9e33d17 | ||
|
|
2e2dffd8e2 | ||
|
|
0b1d5bf514 | ||
|
|
2570c8e289 | ||
|
|
43a8ba4de1 | ||
|
|
3f2fb1c1ed | ||
|
|
40d303fb57 | ||
|
|
2c1687c33d | ||
|
|
b688e7f160 | ||
|
|
0c80d88583 | ||
|
|
b293b1e9ff | ||
|
|
61038a8969 | ||
|
|
b92d1541bf | ||
|
|
13f0ccb10d | ||
|
|
ed732cfad2 | ||
|
|
fb206aee84 | ||
|
|
285530784d | ||
|
|
0ddb5b5e81 | ||
|
|
c16cda1962 | ||
|
|
49022726d3 | ||
|
|
a311894b9f | ||
|
|
29e13efd66 | ||
|
|
8c2a97e092 | ||
|
|
34d571f586 | ||
|
|
a20b87889b | ||
|
|
c7319cd958 | ||
|
|
d61fd800ef | ||
|
|
12dda8bfa9 | ||
|
|
0657f832e1 | ||
|
|
6431357b15 | ||
|
|
dcc42bdfe5 | ||
|
|
85718dc3a3 | ||
|
|
9dff352796 | ||
|
|
c5477767a0 | ||
|
|
3131ef8c4e | ||
|
|
4d9f05aa87 | ||
|
|
7b882d9bd8 | ||
|
|
d1a2831922 | ||
|
|
b042050ea3 | ||
|
|
2d43dfe0a7 | ||
|
|
b1a740223c | ||
|
|
41c98d97b1 | ||
|
|
fb06babb96 | ||
|
|
ccc0e7696b | ||
|
|
6d41ed8011 | ||
|
|
c1476a7d58 | ||
|
|
b1ee39be3b | ||
|
|
c2ccdd3d05 | ||
|
|
a621f1dfb4 | ||
|
|
8527ad0007 | ||
|
|
377ecef1b4 | ||
|
|
1f9921e6ff | ||
|
|
00988ed04d | ||
|
|
3d1113c057 | ||
|
|
1c81f0c1e1 | ||
|
|
b26043ef5e | ||
|
|
e3ad144c06 | ||
|
|
fdb45c48ce | ||
|
|
8619cfa819 | ||
|
|
1fe4cdd648 | ||
|
|
2289e58dda | ||
|
|
049bcfbaf5 | ||
|
|
5b5f922b54 | ||
|
|
6545498c21 | ||
|
|
ce22197ec9 | ||
|
|
f3d2dfe386 | ||
|
|
677790af9c | ||
|
|
85941349d8 | ||
|
|
db755e0f90 | ||
|
|
1019a01249 | ||
|
|
35da44bc64 | ||
|
|
b7ce07f965 | ||
|
|
f2c15c5c8e | ||
|
|
8f0a6b7635 | ||
|
|
491c42d68b | ||
|
|
a883857230 | ||
|
|
6468d0edcc | ||
|
|
48eeeb4f00 | ||
|
|
0b25f25669 | ||
|
|
d76b7375cb | ||
|
|
f76aa51b54 | ||
|
|
9e19a293d4 | ||
|
|
381758550c | ||
|
|
da496f7c53 | ||
|
|
f905aa7dfd | ||
|
|
0db27c773b | ||
|
|
4e968826ba | ||
|
|
e95f575530 | ||
|
|
3943caeedb | ||
|
|
4495051813 | ||
|
|
558d23b8c0 | ||
|
|
2e90d1fb18 | ||
|
|
9dae24ace6 | ||
|
|
b6e6568500 | ||
|
|
a8eae7f04b | ||
|
|
4902e0ef06 | ||
|
|
e6926a714f | ||
|
|
c9eee4331d | ||
|
|
9a8cc2e5c3 | ||
|
|
a7c9cd0f27 | ||
|
|
efc6846c1c | ||
|
|
89edddd38c | ||
|
|
e7c2112f41 | ||
|
|
d8cd3b6c92 | ||
|
|
bb1dd4ecfd | ||
|
|
3824b386d7 | ||
|
|
a9159fc8a0 | ||
|
|
7f4cf6e681 | ||
|
|
215f05fbc2 | ||
|
|
6d6afe9e69 | ||
|
|
66122f1608 | ||
|
|
0ad9bbdd8e | ||
|
|
59cc08f28a | ||
|
|
afc0c3a2fa | ||
|
|
a827046acc | ||
|
|
fd8f967036 | ||
|
|
26be524c6d | ||
|
|
8ec3360575 | ||
|
|
f66753498b | ||
|
|
90e6e5a3a3 | ||
|
|
2c4713ff25 | ||
|
|
ef3b01ac56 | ||
|
|
1cfb258dcc | ||
|
|
d13464a392 | ||
|
|
f1dd50faee | ||
|
|
6f5315db27 | ||
|
|
02922c3c8e | ||
|
|
03ef649c0f | ||
|
|
9083e28637 | ||
|
|
cfa4c680cf | ||
|
|
a5b3b9d1d0 | ||
|
|
13cd7e76c1 | ||
|
|
6e7ccfba37 | ||
|
|
34fd673e25 | ||
|
|
db484f7b28 | ||
|
|
b4cf0f5d29 | ||
|
|
4db205dd9a | ||
|
|
64f8e2a57b | ||
|
|
eddc245ff4 | ||
|
|
9717db0ca4 | ||
|
|
ed2f0f2c0c | ||
|
|
7d5a422855 | ||
|
|
e420862546 | ||
|
|
2cc59948a7 | ||
|
|
188d33199e | ||
|
|
24928261bb | ||
|
|
4c7bda8343 | ||
|
|
d88709c999 | ||
|
|
29e1481a3b | ||
|
|
9d76359543 | ||
|
|
f50e791221 | ||
|
|
7d08e5225c | ||
|
|
3842c1e2df | ||
|
|
21b1990991 | ||
|
|
e278c9fb5a | ||
|
|
bfa29bb7c2 | ||
|
|
efa55a9696 | ||
|
|
8888cd6264 | ||
|
|
34e2c014e3 | ||
|
|
5d92a6d013 | ||
|
|
fb03b2e95c | ||
|
|
5a1e09564c | ||
|
|
3783eba401 | ||
|
|
a3530ce6bb | ||
|
|
27d8655d58 | ||
|
|
fb845ce601 | ||
|
|
dbd433df9d | ||
|
|
badf32ff11 | ||
|
|
5e40f3264e | ||
|
|
71bb6d9410 | ||
|
|
731b682fc9 | ||
|
|
c9751227a5 | ||
|
|
6ef2a26e94 | ||
|
|
5a8efa2388 | ||
|
|
d25895fa28 | ||
|
|
de09f3ece2 | ||
|
|
1135ffb30f | ||
|
|
5379cb31e8 | ||
|
|
7d41ab44b7 | ||
|
|
b239f9bd05 | ||
|
|
1acc923ec2 | ||
|
|
8d6edfb91f | ||
|
|
491b62bb0d | ||
|
|
d1413fc478 | ||
|
|
761f7039e9 | ||
|
|
59783a341d | ||
|
|
0100fa4236 | ||
|
|
f6f9ed68dc | ||
|
|
8adb5e84d1 | ||
|
|
3940b65156 | ||
|
|
98e7f27eab | ||
|
|
7cf5cafd75 | ||
|
|
d750e35273 | ||
|
|
1f12e12130 | ||
|
|
4a4fb4716e | ||
|
|
efe6ec0d43 | ||
|
|
00d605e195 | ||
|
|
f06de8d19f | ||
|
|
fac335c849 | ||
|
|
5bef546ae4 | ||
|
|
b690cd6a67 | ||
|
|
dbee4a2657 | ||
|
|
67420f293b | ||
|
|
d1d6b0792b | ||
|
|
2ec039ba0f | ||
|
|
4b9b9a10ad | ||
|
|
4193d8ada1 | ||
|
|
4701b090b0 | ||
|
|
8b0c85827c | ||
|
|
57e02d1c80 | ||
|
|
4446929b10 | ||
|
|
c31f65b0e4 | ||
|
|
f6c3b8dceb | ||
|
|
10bd825972 | ||
|
|
303bee7c57 | ||
|
|
369ae43062 | ||
|
|
62dc7fd88b | ||
|
|
4aace63cd7 | ||
|
|
5d588a05a7 | ||
|
|
cab6f9d026 | ||
|
|
fc94d553eb | ||
|
|
2952345585 | ||
|
|
becc3828b7 | ||
|
|
2a62248991 | ||
|
|
eb2b31219c | ||
|
|
55053f9627 | ||
|
|
5ff6c64938 | ||
|
|
c1c517cf38 | ||
|
|
b3de2e5d6b | ||
|
|
0183070b2b | ||
|
|
089bbc3ee9 | ||
|
|
b085801c2c | ||
|
|
ad4cae4288 | ||
|
|
95c285e1f7 | ||
|
|
c31a1912a5 | ||
|
|
99da1f8481 | ||
|
|
bf6b3a7da0 | ||
|
|
4ab4f58f65 | ||
|
|
29a76ac8e6 | ||
|
|
e2761ce284 | ||
|
|
dc45334cb9 | ||
|
|
56052bfd79 | ||
|
|
f5067d636a | ||
|
|
b173b1300a | ||
|
|
15a34e53bf | ||
|
|
183fb26988 | ||
|
|
2da9a8cd59 | ||
|
|
bb07000280 | ||
|
|
3a46ad2957 | ||
|
|
6b631d119c | ||
|
|
051762fcc1 | ||
|
|
595d5dc283 | ||
|
|
1d328f07e2 | ||
|
|
b1c8ac6f2a | ||
|
|
da619d37f7 | ||
|
|
236c4722e5 | ||
|
|
92f59116a0 | ||
|
|
c28040f0cb | ||
|
|
e8b9654320 | ||
|
|
be1a793a37 | ||
|
|
440720afb6 | ||
|
|
900d3e734e | ||
|
|
0bf096abb5 | ||
|
|
632efb9e22 | ||
|
|
bfc506e450 | ||
|
|
ffe612708c | ||
|
|
5d161160aa | ||
|
|
eadf3e5a29 | ||
|
|
6b6b4084a0 | ||
|
|
30a4633f72 | ||
|
|
d44139dd72 | ||
|
|
60869a0490 | ||
|
|
102be5fa2e | ||
|
|
a25c04b390 | ||
|
|
d46f0a1c33 | ||
|
|
5c7f5a99e1 | ||
|
|
d949a80f19 | ||
|
|
6b5d4e2027 | ||
|
|
199d071ee8 | ||
|
|
5b78de5d61 | ||
|
|
db4f13e2e1 | ||
|
|
5e869b7e03 | ||
|
|
34fee36132 | ||
|
|
ea09bc36a5 | ||
|
|
f7e0678cb2 | ||
|
|
223ae378a9 | ||
|
|
6f52a48991 | ||
|
|
0280935955 | ||
|
|
eebc7d9ab3 | ||
|
|
aa7b5652ff | ||
|
|
ec6683a198 | ||
|
|
05372924a0 | ||
|
|
e63c1399dd | ||
|
|
4fcad92e1d | ||
|
|
e18e7298bc | ||
|
|
76a6a509cd | ||
|
|
7a03561ff6 | ||
|
|
70e54ce14a | ||
|
|
b5f86f0e79 | ||
|
|
4ebca49ef7 | ||
|
|
49cfa95463 | ||
|
|
4600ab0593 | ||
|
|
7f2ca98bde | ||
|
|
6511485bcf | ||
|
|
fd22113e44 | ||
|
|
f2ae078723 | ||
|
|
c3dacf2b9b | ||
|
|
e8580dc1d5 | ||
|
|
daffff7eb0 | ||
|
|
751fda5e37 | ||
|
|
1e38abc500 | ||
|
|
8e7137815b | ||
|
|
4226ee453f | ||
|
|
568a110443 | ||
|
|
7fd7a6ca8d | ||
|
|
3092c854ff | ||
|
|
a9566f4b23 | ||
|
|
7a5aa3c11d | ||
|
|
f2454bb028 | ||
|
|
2c1023a6fd | ||
|
|
9187ac6684 | ||
|
|
9640e43bc5 | ||
|
|
09b24b36f2 | ||
|
|
06a761c0d0 | ||
|
|
6ff7d7fcd7 | ||
|
|
0f27c79303 | ||
|
|
9961ad081f | ||
|
|
02056b4f78 | ||
|
|
0492588c25 | ||
|
|
ad8463ac8b | ||
|
|
7843de240a | ||
|
|
0037a08017 | ||
|
|
92ba432992 | ||
|
|
3a521f4014 | ||
|
|
e8bbc9799c | ||
|
|
96a48b4813 | ||
|
|
355450e82d | ||
|
|
99307c5b0c | ||
|
|
ace8be6514 | ||
|
|
1b538931b1 | ||
|
|
7ccd08ee2b | ||
|
|
104beed594 | ||
|
|
e8df594edb | ||
|
|
6e922a84f8 | ||
|
|
b74bf86277 | ||
|
|
6cbe50c9ef | ||
|
|
14fcca040b | ||
|
|
7bb545cff9 | ||
|
|
f52b1a246c | ||
|
|
273dfd89cc | ||
|
|
52d72d287b | ||
|
|
e8d8fcbe69 | ||
|
|
30c6c1afd3 | ||
|
|
c8c8093b3a | ||
|
|
c970b2cf2e | ||
|
|
0815283d31 | ||
|
|
55ee2d93b8 | ||
|
|
c9e17ed42b | ||
|
|
76a91b7fe0 | ||
|
|
f9b3169b6a | ||
|
|
4f42b5a3ee | ||
|
|
5d9645eaff | ||
|
|
c032808d82 | ||
|
|
282c94266e | ||
|
|
e2f7d52bee | ||
|
|
6317f0162a | ||
|
|
436a6310d7 | ||
|
|
0f74195255 | ||
|
|
5ba33786ab | ||
|
|
70fcfc1753 | ||
|
|
1e34048c0c | ||
|
|
45b1b23262 | ||
|
|
07842ace4e | ||
|
|
8e75280093 | ||
|
|
029b9ef498 | ||
|
|
51970abce7 | ||
|
|
f6faa19e5f | ||
|
|
dd7217ca54 | ||
|
|
6e13457eb2 | ||
|
|
3b319ee19b | ||
|
|
8ea8daa811 | ||
|
|
eb29b908c2 | ||
|
|
9935cb482e | ||
|
|
0d6e7181cf | ||
|
|
d225650e15 | ||
|
|
bfe09791d5 | ||
|
|
85d103f3f6 | ||
|
|
9e6dbe2465 | ||
|
|
355e5ccda6 | ||
|
|
a5f6f05e10 | ||
|
|
db0d35d80b | ||
|
|
6fca2a3931 | ||
|
|
0305a42b02 | ||
|
|
77d81716ed | ||
|
|
1fc8ee6fee | ||
|
|
3e54e80eb6 | ||
|
|
3c9bb63d32 | ||
|
|
617ab9efab | ||
|
|
bc574097e2 | ||
|
|
4cc64dc233 | ||
|
|
45fa0a165a | ||
|
|
0e5c07a078 | ||
|
|
601a742c71 | ||
|
|
c972401b6e | ||
|
|
024e81cf01 | ||
|
|
975ed402d5 | ||
|
|
b9e8083744 | ||
|
|
bb859708bc | ||
|
|
3cf2c221ac | ||
|
|
6edd7edcd2 | ||
|
|
4e26a02d78 | ||
|
|
bb36a57053 | ||
|
|
b291c800f1 | ||
|
|
b63a798d86 | ||
|
|
eacf3777a4 | ||
|
|
a5a7436bb1 | ||
|
|
2a4cc35df7 | ||
|
|
cdccf58b76 | ||
|
|
27300383a1 | ||
|
|
375b9bce30 | ||
|
|
b6b8db48df | ||
|
|
36e6c267b9 | ||
|
|
13066b3b4a | ||
|
|
ccbe9d00c8 | ||
|
|
ce291582cb | ||
|
|
bb37bc3b51 | ||
|
|
5a7747acd1 | ||
|
|
1bc2ec9461 | ||
|
|
2b977fc2b0 | ||
|
|
de60c4ee9e | ||
|
|
de67e40c00 | ||
|
|
cc4b016c64 | ||
|
|
f64f561d6f | ||
|
|
80bddf8a6b | ||
|
|
cbaaec961c | ||
|
|
5477b3f936 | ||
|
|
fd59a93ede | ||
|
|
cd316b7138 | ||
|
|
d1955192ed | ||
|
|
9beb839bf4 | ||
|
|
29ea303093 | ||
|
|
feff6751ca | ||
|
|
ca33fdf752 | ||
|
|
fdcdfe89d6 | ||
|
|
48ed2115a7 | ||
|
|
65988f4e08 | ||
|
|
ede65eda6c | ||
|
|
5da56acac8 | ||
|
|
5720c55301 | ||
|
|
ffefa4b30e | ||
|
|
80e4986b23 | ||
|
|
dc92ccda0a | ||
|
|
f9bab18076 | ||
|
|
2dec52e221 | ||
|
|
7413f693d7 | ||
|
|
415460df75 | ||
|
|
125a194468 | ||
|
|
32e9afbf36 | ||
|
|
3eca704f4a | ||
|
|
9c95129311 | ||
|
|
bf34c1bcdb | ||
|
|
284c687d77 | ||
|
|
09afb5a3f5 | ||
|
|
0138721451 | ||
|
|
2d5f610941 | ||
|
|
864fa7762b | ||
|
|
4fde38ee6a | ||
|
|
6cdf0f10d4 | ||
|
|
b66592c25f | ||
|
|
43616c566d | ||
|
|
62f1dc17a0 | ||
|
|
0e9a8a937a | ||
|
|
9a86b245ce | ||
|
|
64533f7a3f | ||
|
|
0b7de8d387 | ||
|
|
8eba4860fe | ||
|
|
b53e2f57e6 | ||
|
|
e1e834297b | ||
|
|
e37dc6e341 | ||
|
|
c91c896854 | ||
|
|
7e5dfa03d6 | ||
|
|
1a4ec3f049 | ||
|
|
756763fcbe | ||
|
|
93036c4e67 | ||
|
|
15bf972ef6 | ||
|
|
bcb4567382 | ||
|
|
3890c4ffb9 | ||
|
|
8dd8f471a2 | ||
|
|
657d1cd042 | ||
|
|
c8d82f24a8 | ||
|
|
367e740a9c | ||
|
|
5b5a51bdad | ||
|
|
7fc030a53e | ||
|
|
65087fb5f8 | ||
|
|
d5e789af3c | ||
|
|
45212c699f | ||
|
|
03106f3522 | ||
|
|
cd2857cb94 | ||
|
|
b3ddb8658f | ||
|
|
7ae80b6b49 | ||
|
|
34f27f9cd4 | ||
|
|
4c53b58fd8 | ||
|
|
6102ab4be8 | ||
|
|
6fcf565974 | ||
|
|
77bd16c7cf | ||
|
|
7d593080ec | ||
|
|
96ea74abeb | ||
|
|
e50e3957ec | ||
|
|
8cb9420939 | ||
|
|
bbb7b8caa2 | ||
|
|
cc284f3c9e | ||
|
|
bd8aae5fbf | ||
|
|
3d162f33df | ||
|
|
ff3c37f1b4 | ||
|
|
35160ce385 | ||
|
|
c8eef91813 | ||
|
|
6791b20121 | ||
|
|
3311c49236 | ||
|
|
5223e58f83 | ||
|
|
e0f9fc233d | ||
|
|
509bd21655 | ||
|
|
5be7229c4b | ||
|
|
efd3c461e0 | ||
|
|
13aed69f0e | ||
|
|
b03675811c | ||
|
|
e4beac185e | ||
|
|
bdc7d9dd84 | ||
|
|
b49d1e0529 | ||
|
|
3c0eb48d53 | ||
|
|
0d65531ec1 | ||
|
|
3848f9822b | ||
|
|
e1cdc002b9 | ||
|
|
168c1cf1ce | ||
|
|
5b142b4401 | ||
|
|
9be3d76590 | ||
|
|
26971aa109 | ||
|
|
18f9049bc6 | ||
|
|
53cf253b67 | ||
|
|
6d416f45a9 | ||
|
|
a34a447164 | ||
|
|
d3247a9ec1 | ||
|
|
dfb5a2b97f | ||
|
|
deae96e191 | ||
|
|
b8c0e18bb4 | ||
|
|
a083da1546 | ||
|
|
2518c70ec8 | ||
|
|
6ead8d261c | ||
|
|
b5f76a6ebc | ||
|
|
5e38bca535 | ||
|
|
c39142ee77 | ||
|
|
d0174479d7 | ||
|
|
aa23267d70 | ||
|
|
92f5da306d | ||
|
|
8601e2af7a | ||
|
|
5d9f31b99e | ||
|
|
8b29158d8b | ||
|
|
b68cf3c671 | ||
|
|
858a50a414 | ||
|
|
598df624df | ||
|
|
e6da2b73ed | ||
|
|
f44424d37a | ||
|
|
623804ae68 | ||
|
|
b7544939c2 | ||
|
|
63fa6d4660 | ||
|
|
9506c11951 | ||
|
|
ce5639bb98 | ||
|
|
99ef6ca861 | ||
|
|
43d5a3da27 | ||
|
|
30723110af | ||
|
|
a0ecb4e2bb | ||
|
|
a726b09f26 | ||
|
|
52b1d1fe33 | ||
|
|
8b4709c132 | ||
|
|
b4217a791e | ||
|
|
9b3a982d54 | ||
|
|
0a47d3eab5 | ||
|
|
7d7803247d | ||
|
|
aa8d45e636 | ||
|
|
7f3e87767c | ||
|
|
f76ea9bba5 | ||
|
|
1eefeae060 | ||
|
|
03447c33be | ||
|
|
f6c6cd5cb8 | ||
|
|
67b56cee50 | ||
|
|
2d7465cca6 | ||
|
|
d06f20690d | ||
|
|
e613a494ff | ||
|
|
503a01675e | ||
|
|
723f7e63e9 | ||
|
|
7ff6916e5f | ||
|
|
910ed26525 | ||
|
|
c0fffa6293 | ||
|
|
d800d3ba41 | ||
|
|
d267343c64 | ||
|
|
0f72e37272 | ||
|
|
89e44d581f | ||
|
|
c908a710de | ||
|
|
f0809cde72 | ||
|
|
e6da8c1c18 | ||
|
|
81ea67afa8 | ||
|
|
488049821e | ||
|
|
fc7408a8f8 | ||
|
|
6955191fdd | ||
|
|
ba59693d90 | ||
|
|
ce6e51128d | ||
|
|
a7a26037ae | ||
|
|
00854abd1a | ||
|
|
c26649d5f2 | ||
|
|
63b76a249f | ||
|
|
95f19208ad | ||
|
|
f4a902ee47 | ||
|
|
036823cbb6 | ||
|
|
547c2c8c0d | ||
|
|
7453d19d27 | ||
|
|
2a0b68d637 | ||
|
|
02f374fdf0 | ||
|
|
3536dcd775 | ||
|
|
6fa887e536 | ||
|
|
71755763b9 | ||
|
|
5aabcccfd4 | ||
|
|
1e009e2211 | ||
|
|
811bc6434b | ||
|
|
a5b2d06742 | ||
|
|
2c00674835 | ||
|
|
9bbff1f436 | ||
|
|
c75416f9be | ||
|
|
2cb750a7b0 | ||
|
|
abfec51964 | ||
|
|
e314717a3e | ||
|
|
290a78acb5 | ||
|
|
b40a4dd243 | ||
|
|
b38f6d0581 | ||
|
|
5c562a6bdb | ||
|
|
238d43f916 | ||
|
|
818dc23089 | ||
|
|
ddb50940ce | ||
|
|
96fb97f4ef | ||
|
|
6c0459c2ae | ||
|
|
30a9e1ee4b | ||
|
|
15a1f1e6e0 | ||
|
|
827e2b161c | ||
|
|
e95230859e | ||
|
|
95cb8ca493 | ||
|
|
5735293efb | ||
|
|
035df1f338 | ||
|
|
36a8b04c4f | ||
|
|
3d5411ef28 | ||
|
|
df7b74e727 | ||
|
|
0138599120 | ||
|
|
66446cbffc | ||
|
|
ef40f3900f | ||
|
|
ce259764a8 | ||
|
|
65187cb17b | ||
|
|
8f25245ad1 | ||
|
|
58b8baa019 | ||
|
|
ba1db88c8d | ||
|
|
c8f2f42a35 | ||
|
|
4e5a9b2af5 | ||
|
|
d1ab961c14 | ||
|
|
33caf4644e | ||
|
|
f34ffe18a8 | ||
|
|
4b1c5e111d | ||
|
|
dbd7c296c6 | ||
|
|
df93cbde8f | ||
|
|
d8cbc121a7 | ||
|
|
8163cd5c8f | ||
|
|
e9e9adb174 | ||
|
|
21fdfc1eef | ||
|
|
4304251e1e | ||
|
|
ae0cd74b0e | ||
|
|
6935bff244 | ||
|
|
102b9da617 | ||
|
|
8bd0d185ae | ||
|
|
7bfc5ae0a8 | ||
|
|
c72a41db9d | ||
|
|
5fbb1c2e32 | ||
|
|
850dd53c69 | ||
|
|
e3baf91037 | ||
|
|
4c811c9c04 | ||
|
|
4bc9f5826e | ||
|
|
9f19e4dc83 | ||
|
|
c9cb865c9c | ||
|
|
b8a7dd984d | ||
|
|
3ede46c445 | ||
|
|
87c4af94d3 | ||
|
|
6c30bd36d1 | ||
|
|
7e85ccaf28 | ||
|
|
db3a262410 | ||
|
|
70162da491 | ||
|
|
f3632a4033 | ||
|
|
2f4e176116 | ||
|
|
3ea8dc598c | ||
|
|
3f63dc0180 | ||
|
|
1f6701b183 | ||
|
|
3a170fbf90 | ||
|
|
b57e523a07 | ||
|
|
8c4509f255 | ||
|
|
3878c287be | ||
|
|
7789e3f3a7 | ||
|
|
94ac744f41 | ||
|
|
a83211f260 | ||
|
|
4f733affc5 | ||
|
|
ffb2a982bc | ||
|
|
9658a65661 | ||
|
|
68d4083708 | ||
|
|
af5377d2eb | ||
|
|
6ac8ad543c | ||
|
|
c10a63e76e | ||
|
|
b423a2af52 | ||
|
|
9b70289345 | ||
|
|
2a94c1f33a | ||
|
|
ea205c1acb | ||
|
|
6e1767730b | ||
|
|
3fc1ab37d7 | ||
|
|
c4a3f7333d | ||
|
|
fb820e01b1 | ||
|
|
504aca5287 | ||
|
|
ae7d5546ae | ||
|
|
8bcb10b281 | ||
|
|
845eaab73e | ||
|
|
298ff190ec | ||
|
|
734a548bc3 | ||
|
|
591973d97f | ||
|
|
a2f57ecd66 | ||
|
|
13dcfb479d | ||
|
|
3328d80130 | ||
|
|
f9df3ac0e2 | ||
|
|
61d66e80d8 | ||
|
|
93e0360116 | ||
|
|
2cec5f7934 | ||
|
|
8d4a09b007 | ||
|
|
6d8d2fa99d | ||
|
|
59ecd55530 | ||
|
|
77fcc89e37 | ||
|
|
7d63da4d9c | ||
|
|
8eea0bae99 | ||
|
|
f5dcd8f776 | ||
|
|
e1f2baeeda | ||
|
|
84d8ff8b0b | ||
|
|
9862c22fdb | ||
|
|
8c82f53dd2 | ||
|
|
5cf4c21ce3 | ||
|
|
4951f62d4a | ||
|
|
4c0c4b7010 | ||
|
|
96051e614c | ||
|
|
a2e494b2ba | ||
|
|
2d5a9bec84 | ||
|
|
7d9571cfdd | ||
|
|
acc6a780fa | ||
|
|
0e36d42a06 | ||
|
|
5cfddd807a | ||
|
|
17b5604f2a | ||
|
|
fdb6eed6d0 | ||
|
|
13de0437b8 | ||
|
|
a43c45f310 | ||
|
|
9c56a4d1fc | ||
|
|
2aea1d2631 | ||
|
|
118e05a0fa | ||
|
|
05f497787e | ||
|
|
53cf4d0234 | ||
|
|
6e735b1763 | ||
|
|
873a1a0683 | ||
|
|
27b1149d1c | ||
|
|
c1e64f111e | ||
|
|
b2af8448fd | ||
|
|
8f44338e76 | ||
|
|
e4a55abcb3 | ||
|
|
d7348b4973 | ||
|
|
09421230f2 | ||
|
|
4596e32258 | ||
|
|
4dc007a4f6 | ||
|
|
c53a156969 | ||
|
|
94bd0774fb | ||
|
|
b178b1d780 | ||
|
|
cbc840378c | ||
|
|
aa7989c16d | ||
|
|
85f9336804 | ||
|
|
d66be3ecda | ||
|
|
c08e05bd6c | ||
|
|
e08ab89fd4 | ||
|
|
8ba0a0a776 | ||
|
|
73ed0cea88 | ||
|
|
58a378d212 | ||
|
|
553307ba70 | ||
|
|
78f73a9049 | ||
|
|
abc69e7a0e | ||
|
|
503914dbce | ||
|
|
69680f8680 | ||
|
|
1a616aacb7 | ||
|
|
52d19f267e | ||
|
|
75a8dc9ee4 | ||
|
|
8c63920ef6 | ||
|
|
dda5619cdc | ||
|
|
9cc25fd931 | ||
|
|
f5a9eb4295 | ||
|
|
ebe256e482 | ||
|
|
e215585a64 | ||
|
|
8b01fc03ec | ||
|
|
9af2c8370d | ||
|
|
01f9003b8d | ||
|
|
73a3c7eb71 | ||
|
|
370ec1432e | ||
|
|
4b63c124bb | ||
|
|
3d7df604b1 | ||
|
|
59d1d275bc | ||
|
|
f7f0332b13 | ||
|
|
917c5f5083 | ||
|
|
370cf05615 | ||
|
|
101d892c7f | ||
|
|
db2faf6511 | ||
|
|
3a83d1bf79 | ||
|
|
070e309aa6 | ||
|
|
11ca7c3f7d | ||
|
|
7a0acaefb3 | ||
|
|
486f3664ef | ||
|
|
387b165314 | ||
|
|
4ba3f7996e | ||
|
|
7ce41dfb67 | ||
|
|
efd55c8d36 | ||
|
|
71543c64d4 | ||
|
|
408c1a541f | ||
|
|
8f739d9e0b | ||
|
|
bd8f608ee6 | ||
|
|
551643b11c | ||
|
|
075ecae540 | ||
|
|
dcb8807fa7 | ||
|
|
77ec2df31c | ||
|
|
d92e812554 | ||
|
|
0a6e1f9cc6 | ||
|
|
f3c3680382 | ||
|
|
f7e783a556 | ||
|
|
fd1b72563d | ||
|
|
5a94ef82dd | ||
|
|
3ad46b7e21 | ||
|
|
1e94d71665 | ||
|
|
0899a576ff | ||
|
|
db2476f3a5 | ||
|
|
8ba182870d | ||
|
|
d81085a9e8 | ||
|
|
ae92a459b7 | ||
|
|
e101c434d5 | ||
|
|
9686d0f0ca | ||
|
|
1dd0392b78 | ||
|
|
5e184d733b | ||
|
|
ef0b20887b | ||
|
|
5cb4bd998b | ||
|
|
57f499c6a5 | ||
|
|
c2ae6e19f9 | ||
|
|
16104fde03 | ||
|
|
13e10692b1 | ||
|
|
7fc93f23c0 | ||
|
|
d6ddf87365 | ||
|
|
05172744cd | ||
|
|
2504ae24fc | ||
|
|
5e41d5762b | ||
|
|
57d44c972f | ||
|
|
be0b57ac00 | ||
|
|
862bb2b276 | ||
|
|
5eac851f80 | ||
|
|
0deab8296f | ||
|
|
6785708661 | ||
|
|
5307fd0901 | ||
|
|
788c4c7917 | ||
|
|
3f6d96289d | ||
|
|
5cfd80e510 | ||
|
|
9eb32ae55a | ||
|
|
82e08150ab | ||
|
|
e956ee9254 | ||
|
|
bb4f34317b | ||
|
|
d08ff3fbae | ||
|
|
f5479ec82f | ||
|
|
3eb25c4060 | ||
|
|
03bb1ad9a6 | ||
|
|
b59d3e77f9 | ||
|
|
7c41692d5f | ||
|
|
ce89d456e1 | ||
|
|
a0c48fcca6 | ||
|
|
d0cba6d6bc | ||
|
|
6baf6ccf4b | ||
|
|
a2571e61a8 | ||
|
|
f61824ff0d | ||
|
|
734a1aeb01 | ||
|
|
5269aad90d | ||
|
|
eaeee188eb | ||
|
|
8c850e06ca | ||
|
|
0f754a8da8 | ||
|
|
81c4d4f393 | ||
|
|
5317aee18d | ||
|
|
b66eeba3b5 | ||
|
|
522ff1ddf7 | ||
|
|
7ef9c43ee3 | ||
|
|
05ac76e3e9 | ||
|
|
4838402797 | ||
|
|
0de12c7c07 | ||
|
|
0dadd8bffe | ||
|
|
75a0cb1189 | ||
|
|
a52d936b4d | ||
|
|
cfd04dc602 | ||
|
|
7622e63975 | ||
|
|
3e7172b9d1 | ||
|
|
7aa0294447 | ||
|
|
5b89c4d293 | ||
|
|
3bb991f57f | ||
|
|
1dd3aa0e8a | ||
|
|
189cecbc37 | ||
|
|
9680ba08e9 | ||
|
|
dcf5bd37ca | ||
|
|
d6a4b0a96d | ||
|
|
5f8638d349 | ||
|
|
1de274df39 | ||
|
|
3df801c54e | ||
|
|
d473cb49c5 | ||
|
|
2a5118a2cf | ||
|
|
347c56da55 | ||
|
|
8073723e1f | ||
|
|
c8e90a755d | ||
|
|
aeb0a5d8ea | ||
|
|
09fe7f3ecd | ||
|
|
a549411589 | ||
|
|
f426b5e118 | ||
|
|
12a8ef5e31 | ||
|
|
8210279a4c | ||
|
|
999d13b80d | ||
|
|
0573c0b270 | ||
|
|
5a36a7980c | ||
|
|
6bf9b5297f | ||
|
|
fdc6b0ff00 | ||
|
|
73c20a53d2 | ||
|
|
16ae8d8e4d | ||
|
|
3f8cfae64e | ||
|
|
6861a71efb | ||
|
|
2a603f410f | ||
|
|
6d22788f92 | ||
|
|
e36e6bbaca | ||
|
|
ac3a13ed73 | ||
|
|
202f7df5cb | ||
|
|
1b902b1a18 | ||
|
|
1765d399c8 | ||
|
|
5dd79707f1 | ||
|
|
14755134bf | ||
|
|
cccde7e135 | ||
|
|
3dafd643c0 | ||
|
|
76f8cd4102 | ||
|
|
a8bfb03f58 | ||
|
|
10ead4f3e0 | ||
|
|
af5a6857ba | ||
|
|
d9225fd639 | ||
|
|
995d44551c | ||
|
|
68c0224b98 | ||
|
|
7767468d5d | ||
|
|
f9f83d8243 | ||
|
|
cc4cec829f | ||
|
|
f665bf1eb2 | ||
|
|
bccd52054e | ||
|
|
3a4c782aab | ||
|
|
6e047e96fa | ||
|
|
42f8753166 | ||
|
|
b6a64129b3 | ||
|
|
bb5bd23263 | ||
|
|
58361a0493 | ||
|
|
7671ab95f9 | ||
|
|
41f102c0ce | ||
|
|
5cee527d6f | ||
|
|
c2eb13d5e5 | ||
|
|
ed89e20826 | ||
|
|
e50c7554e3 | ||
|
|
00354f10a3 | ||
|
|
031d0b7444 | ||
|
|
2ced06367b | ||
|
|
9ef3279bae | ||
|
|
b055486d74 | ||
|
|
c70f2c481e | ||
|
|
e4b26be01f | ||
|
|
b27bdc63c1 | ||
|
|
543e7d8cdc | ||
|
|
852decbe7d | ||
|
|
d2150320fa | ||
|
|
9c723c7522 | ||
|
|
ac5660de88 | ||
|
|
e6467452a6 | ||
|
|
04b49c2e61 | ||
|
|
324773ba58 | ||
|
|
d885ca5db7 | ||
|
|
e093fc28b0 | ||
|
|
579bc94847 | ||
|
|
53dba06cc3 | ||
|
|
1c390a7f04 | ||
|
|
6b16857186 | ||
|
|
785bba1f45 | ||
|
|
3befcbc16d | ||
|
|
be300608a9 | ||
|
|
f05902925d | ||
|
|
d32e7acc8f | ||
|
|
fda9e6195a | ||
|
|
b5d91adb21 | ||
|
|
1b52cb6233 | ||
|
|
5669634b44 | ||
|
|
d6bb365cf1 | ||
|
|
53b7cfb9bf | ||
|
|
2558c851c0 | ||
|
|
a8aa63daa3 | ||
|
|
d587e9f708 | ||
|
|
e2a8f40e3e | ||
|
|
1cc4241fac | ||
|
|
bb98dfe0b3 | ||
|
|
6395dd820b | ||
|
|
e909c03356 | ||
|
|
6a3278432d | ||
|
|
81c51970aa | ||
|
|
7f9d431b9b | ||
|
|
360591ebb9 | ||
|
|
814ff2c2e5 | ||
|
|
f74df57400 | ||
|
|
6b29a0f0f3 | ||
|
|
2a58ee8729 | ||
|
|
41d9edcf83 | ||
|
|
3ec33a1c77 | ||
|
|
3f4c6a1b76 | ||
|
|
60ad3199ca | ||
|
|
1984ffb1c0 | ||
|
|
7877a14f07 | ||
|
|
568a94968b | ||
|
|
a6a68af24e | ||
|
|
5cdd110544 | ||
|
|
5bbe306f8f | ||
|
|
48acbc563a | ||
|
|
ab1f7e7179 | ||
|
|
667e909a70 | ||
|
|
bcc0d280ab | ||
|
|
da027b7734 | ||
|
|
09056665c2 | ||
|
|
f4c3951f6d | ||
|
|
00f42bb881 | ||
|
|
d8408a26c2 | ||
|
|
ede54531c8 | ||
|
|
0fa0a4b19a | ||
|
|
af5ea3db0f | ||
|
|
e75ca05aa4 | ||
|
|
3483190b78 | ||
|
|
69656e65c3 | ||
|
|
e6c6841fac | ||
|
|
16d63a4416 | ||
|
|
2eaf443359 | ||
|
|
5979957d60 | ||
|
|
049eb5914c | ||
|
|
7577f3ac9b | ||
|
|
5b05745b02 | ||
|
|
4366b4e369 | ||
|
|
9c60f10005 | ||
|
|
a0d02043c4 | ||
|
|
ff5152bf79 | ||
|
|
995a826917 | ||
|
|
2965e159cb | ||
|
|
0dcd5163d4 | ||
|
|
d31bbd9ea8 | ||
|
|
423ba07fad | ||
|
|
3697d74185 | ||
|
|
038f089aac | ||
|
|
ba3f512445 | ||
|
|
0fc1cff0a8 | ||
|
|
40bec4b732 | ||
|
|
8ca1e09c86 | ||
|
|
e0c4ec5028 | ||
|
|
7fcba6c911 | ||
|
|
0186c00d97 | ||
|
|
9294540b5d | ||
|
|
0b16c28224 | ||
|
|
83028d405a | ||
|
|
c4262c3eaa | ||
|
|
f0e1831d30 | ||
|
|
9efd0dd46e | ||
|
|
eb26c0be0b | ||
|
|
1ff1c6ca33 | ||
|
|
6169915e63 | ||
|
|
a0d466a074 | ||
|
|
6f9ef6d51e | ||
|
|
feb4c2eabc | ||
|
|
c2cf39125d | ||
|
|
833ece1ef5 | ||
|
|
fee23cadfa | ||
|
|
4c6303f783 | ||
|
|
ce6e6c47db | ||
|
|
2ccff15525 | ||
|
|
d24f2b4e6d | ||
|
|
03fc6a530f | ||
|
|
4136734a60 | ||
|
|
ca1371260c | ||
|
|
03c2ce9a01 | ||
|
|
eda5fec37c | ||
|
|
e21c6297ff | ||
|
|
c6297ea449 | ||
|
|
e33cc00266 | ||
|
|
96c92c8df9 | ||
|
|
e256ada2dc | ||
|
|
eb0be196da | ||
|
|
1606ad3442 | ||
|
|
d212f13345 | ||
|
|
f80c9ec33e | ||
|
|
7681e23de9 | ||
|
|
22a60df40e | ||
|
|
431a01f2a5 | ||
|
|
0cd4385de7 | ||
|
|
0774b3ddc3 | ||
|
|
b60b1ed058 | ||
|
|
b196715d60 | ||
|
|
0673e90dff | ||
|
|
59cfa242fb | ||
|
|
65ab72ba90 | ||
|
|
408bca8951 | ||
|
|
480467492a | ||
|
|
219b04aedb | ||
|
|
a0709856ef | ||
|
|
577642850f | ||
|
|
e1db024034 | ||
|
|
cc268aeeda | ||
|
|
d5f7eced8b | ||
|
|
cc3c4cc79f | ||
|
|
89b61884f1 | ||
|
|
33d3fc2d8e | ||
|
|
b07f5dd6b6 | ||
|
|
b43e13bb56 | ||
|
|
3be5543df4 | ||
|
|
91ad9efa43 | ||
|
|
ee292146d1 | ||
|
|
12867634b6 | ||
|
|
2e4fe89b85 | ||
|
|
b4642e16ad | ||
|
|
8ca5964d31 | ||
|
|
30c89fe385 | ||
|
|
fb9e66332d | ||
|
|
84b512f282 | ||
|
|
6f9aa85a72 | ||
|
|
639fc20fcb | ||
|
|
75631e5c5a | ||
|
|
9ec345761e | ||
|
|
1f9c2c1699 | ||
|
|
cbec486ad1 | ||
|
|
85ef60d4a8 | ||
|
|
44bde571bf | ||
|
|
eaaee5e7cd | ||
|
|
e1980f4de2 | ||
|
|
be9c671a56 | ||
|
|
0404449842 | ||
|
|
d6366a145e | ||
|
|
0a751cf545 | ||
|
|
5885ea57ad | ||
|
|
f8b326529f | ||
|
|
9f5f1174ba | ||
|
|
779de58f74 | ||
|
|
76e316422c | ||
|
|
36273fe02d | ||
|
|
03b592521c | ||
|
|
a5ef47a060 | ||
|
|
289c801fec | ||
|
|
73d04b1564 | ||
|
|
9469d56144 | ||
|
|
d063bca474 | ||
|
|
7c592756fe | ||
|
|
08586870fb | ||
|
|
8b123acdcd | ||
|
|
08aa04f812 | ||
|
|
dd19fcf6da | ||
|
|
04b3c8ad7d | ||
|
|
81f66683d2 | ||
|
|
392554123b | ||
|
|
3e4d5c26ac | ||
|
|
86b7f6fef3 | ||
|
|
e1d914118f | ||
|
|
4a07cf9951 | ||
|
|
ce44e200a5 | ||
|
|
e8ebf53626 | ||
|
|
cdd6a4124c | ||
|
|
09c71e4e9f | ||
|
|
28a996441c | ||
|
|
396bf012c9 | ||
|
|
02da24aa75 | ||
|
|
3c6e66ce12 | ||
|
|
7987083555 | ||
|
|
e94e195e73 | ||
|
|
869ebd90df |
1
.flutter
Submodule
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something isn't working right.
|
||||
title: ''
|
||||
labels: bug, to check
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Prerequisites**
|
||||
<!-- Please ensure your request is not part of an existing issue. -->
|
||||
<!-- Please ensure you have checked the Obtainium Wiki. -->
|
||||
<!-- Please ensure your request is an actual bug and not intended behaviour (this is frequently the case for issues involving version strings and the HTML source. -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Tap on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error -->
|
||||
|
||||
**Screenshots and Logs**
|
||||
<!-- If applicable, add screenshots, logs, and any other artifacts (like some/all files under `/Android/data/dev.imranr.obtainium/`) that you think may help troubleshoot the issue. -->
|
||||
|
||||
**Please complete the following information:**
|
||||
- Device: <!-- [e.g. Pixel 7] -->
|
||||
- OS: <!-- [e.g. GrapheneOS] -->
|
||||
- Obtainium Version: <!-- [e.g. 0.14.6-beta] -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new Source, setting, or other feature.
|
||||
title: ''
|
||||
labels: enhancement, to check
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Prerequisites**
|
||||
<!-- Please ensure your request is not part of an existing issue. -->
|
||||
<!-- Please ensure you have checked the Obtainium Wiki. -->
|
||||
|
||||
**Describe the feature**
|
||||
<!-- A clear and concise description of what you want to happen.
|
||||
|
||||
For new Sources, please ensure:
|
||||
1. It is not already possible to use the HTML Source for your purposes.
|
||||
2. It must be possible to extract the following details from the Source in a reliable way:
|
||||
- The App version (or any release-specific identifier - a "pseudo-version") for the latest release
|
||||
- One or more APK URL(s) for the latest release
|
||||
- Above details for previous releases (optional)
|
||||
|
||||
If you're not sure about 1 or 2, open a discussion item instead.
|
||||
Note that the Web scraper cannot deal with JavaScript-enabled content. -->
|
||||
|
||||
**Describe alternatives you've considered (if applicable)**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
Note that app-specific Sources are less likely to be added. In those cases, see if the HTML Source will work for you (if not, see if a generally-applicable enhancement to the HTML Source would work, and suggest that instead). -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
96
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
beta:
|
||||
type: boolean
|
||||
description: Is beta?
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
|
||||
- name: Flutter Doctor
|
||||
id: flutter_doctor
|
||||
run: |
|
||||
flutter doctor -v
|
||||
|
||||
- name: Import GPG key
|
||||
id: import_pgp_key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.PGP_KEY_BASE64 }}
|
||||
passphrase: ${{ secrets.PGP_PASSPHRASE }}
|
||||
|
||||
- name: Check submodule
|
||||
id: check_submodule
|
||||
run: |
|
||||
SUBMODULE_COMMIT_LONG="$(git submodule status | head -1 | tail -c +2 | awk '{print $1}')"
|
||||
FLUTTER_COMMIT_SHORT="$(flutter --version | head -2 | tail -1 | awk '{print $4}')"
|
||||
echo "SUBMODULE_COMMIT_LONG=$SUBMODULE_COMMIT_LONG, FLUTTER_COMMIT_SHORT=$FLUTTER_COMMIT_SHORT"
|
||||
if ! [[ "$SUBMODULE_COMMIT_LONG" =~ ^$FLUTTER_COMMIT_SHORT ]]; then
|
||||
echo "Your submodule has not been updated!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Extract Version
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION=$(grep -oP "^version: [^\+]+" pubspec.yaml | tail -c +10)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
if [ ${{ inputs.beta }} == true ]; then BETA=true; else BETA=false; fi
|
||||
echo "beta=$BETA" >> $GITHUB_OUTPUT
|
||||
TAG="v$VERSION"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build APKs
|
||||
run: |
|
||||
sed -i 's/signingConfig signingConfigs.release//g' android/app/build.gradle
|
||||
flutter build apk --flavor normal && flutter build apk --split-per-abi --flavor normal
|
||||
for file in build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done
|
||||
flutter build apk --flavor fdroid -t lib/main_fdroid.dart && flutter build apk --split-per-abi --flavor fdroid -t lib/main_fdroid.dart
|
||||
rm ./build/app/outputs/flutter-apk/*.sha1
|
||||
ls -l ./build/app/outputs/flutter-apk/
|
||||
|
||||
- name: Sign APKs
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
|
||||
run: |
|
||||
echo "${KEYSTORE_BASE64}" | base64 -d > apksign.keystore
|
||||
for apk in ./build/app/outputs/flutter-apk/*-release*.apk; do
|
||||
unsignedFn=${apk/-release/-unsigned}
|
||||
mv "$apk" "$unsignedFn"
|
||||
${ANDROID_HOME}/build-tools/$(ls ${ANDROID_HOME}/build-tools/ | tail -1)/apksigner sign --ks apksign.keystore --ks-pass pass:"${KEYSTORE_PASSWORD}" --out "${apk}" "${unsignedFn}"
|
||||
sha256sum ${apk} | cut -d " " -f 1 > "$apk".sha256
|
||||
gpg --batch --pinentry-mode loopback --passphrase "${PGP_PASSPHRASE}" --sign --detach-sig "$apk".sha256
|
||||
done
|
||||
rm apksign.keystore
|
||||
PGP_KEY_FINGERPRINT="${{ steps.import_pgp_key.outputs.fingerprint }}"
|
||||
|
||||
- name: Create Tag
|
||||
uses: mathieudutour/github-tag-action@v6.1
|
||||
with:
|
||||
github_token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
custom_tag: "${{ steps.extract_version.outputs.tag }}"
|
||||
tag_prefix: ""
|
||||
|
||||
- name: Create Release And Upload APKs
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
tag: "${{ steps.extract_version.outputs.tag }}"
|
||||
prerelease: "${{ steps.extract_version.outputs.beta }}"
|
||||
artifacts: ./build/app/outputs/flutter-apk/*-release*.apk*
|
||||
generateReleaseNotes: true
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule ".flutter"]
|
||||
path = .flutter
|
||||
url = https://github.com/flutter/flutter/
|
||||
58
README.md
@@ -1,29 +1,42 @@
|
||||
#  Obtainium
|
||||
|
||||
[](https://techforpalestine.org/learn-more)
|
||||
|
||||
Get Android App Updates Directly From the Source.
|
||||
|
||||
Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available.
|
||||
Obtainium allows you to install and update apps directly from their releases pages, and receive notifications when new releases are made available.
|
||||
|
||||
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
|
||||
More info:
|
||||
- [Obtainium/wiki](https://github.com/ImranR98/Obtainium/wiki)
|
||||
- [AppVerifier](https://github.com/soupslurpr/AppVerifier) - App verification tool (recommended, integrates with Obtainium)
|
||||
- [apps.obtainium.imranr.dev](https://apps.obtainium.imranr.dev/) - Crowdsourced app configurations
|
||||
- [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) - Original motivation for this app
|
||||
|
||||
Currently supported App sources:
|
||||
- [GitHub](https://github.com/)
|
||||
- [GitLab](https://gitlab.com/)
|
||||
- [Codeberg](https://codeberg.org/)
|
||||
- [F-Droid](https://f-droid.org/)
|
||||
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||
- [Mullvad](https://mullvad.net/en/)
|
||||
- [Signal](https://signal.org/)
|
||||
- [SourceForge](https://sourceforge.net/)
|
||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||
- Third Party F-Droid Repos
|
||||
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
|
||||
- [Steam](https://store.steampowered.com/mobile)
|
||||
- [Telegram App](https://telegram.org)
|
||||
- [VLC](https://www.videolan.org/vlc/download-android.html)
|
||||
- [Neutron Code](https://neutroncode.com)
|
||||
- "HTML" (Fallback)
|
||||
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
|
||||
- Open Source - General:
|
||||
- [GitHub](https://github.com/)
|
||||
- [GitLab](https://gitlab.com/)
|
||||
- [Codeberg](https://codeberg.org/)
|
||||
- [F-Droid](https://f-droid.org/)
|
||||
- Third Party F-Droid Repos
|
||||
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||
- [SourceHut](https://git.sr.ht/)
|
||||
- Other - General:
|
||||
- [APKPure](https://apkpure.net/)
|
||||
- [Aptoide](https://aptoide.com/)
|
||||
- [Uptodown](https://uptodown.com/)
|
||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||
- [Huawei AppGallery](https://appgallery.huawei.com/)
|
||||
- Jenkins Jobs
|
||||
- Open Source - App-Specific:
|
||||
- [Mullvad](https://mullvad.net/en/)
|
||||
- [Signal](https://signal.org/)
|
||||
- [VLC](https://videolan.org/)
|
||||
- Other - App-Specific:
|
||||
- [WhatsApp](https://whatsapp.com)
|
||||
- [Telegram App](https://telegram.org)
|
||||
- [Neutron Code](https://neutroncode.com)
|
||||
- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -33,10 +46,13 @@ Currently supported App sources:
|
||||
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
|
||||
alt="Get it on IzzyOnDroid"
|
||||
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/dev.imranr.obtainium.fdroid/)
|
||||
|
||||
[PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index)
|
||||
|
||||
## Limitations
|
||||
- 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.
|
||||
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@@ -21,10 +22,6 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
@@ -32,7 +29,8 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
namespace "dev.imranr.obtainium"
|
||||
compileSdk flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@@ -49,16 +47,28 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "dev.imranr.obtainium"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
minSdkVersion 24
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
flavorDimensions "flavor"
|
||||
|
||||
productFlavors {
|
||||
normal {
|
||||
dimension "flavor"
|
||||
applicationIdSuffix ""
|
||||
}
|
||||
fdroid {
|
||||
dimension "flavor"
|
||||
applicationIdSuffix ".fdroid"
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
@@ -78,6 +88,19 @@ flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
repositories {
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
|
||||
import com.android.build.OutputFile
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
|
||||
if (abiVersionCode != null) {
|
||||
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
|
||||
} else {
|
||||
output.versionCodeOverride = variant.versionCode * 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="dev.imranr.obtainium">
|
||||
<application
|
||||
android:label="Obtainium"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -25,35 +27,49 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
|
||||
android:exported="false" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="obtainium" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<service
|
||||
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"/>
|
||||
android:maxSdkVersion="29" />\
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
</manifest>
|
||||
@@ -2,5 +2,4 @@ package dev.imranr.obtainium
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
||||
class MainActivity: FlutterActivity()
|
||||
@@ -1,8 +1,8 @@
|
||||
<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">
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
<group
|
||||
android:translateX="-30.39437"
|
||||
android:translateY="-54.68043">
|
||||
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
@@ -2,4 +2,5 @@
|
||||
<paths>
|
||||
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
|
||||
<external-path path="." name="external_storage_root" />
|
||||
<external-path name="external_files" path="."/>
|
||||
</paths>
|
||||
@@ -1,20 +1,11 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
// [required] background_fetch
|
||||
url "${project(':background_fetch').projectDir}/libs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +17,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.4.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
BIN
assets/graphics/badge_obtainium.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/graphics/icon.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
372
assets/translations/bs.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "Nije važeći URL aplikacije {}",
|
||||
"noReleaseFound": "Nije moguće pronaći odgovarajuće izdanje",
|
||||
"noVersionFound": "Nije moguće odrediti verziju izdanja",
|
||||
"urlMatchesNoSource": "URL se ne podudara s poznatim izvorom",
|
||||
"cantInstallOlderVersion": "Nije moguće instalirati stariju verziju aplikacije",
|
||||
"appIdMismatch": "ID preuzetog paketa se ne podudara s postojećim ID-om aplikacije",
|
||||
"functionNotImplemented": "Ova klasa nije implementirala ovu funkciju",
|
||||
"placeholder": "Rezervirano mjesto",
|
||||
"someErrors": "Došlo je do nekih grešaka",
|
||||
"unexpectedError": "Neočekivana greška",
|
||||
"ok": "Dobro",
|
||||
"and": "i",
|
||||
"githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)",
|
||||
"includePrereleases": "Uključi preliminarna izdanja",
|
||||
"fallbackToOlderReleases": "Povratak na starija izdanja",
|
||||
"filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu",
|
||||
"invalidRegEx": "Nevažeći regularni izraz",
|
||||
"noDescription": "Bez opisa",
|
||||
"cancel": "Otkaži",
|
||||
"continue": "Nastavite",
|
||||
"requiredInBrackets": "(obavezno)",
|
||||
"dropdownNoOptsError": "GREŠKA: PADAJUĆI MENI MORA IMATI NAJMANJE JEDNU OPCIJU",
|
||||
"colour": "Boja",
|
||||
"standard": "Standard",
|
||||
"custom": "Custom",
|
||||
"useMaterialYou": "Use Material You",
|
||||
"githubStarredRepos": "GitHub repo-i sa zvjezdicom",
|
||||
"uname": "Korisničko ime",
|
||||
"wrongArgNum": "Naveden je pogrešan broj argumenata",
|
||||
"xIsTrackOnly": "{} je samo za praćenje",
|
||||
"source": "Izvor",
|
||||
"app": "Aplikacija. ",
|
||||
"appsFromSourceAreTrackOnly": "Aplikacije iz ovog izvora su 'Samo za praćenje'.",
|
||||
"youPickedTrackOnly": "Odabrali ste opciju „Samo za praćenje”.",
|
||||
"trackOnlyAppDescription": "Aplikacija će se pratiti radi ažuriranja, ali Obtainium neće moći da je preuzme ili instalira.",
|
||||
"cancelled": "Otkazano",
|
||||
"appAlreadyAdded": "Aplikacija je već dodana",
|
||||
"alreadyUpToDateQuestion": "Aplikacija je već ažurirana?",
|
||||
"addApp": "Dodaj aplikaciju",
|
||||
"appSourceURL": "Izvorni URL aplikacije",
|
||||
"error": "Greška",
|
||||
"add": "Dodaj",
|
||||
"searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)",
|
||||
"search": "Pretraživanje",
|
||||
"additionalOptsFor": "Dodatne opcije za {}",
|
||||
"supportedSources": "Podržani izvori",
|
||||
"trackOnlyInBrackets": "(Samo za praćenje)",
|
||||
"searchableInBrackets": "(Može se pretraživati)",
|
||||
"appsString": "Aplikacije",
|
||||
"noApps": "Nema aplikacija",
|
||||
"noAppsForFilter": "Nema aplikacija za filter",
|
||||
"byX": "Autor {}",
|
||||
"percentProgress": "Napredak: {}%",
|
||||
"pleaseWait": "Molimo sačekajte",
|
||||
"updateAvailable": "Ažuriranje dostupno",
|
||||
"notInstalled": "Nije instalirano",
|
||||
"pseudoVersion": "pseudo-verzija",
|
||||
"selectAll": "Označi sve",
|
||||
"deselectX": "Poništi odabir {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} će biti uklonjen iz Obtainiuma, ali će ostati instaliran na uređaju.",
|
||||
"removeSelectedAppsQuestion": "Želite li ukloniti odabrane aplikacije?",
|
||||
"removeSelectedApps": "Ukloni odabrane aplikacije",
|
||||
"updateX": "Nadogradi {}",
|
||||
"installX": "Instaliraj {}",
|
||||
"markXTrackOnlyAsUpdated": "Označi {}\n(samo za praćenje)\nkao ažurirano",
|
||||
"changeX": "Promjena {}",
|
||||
"installUpdateApps": "Instalirajte/ažurirajte aplikacije",
|
||||
"installUpdateSelectedApps": "Instalirajte/ažurirajte odabrane aplikacije",
|
||||
"markXSelectedAppsAsUpdated": "Označite {} odabrane aplikacije kao ažurirane?",
|
||||
"no": "Ne",
|
||||
"yes": "Da",
|
||||
"markSelectedAppsUpdated": "Označi odabrane aplikacije kao ažurirane",
|
||||
"pinToTop": "Prikvači na vrh",
|
||||
"unpinFromTop": "Otkvači sa vrha",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Resetujte status instalacije za odabrane aplikacije?",
|
||||
"installStatusOfXWillBeResetExplanation": "Status instalacije bilo koje odabrane aplikacije će se resetovati.\n\nTo može pomoći kada je verzija aplikacije prikazana u Obtainiumu netačna zbog neuspjelih ažuriranja ili drugih problema.",
|
||||
"customLinkMessage": "Ove veze rade na uređajima s instaliranim Obtainiumom",
|
||||
"shareAppConfigLinks": "Podijelite konfiguraciju aplikacije kao HTML vezu",
|
||||
"shareSelectedAppURLs": "Podijeli odabrane URL-ove aplikacija",
|
||||
"resetInstallStatus": "Resetujte status instalacije",
|
||||
"more": "Više",
|
||||
"removeOutdatedFilter": "Uklonite zastarjeli filter aplikacija",
|
||||
"showOutdatedOnly": "Prikaži samo zastarjele aplikacije",
|
||||
"filter": "Filtriranje",
|
||||
"filterApps": "Filtriraj aplikacije",
|
||||
"appName": "Naziv aplikacije",
|
||||
"author": "Autor",
|
||||
"upToDateApps": "Ažurirane aplikacije",
|
||||
"nonInstalledApps": "Neinstalirane aplikacije",
|
||||
"importExport": "Uvoz/izvoz",
|
||||
"settings": "Postavke",
|
||||
"exportedTo": "Izvezeno u {}",
|
||||
"obtainiumExport": "Obtainium izvoz",
|
||||
"invalidInput": "Neispravan unos.",
|
||||
"importedX": "Uvezeno {}",
|
||||
"obtainiumImport": "Obtainium uvoz",
|
||||
"importFromURLList": "Uvoz iz URL liste",
|
||||
"searchQuery": "Pretraga za: ",
|
||||
"appURLList": "Lista URL adresa aplikacija",
|
||||
"line": "Linija",
|
||||
"searchX": "Pretraživanje {}",
|
||||
"noResults": "Nema rezultata",
|
||||
"importX": "Uvoz {}",
|
||||
"importedAppsIdDisclaimer": "Uvezene aplikacije mogu se pogrešno prikazati kao „Nije instalirano”.\nDa biste to riješili, ponovo ih instalirajte putem aplikacije Obtainium.\nTo ne bi trebalo uticati na podatke aplikacije.\n\nUtječe samo na URL i metode uvoza treće strane.",
|
||||
"importErrors": "Uvezi greške",
|
||||
"importedXOfYApps": "{} od {} aplikacija uvezeno.",
|
||||
"followingURLsHadErrors": "Sljedeći URL-ovi su imali greške:",
|
||||
"selectURL": "Odaberite URL",
|
||||
"selectURLs": "Odaberite URL-ove",
|
||||
"pick": "Odaberi",
|
||||
"theme": "Tema",
|
||||
"dark": "Tamna",
|
||||
"light": "Svijetla",
|
||||
"followSystem": "Pratite sistem",
|
||||
"useBlackTheme": "Koristite čisto crnu tamnu temu",
|
||||
"appSortBy": "Aplikacije sortirane po",
|
||||
"authorName": "Autor/Ime",
|
||||
"nameAuthor": "Ime/Autor",
|
||||
"asAdded": "Kao što je dodano",
|
||||
"appSortOrder": "Redoslijed sortiranja aplikacija",
|
||||
"ascending": "Uzlazno",
|
||||
"descending": "Silazno",
|
||||
"bgUpdateCheckInterval": "Interval provjere ažuriranja u pozadini",
|
||||
"neverManualOnly": "Nikada - samo ručno",
|
||||
"appearance": "Izgled",
|
||||
"showWebInAppView": "Prikaži izvornu web stranicu u prikazu aplikacije",
|
||||
"pinUpdates": "Prikvačite ažuriranja na vrh prikaza aplikacija",
|
||||
"updates": "Nadogradnje",
|
||||
"sourceSpecific": "Specifično za izvor",
|
||||
"appSource": "Izvor aplikacije",
|
||||
"noLogs": "Nema evidencije",
|
||||
"appLogs": "Evidencije aplikacija",
|
||||
"close": "Zatvori",
|
||||
"share": "Podijeli",
|
||||
"appNotFound": "Aplikacija nije pronađena",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-izvoz",
|
||||
"pickAnAPK": "Odaberite APK",
|
||||
"appHasMoreThanOnePackage": "{} ima više od jednog paketa:",
|
||||
"deviceSupportsXArch": "Vaš uređaj podržava {} arhitekturu procesora.",
|
||||
"deviceSupportsFollowingArchs": "Vaš uređaj podržava sljedeće arhitekture procesora:",
|
||||
"warning": "Upozorenje",
|
||||
"sourceIsXButPackageFromYPrompt": "Izvor aplikacije je '{}', ali paket za izdavanje dolazi iz '{}'. Želite li nastaviti?",
|
||||
"updatesAvailable": "Dostupna ažuriranja",
|
||||
"updatesAvailableNotifDescription": "Obavještava korisnika da su ažuriranja dostupna za jednu ili više aplikacija koje prati Obtainium",
|
||||
"noNewUpdates": "Nema novih ažuriranja.",
|
||||
"xHasAnUpdate": "{} ima ažuriranje.",
|
||||
"appsUpdated": "Aplikacije su ažurirane",
|
||||
"appsNotUpdated": "Failed to update applications",
|
||||
"appsUpdatedNotifDescription": "Obavještava korisnika da su u pozadini primijenjena ažuriranja na jednu ili više aplikacija",
|
||||
"xWasUpdatedToY": "{} je ažuriran na {}.",
|
||||
"xWasNotUpdatedToY": "Failed to update {} to {}.",
|
||||
"errorCheckingUpdates": "Greška pri provjeri ažuriranja",
|
||||
"errorCheckingUpdatesNotifDescription": "Obavijest koja se prikazuje kada provjera sigurnosnog ažuriranja ne uspije",
|
||||
"appsRemoved": "Aplikacije su uklonjene",
|
||||
"appsRemovedNotifDescription": "Obavještava korisnika da je jedna ili više aplikacija uklonjeno zbog grešaka prilikom učitavanja",
|
||||
"xWasRemovedDueToErrorY": "{} je uklonjen zbog ove greške: {}",
|
||||
"completeAppInstallation": "Dovršite instalaciju aplikacije",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium mora biti otvoren za instalaciju aplikacija",
|
||||
"completeAppInstallationNotifDescription": "Traži od korisnika da se vrati u Obtainium kako bi dovršio instalaciju aplikacije",
|
||||
"checkingForUpdates": "Tražim moguće nadogradnje",
|
||||
"checkingForUpdatesNotifDescription": "Privremeno obavještenje koje se pojavljuje prilikom provjere ažuriranja",
|
||||
"pleaseAllowInstallPerm": "Dozvolite Obtainiumu da instalira aplikacije",
|
||||
"trackOnly": "Samo za praćenje",
|
||||
"errorWithHttpStatusCode": "Greška {}",
|
||||
"versionCorrectionDisabled": "Ispravka verzije je onemogućena (izgleda da plugin ne radi)",
|
||||
"unknown": "Nepoznato",
|
||||
"none": "Ništa",
|
||||
"never": "Nikad",
|
||||
"latestVersionX": "Najnovija verzija: {}",
|
||||
"installedVersionX": "Instalirana verzija: {}",
|
||||
"lastUpdateCheckX": "Posljednja provjera ažuriranja: {}",
|
||||
"remove": "Izbriši",
|
||||
"yesMarkUpdated": "Da, označi kao ažurirano",
|
||||
"fdroid": "Službeni F-Droid",
|
||||
"appIdOrName": "ID ili ime aplikacije",
|
||||
"appId": "Apl ID",
|
||||
"appWithIdOrNameNotFound": "Nije pronađena aplikacija s tim ID-om ili imenom",
|
||||
"reposHaveMultipleApps": "Repo-i mogu sadržavati više aplikacija",
|
||||
"fdroidThirdPartyRepo": "F-Droid Repo treće strane",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Razgovor na Steamu (chat)",
|
||||
"install": "Instaliraj",
|
||||
"markInstalled": "Označi kao instalirano",
|
||||
"update": "Nadogradi",
|
||||
"markUpdated": "Označi kao ažurirano",
|
||||
"additionalOptions": "Dodatne opcije",
|
||||
"disableVersionDetection": "Onemogući detekciju verzije",
|
||||
"noVersionDetectionExplanation": "Ova opcija bi se trebala koristiti samo za aplikacije gdje detekcija verzije ne radi ispravno.",
|
||||
"downloadingX": "Preuzimanje {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Obavještava korisnika o napretku u preuzimanju aplikacije",
|
||||
"noAPKFound": "APK nije pronađen",
|
||||
"noVersionDetection": "Nema detekcije verzije",
|
||||
"categorize": "Kategoriziraj",
|
||||
"categories": "Kategorije",
|
||||
"category": "Kategorija",
|
||||
"noCategory": "Nema kategorije",
|
||||
"noCategories": "Nema kategorija",
|
||||
"deleteCategoriesQuestion": "Želite li izbrisati kategorije?",
|
||||
"categoryDeleteWarning": "Sve aplikacije u izbrisanim kategorijama će biti postavljene kao nekategorisane.",
|
||||
"addCategory": "Dodaj kategoriju",
|
||||
"label": "Oznaka",
|
||||
"language": "Jezik",
|
||||
"copiedToClipboard": "Podaci kopirani u međuspremnik",
|
||||
"storagePermissionDenied": "Dozvola za pohranu je odbijena",
|
||||
"selectedCategorizeWarning": "Ovo će zamijeniti sve postojeće postavke kategorije za odabrane aplikacije.",
|
||||
"filterAPKsByRegEx": "Filtrirajte APK-ove prema regularnom izrazu",
|
||||
"removeFromObtainium": "Ukloni iz Obtainiuma",
|
||||
"uninstallFromDevice": "Deinstaliraj s uređaja",
|
||||
"onlyWorksWithNonVersionDetectApps": "Radi samo za aplikacije s onemogućenom detekcijom verzije.",
|
||||
"releaseDateAsVersion": "Koristi datum izdanja kao verziju",
|
||||
"releaseDateAsVersionExplanation": "Ova opcija bi se trebala koristiti samo za aplikacije gdje detekcija verzije ne radi ispravno, ali je datum izdavanja dostupan.",
|
||||
"changes": "Promjene",
|
||||
"releaseDate": "Datum izdavanja",
|
||||
"importFromURLsInFile": "Uvoz iz URL-ova u datoteci (kao što je OPML)",
|
||||
"versionDetectionExplanation": "Pomiri niz verzije sa verzijom otkrivenom iz OS-a",
|
||||
"versionDetection": "Otkrivanje verzije",
|
||||
"standardVersionDetection": "Detekcija standardne verzije",
|
||||
"groupByCategory": "Grupiši po kategoriji",
|
||||
"autoApkFilterByArch": "Pokušajte filtrirati APK-ove po arhitekturi procesora ako je moguće",
|
||||
"overrideSource": "Premosti izvor",
|
||||
"dontShowAgain": "Ne prikazuj ovo ponovo",
|
||||
"dontShowTrackOnlyWarnings": "Ne prikazuj upozorenja „Samo za praćenje”",
|
||||
"dontShowAPKOriginWarnings": "Ne prikazuj upozorenja o porijeklu APK-a",
|
||||
"moveNonInstalledAppsToBottom": "Premjesti neinstalirane aplikacije na dno prikaza aplikacija",
|
||||
"gitlabPATLabel": "GitLab token za lični pristup",
|
||||
"about": "O nama",
|
||||
"requiresCredentialsInSettings": "{}: Za ovo su potrebni dodatni akreditivi (u Postavkama)",
|
||||
"checkOnStart": "Provjerite ima li novosti pri pokretanju",
|
||||
"tryInferAppIdFromCode": "Pokušati otkriti ID aplikacije iz izvornog koda",
|
||||
"removeOnExternalUninstall": "Automatski ukloni eksterno deinstalirane aplikacije",
|
||||
"pickHighestVersionCode": "Automatski odaberite najviši kôd verzije APK-a",
|
||||
"checkUpdateOnDetailPage": "Provjerite ima li novosti pri otvaranju stranice s detaljima aplikacije",
|
||||
"disablePageTransitions": "Ugasite animaciju prijelaza stranice",
|
||||
"reversePageTransitions": "Reverzne animacije prijelaza stranice",
|
||||
"minStarCount": "Najmanji broj zvjezdica",
|
||||
"addInfoBelow": "Dodajte ove informacije ispod.",
|
||||
"addInfoInSettings": "Dodajte ove informacije u Postavkama.",
|
||||
"githubSourceNote": "GitHub ograničavanje se može izbjeći korišćenjem tokena za lični pristup.",
|
||||
"sortByLastLinkSegment": "Sortiraj samo po zadnjem segmentu veze",
|
||||
"filterReleaseNotesByRegEx": "Filtirajte promjene u izdanju po regularnom izrazu",
|
||||
"customLinkFilterRegex": "Prilagođeni APK link filtrira se po regularnom izrazu (Zadano '.apk$')",
|
||||
"appsPossiblyUpdated": "Pokušano ažuriranje aplikacija",
|
||||
"appsPossiblyUpdatedNotifDescription": "Obavještava korisnika da je ažuriranje jedne ili više aplikacija potencijalno izvršeno u pozadini",
|
||||
"xWasPossiblyUpdatedToY": "{} aplikacija bi trebala biti ažurirana na {}.",
|
||||
"enableBackgroundUpdates": "Dozvolite ažuriranja u pozadini",
|
||||
"backgroundUpdateReqsExplanation": "Ažuriranja u pozadini možda neće raditi za sve aplikacije.",
|
||||
"backgroundUpdateLimitsExplanation": "Uspjeh ažuriranja u pozadini se može provjeriti tek kada otvorite Obtainium.",
|
||||
"verifyLatestTag": "Provjerite 'posljednu' ('latest') oznaku",
|
||||
"intermediateLinkRegex": "Filter za 'srednju' vezu za posjetu",
|
||||
"filterByLinkText": "Filtriraj linkove prema tekstu linka",
|
||||
"intermediateLinkNotFound": "Intermediate link nije nađen",
|
||||
"intermediateLink": "srednja karika",
|
||||
"exemptFromBackgroundUpdates": "Izuzmi iz ažuriranja u pozadini (ako su uključeni)",
|
||||
"bgUpdatesOnWiFiOnly": "Isključite ažuriranje u pozadini kada niste na WiFi-ju",
|
||||
"autoSelectHighestVersionCode": "Automatski izaberite najveću (verziju) versionCode APK-a",
|
||||
"versionExtractionRegEx": "RegEx ekstrakcija verzije",
|
||||
"matchGroupToUse": "Podjesite grupu za upotebu",
|
||||
"highlightTouchTargets": "Istaknite manje vidljive touch mete",
|
||||
"pickExportDir": "Izaberite datoteku za izvoz",
|
||||
"autoExportOnChanges": "Automatski izvezite pri promjenama",
|
||||
"includeSettings": "Uključi postavke",
|
||||
"filterVersionsByRegEx": "Filtrirajte verzije po regulatnom izrazu",
|
||||
"trySelectingSuggestedVersionCode": "Probajte izabrati preloženu (verziju) versionCode APK-a",
|
||||
"dontSortReleasesList": "Zadrži redosled izdanja iz API-a",
|
||||
"reverseSort": "Obrni redosled",
|
||||
"takeFirstLink": "Uzmi prvi link",
|
||||
"skipSort": "Preskoči sortiranje",
|
||||
"debugMenu": "Meni za otkrivanje grešaka",
|
||||
"bgTaskStarted": "Rad u pozadini pokrenut - provjerite log-ove.",
|
||||
"runBgCheckNow": "Pokrenite pozadinsku provjeru ažuriranja sad",
|
||||
"versionExtractWholePage": "Primjenite Regex ekstrakciju verzije na cijelu stranicu",
|
||||
"installing": "Instaliranje",
|
||||
"skipUpdateNotifications": "Ne prikazujte obavještenja ažuriranja",
|
||||
"updatesAvailableNotifChannel": "Dostupna ažuriranja",
|
||||
"appsUpdatedNotifChannel": "Aplikacije su ažurirane",
|
||||
"appsPossiblyUpdatedNotifChannel": "Pokušano ažuriranje aplikacija",
|
||||
"errorCheckingUpdatesNotifChannel": "Greška pri provjeri ažuriranja",
|
||||
"appsRemovedNotifChannel": "Aplikacije su uklonjene",
|
||||
"downloadingXNotifChannel": "Preuzimanje {}",
|
||||
"completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije",
|
||||
"checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Isključivo provjerite ažuriranje za instalirane i aplikacije 'samo za praćenje'",
|
||||
"supportFixedAPKURL": "Podržite fiksne APK URL-ove",
|
||||
"selectX": "Izaberite {}",
|
||||
"parallelDownloads": "Dozvoli paralelna preuzimanja",
|
||||
"useShizuku": "Use Shizuku or Sui to install",
|
||||
"shizukuBinderNotFound": "Shizuku is not running",
|
||||
"shizukuOld": "Old Shizuku version (<11) - update it",
|
||||
"shizukuOldAndroidWithADB": "Shizuku running on Android < 8.1 with ADB - update Android or use Sui instead",
|
||||
"shizukuPretendToBeGooglePlay": "Set Google Play as the installation source (if Shizuku is used)",
|
||||
"useSystemFont": "Koristite sistemski font",
|
||||
"useVersionCodeAsOSVersion": "Koristite kod verzije aplikacije kao verziju koju je otkrio OS",
|
||||
"requestHeader": "Zaglavlje zahtjeva",
|
||||
"useLatestAssetDateAsReleaseDate": "Koristite najnovije otpremanje materijala kao datum izdavanja",
|
||||
"defaultPseudoVersioningMethod": "Zadana metoda pseudo-verzija",
|
||||
"partialAPKHash": "Djelomični APK Hash",
|
||||
"APKLinkHash": "APK Link Hash",
|
||||
"directAPKLink": "Direktna APK veza",
|
||||
"pseudoVersionInUse": "Pseudo-verzija je u upotrebi",
|
||||
"installed": "Instalirano",
|
||||
"latest": "Najnoviji",
|
||||
"invertRegEx": "Obrni regularni izraz",
|
||||
"note": "Note",
|
||||
"selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.",
|
||||
"badDownload": "The APK could not be parsed (incompatible or partial download)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)",
|
||||
"appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Želite li ukloniti aplikaciju?",
|
||||
"other": "Želite li ukloniti aplikacije?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Previše zahtjeva (ograničena broj zahteva) - pokušajte ponovo za {} minutu",
|
||||
"other": "Previše zahtjeva (ograničena cijena) - pokušajte ponovo za {} min."
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Provjera ažuriranja u pozadini naišla je na {}, zakazuje se ponovni pokušaj za {} minutu",
|
||||
"other": "Provjera ažuriranja u pozadini naišla je na {}, zakazuje se ponovni pokušaj za {} min."
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Provjera ažuriranja u pozadini je pronašla {} ažuriranje - korisnik će biti obavješten ako je to potrebno",
|
||||
"other": "Provjera ažuriranja u pozadini je pronašla {} ažuriranja - korisnik će biti obavješten ako je to potrebno"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} aplikacija",
|
||||
"other": "{} aplikacije"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URL-ovi"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} minuta",
|
||||
"other": "min."
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} sat",
|
||||
"other": "{} sat/i"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} dan",
|
||||
"other": "{} dana"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Izbrisan {n} log (prije = {before}, nakon = {after})",
|
||||
"other": "Izbrisano {n} log-ova (prije = {before}, nakon = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} i još 1 aplikacija ima ažuriranja.",
|
||||
"other": "{} i još {} aplikacija imaju ažuriranja."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} i još 1 aplikacija je ažurirana.",
|
||||
"other": "{} i još {} aplikacija je ažurirano."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Failed to update {} and 1 more app.",
|
||||
"other": "Failed to update {} and {} more apps."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} i još jedna aplikacija je vjerovatno ažurirana.",
|
||||
"other": "{} i još {} aplikacija su vjerovatno ažurirane."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
372
assets/translations/cs.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "Žádná platná {} adresa URL aplikace",
|
||||
"noReleaseFound": "Nebyla nalezena odpovídající verze",
|
||||
"noVersionFound": "Nelze určit verzi vydání",
|
||||
"urlMatchesNoSource": "URL neodpovídá žádnému známému zdroji",
|
||||
"cantInstallOlderVersion": "Nelze nainstalovat starší verzi aplikace",
|
||||
"appIdMismatch": "ID staženého balíčku neodpovídá ID existující aplikace",
|
||||
"functionNotImplemented": "Tato třída nemá implementovánu tuto funkci",
|
||||
"placeholder": "Zástupce",
|
||||
"someErrors": "Vyskytly se nějaké chyby",
|
||||
"unexpectedError": "Neočekávaná chyba",
|
||||
"ok": "Ok",
|
||||
"and": "a",
|
||||
"githubPATLabel": "GitHub Personal Access Token (zvyšuje limit rychlosti)",
|
||||
"includePrereleases": "Zahrnout předběžné verze",
|
||||
"fallbackToOlderReleases": "Přechod na starší verze",
|
||||
"filterReleaseTitlesByRegEx": "Filtrovat názvy verzí podle regulárního výrazu",
|
||||
"invalidRegEx": "Neplatný regulární výraz",
|
||||
"noDescription": "Žádný popis",
|
||||
"cancel": "Zrušit",
|
||||
"continue": "Pokračovat",
|
||||
"requiredInBrackets": "(Požadované)",
|
||||
"dropdownNoOptsError": "ERROR: DROPDOWN MUSÍ MÍT AŽ JEDNU MOŽNOST",
|
||||
"colour": "Barva",
|
||||
"standard": "Standardní",
|
||||
"custom": "Vlastní",
|
||||
"useMaterialYou": "Použijte materiál, který jste",
|
||||
"githubStarredRepos": "GitHub označená hvězdičkou",
|
||||
"uname": "Uživatelské jméno",
|
||||
"wrongArgNum": "Nesprávný počet zadaných argumentů",
|
||||
"xIsTrackOnly": "{} je určeno pouze pro sledování",
|
||||
"source": "Zdroj",
|
||||
"app": "Aplikace",
|
||||
"appsFromSourceAreTrackOnly": "Aplikace z tohoto zdroje jsou Jen sledovány.",
|
||||
"youPickedTrackOnly": "Vybrali jste možnost Jen sledovat.",
|
||||
"trackOnlyAppDescription": "Aplikace je sledována kvůli aktualizacím, ale Obtainium ji nebude stahovat ani instalovat.",
|
||||
"cancelled": "Zrušeno",
|
||||
"appAlreadyAdded": "Aplikace již přidána",
|
||||
"alreadyUpToDateQuestion": "App already up to date?",
|
||||
"addApp": "Přidat aplikaci",
|
||||
"appSourceURL": "Zdrojová adresa URL aplikace",
|
||||
"error": "Chyba",
|
||||
"add": "Přidat",
|
||||
"searchSomeSourcesLabel": "Vyhledávání (pouze pro určité zdroje)",
|
||||
"search": "Hledat",
|
||||
"additionalOptsFor": "Další možnosti pro {}",
|
||||
"supportedSources": "Podporované zdroje",
|
||||
"trackOnlyInBrackets": "(Pouze stopy)",
|
||||
"searchableInBrackets": "(s možností vyhledávání)",
|
||||
"appsString": "Apky",
|
||||
"noApps": "Žádné aplikace",
|
||||
"noAppsForFilter": "Žádné aplikace pro vybraný filtr",
|
||||
"byX": "Od {}",
|
||||
"percentProgress": "Pokrok: {}%",
|
||||
"pleaseWait": "Počkejte prosím",
|
||||
"updateAvailable": "Aktualizace je k dispozici",
|
||||
"notInstalled": "Není nainstalováno",
|
||||
"pseudoVersion": "pseudoverze",
|
||||
"selectAll": "Vybrat vše",
|
||||
"deselectX": "{} deselected",
|
||||
"xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
|
||||
"removeSelectedAppsQuestion": "Odebrat vybrané aplikace?",
|
||||
"removeSelectedApps": "Odebrat vybrané aplikace",
|
||||
"updateX": "Aktualizovat {}",
|
||||
"installX": "Instalovat {}",
|
||||
"markXTrackOnlyAsUpdated": "Označit {}\n(Jen sledované)\njako aktualizované",
|
||||
"changeX": "Změnit {}",
|
||||
"installUpdateApps": "Instalovat/aktualizovat aplikace",
|
||||
"installUpdateSelectedApps": "Instalovat/aktualizovat vybrané aplikace",
|
||||
"markXSelectedAppsAsUpdated": "Označit {} vybrané aplikace jako aktuální?",
|
||||
"no": "Ne",
|
||||
"yes": "Ano",
|
||||
"markSelectedAppsUpdated": "Označit vybrané aplikace jako aktuální",
|
||||
"pinToTop": "Připnout nahoru",
|
||||
"unpinFromTop": "Odepnout shora",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Obnovit stav instalace vybraných aplikací?",
|
||||
"installStatusOfXWillBeResetExplanation": "Stav instalace vybraných aplikací bude resetován. To může být užitečné, pokud je verze aplikace zobrazená v Obtainium nesprávná z důvodu neúspěšných aktualizací nebo jiných problémů.",
|
||||
"customLinkMessage": "Tyto odkazy fungují na zařízeních s nainstalovaným Obtainium",
|
||||
"shareAppConfigLinks": "Sdílet konfiguraci aplikace jako odkaz HTML",
|
||||
"shareSelectedAppURLs": "Sdílet adresy URL vybraných aplikací",
|
||||
"resetInstallStatus": "Obnovit stav instalace",
|
||||
"more": "Více",
|
||||
"removeOutdatedFilter": "Odstranit filtr Neaktuální",
|
||||
"showOutdatedOnly": "Zobrazovat pouze zastaralé aplikace",
|
||||
"filter": "Filtr",
|
||||
"filterApps": "Filtrovat aplikace",
|
||||
"appName": "Název aplikace",
|
||||
"author": "Autor",
|
||||
"upToDateApps": "Aktuální apky",
|
||||
"nonInstalledApps": "Neinstalované apky",
|
||||
"importExport": "Import Export",
|
||||
"settings": "Nastavení",
|
||||
"exportedTo": "Exportováno do {}",
|
||||
"obtainiumExport": "Export obtainia",
|
||||
"invalidInput": "Neplatný vstup",
|
||||
"importedX": "Importováno {}",
|
||||
"obtainiumImport": "Dovoz obtainia",
|
||||
"importFromURLList": "Import ze seznamu URL",
|
||||
"searchQuery": "Vyhledávací dotaz",
|
||||
"appURLList": "Seznam adres aplikací",
|
||||
"line": "Linka",
|
||||
"searchX": "Vyhledávání {}",
|
||||
"noResults": "Nebyly nalezeny žádné výsledky",
|
||||
"importX": "Import {}",
|
||||
"importedAppsIdDisclaimer": "Importované aplikace mohou být nesprávně zobrazeny jako \"Neinstalovány\". Chcete-li to opravit, nainstalujte je znovu prostřednictvím Obtainium. To nemá vliv na data aplikací. Ovlivňuje pouze metody importu URL a třetích stran.",
|
||||
"importErrors": "Chyba importu",
|
||||
"importedXOfYApps": "{}importováno z {} aplikací.",
|
||||
"followingURLsHadErrors": "U následujících adres došlo k chybám:",
|
||||
"selectURL": "Vybrat adresu",
|
||||
"selectURLs": "Select adresy",
|
||||
"pick": "Vybrat",
|
||||
"theme": "Téma",
|
||||
"dark": "Tmavé",
|
||||
"light": "Světlé",
|
||||
"followSystem": "Jako systém",
|
||||
"useBlackTheme": "Použít čistě černé tmavé téma",
|
||||
"appSortBy": "Seřadit podle",
|
||||
"authorName": "Autor/Jméno",
|
||||
"nameAuthor": "Jméno/Autor",
|
||||
"asAdded": "Přidáno",
|
||||
"appSortOrder": "Seřadit",
|
||||
"ascending": "Vzestupně",
|
||||
"descending": "Sestupně",
|
||||
"bgUpdateCheckInterval": "Interval kontroly aktualizace na pozadí",
|
||||
"neverManualOnly": "Nikdy - pouze ručně",
|
||||
"appearance": "Vzhled",
|
||||
"showWebInAppView": "Zobrazit zdrojové webové stránky v zobrazení aplikace",
|
||||
"pinUpdates": "Připnout aplikace s aktualizacemi nahoru",
|
||||
"updates": "Aktualizace",
|
||||
"sourceSpecific": "Specifické pro zdroj",
|
||||
"appSource": "Zdroj aplikace",
|
||||
"noLogs": "Žádné protokoly",
|
||||
"appLogs": "Záznamy apky",
|
||||
"close": "Zavřít",
|
||||
"share": "Sdílet",
|
||||
"appNotFound": "Aplikace nenalezena",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Vybrat APK",
|
||||
"appHasMoreThanOnePackage": "{} má více než jeden balíček:",
|
||||
"deviceSupportsXArch": "Vaše zařízení podporuje architekturu CPU {}.",
|
||||
"deviceSupportsFollowingArchs": "Vaše zařízení podporuje následující architektury CPU:",
|
||||
"warning": "Varování",
|
||||
"sourceIsXButPackageFromYPrompt": "Zdroj aplikace je '{}', ale balíček pro vydání je z '{}'. Pokračovat?",
|
||||
"updatesAvailable": "Dostupné aktualizace",
|
||||
"updatesAvailableNotifDescription": "Upozorňuje uživatele, že jsou k dispozici aktualizace pro jednu nebo více aplikací sledovaných Obtainium",
|
||||
"noNewUpdates": "Žádné nové aktualizace.",
|
||||
"xHasAnUpdate": "{} má aktualizaci.",
|
||||
"appsUpdated": "Aplikace aktualizovány",
|
||||
"appsNotUpdated": "Nepodařilo se aktualizovat aplikace",
|
||||
"appsUpdatedNotifDescription": "Upozornit, že byly provedeny aktualizace jedné nebo více aplikací na pozadí",
|
||||
"xWasUpdatedToY": "{} byla aktualizována na {}",
|
||||
"xWasNotUpdatedToY": "Nepodařilo se aktualizovat {} na {}.",
|
||||
"errorCheckingUpdates": "Chyba kontroly aktualizací",
|
||||
"errorCheckingUpdatesNotifDescription": "Zobrazit oznámení při neúspěšné kontrole aktualizací na pozadí",
|
||||
"appsRemoved": "Odstraněné aplikace",
|
||||
"appsRemovedNotifDescription": "Oznámit, že jedna nebo více aplikací bylo odstraněno z důvodu chyb při načítání",
|
||||
"xWasRemovedDueToErrorY": "{} byla odstraněna z důvodu následující chyby: {}",
|
||||
"completeAppInstallation": "Dokončit instalaci aplikace",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium musí být otevřeno, aby bylo možné instalovat aplikace",
|
||||
"completeAppInstallationNotifDescription": "Vyzvat k návratu do Obtainium pro dokončení instalace aplikací",
|
||||
"checkingForUpdates": "Zkontrolovat aktualizace",
|
||||
"checkingForUpdatesNotifDescription": "Dočasné oznámení zobrazené při kontrole aktualizací",
|
||||
"pleaseAllowInstallPerm": "Povolte prosím Obtainium instalovat aplikace",
|
||||
"trackOnly": "Jen sledovat",
|
||||
"errorWithHttpStatusCode": "Chyba {}",
|
||||
"versionCorrectionDisabled": "Oprava verze zakázána (zásuvný modul zřejmě nefunguje)",
|
||||
"unknown": "Neznám",
|
||||
"none": "Žádný",
|
||||
"never": "Nikdy",
|
||||
"latestVersionX": "Nejnovější verze: {}",
|
||||
"installedVersionX": "Nainstalovaná verze: {}",
|
||||
"lastUpdateCheckX": "Poslední kontrola aktualizace: {}",
|
||||
"remove": "Odebrat",
|
||||
"yesMarkUpdated": "Ano, označit jako aktualizované",
|
||||
"fdroid": "Oficiální repozitář F-Droid",
|
||||
"appIdOrName": "ID nebo název apky",
|
||||
"appId": "ID aplikace",
|
||||
"appWithIdOrNameNotFound": "Žádná aplikace s tímto ID nebo názvem nebyla nalezena",
|
||||
"reposHaveMultipleApps": "Repozitáře mohou obsahovat více aplikací",
|
||||
"fdroidThirdPartyRepo": "F-Droid repozitář třetí strany",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"install": "Nainstalujte",
|
||||
"markInstalled": "Označit jako nainstalovaný",
|
||||
"update": "Aktualizovat",
|
||||
"markUpdated": "Označit jako aktuální",
|
||||
"additionalOptions": "Další možnosti",
|
||||
"disableVersionDetection": "Deaktivovat detekci verze",
|
||||
"noVersionDetectionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně.",
|
||||
"downloadingX": "Stáhnout {}",
|
||||
"downloadX": "Stáhnout {}",
|
||||
"downloadedX": "Staženo {}",
|
||||
"releaseAsset": "Vydání aktiva",
|
||||
"downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace",
|
||||
"noAPKFound": "Žádná APK nebyla nalezena",
|
||||
"noVersionDetection": "Žádná detekce verze",
|
||||
"categorize": "Kategorizovat",
|
||||
"categories": "Kategorie",
|
||||
"category": "Kategorie",
|
||||
"noCategory": "Žádná kategorie",
|
||||
"noCategories": "Žádné kategorie",
|
||||
"deleteCategoriesQuestion": "Smazat kategorie?",
|
||||
"categoryDeleteWarning": "Všechny aplikace v odstraněných kategoriích budou nastaveny na nekategorizované.",
|
||||
"addCategory": "Přidat kategorii",
|
||||
"label": "Štítek",
|
||||
"language": "Jazyk",
|
||||
"copiedToClipboard": "Zkopírováno do schránky",
|
||||
"storagePermissionDenied": "Oprávnění k ukládání odepřeno",
|
||||
"selectedCategorizeWarning": "Toto nahradí všechna stávající nastavení kategorií pro vybrané aplikace.",
|
||||
"filterAPKsByRegEx": "Filtrovat APK podle regulárního výrazu",
|
||||
"removeFromObtainium": "Odebrat z Obtainium",
|
||||
"uninstallFromDevice": "Odinstalovat ze zařízení",
|
||||
"onlyWorksWithNonVersionDetectApps": "Funguje pouze pro aplikace s vypnutou detekcí verze.",
|
||||
"releaseDateAsVersion": "Použít datum vydání jako verzi",
|
||||
"releaseDateAsVersionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně, ale je k dispozici datum vydání.",
|
||||
"changes": "Změny",
|
||||
"releaseDate": "Datum vydání",
|
||||
"importFromURLsInFile": "Importovat adresy URL ze souboru (např. OPML)",
|
||||
"versionDetectionExplanation": "Sloučit řetězec verze s verzí zjištěnou z OS",
|
||||
"versionDetection": "Detekce verze",
|
||||
"standardVersionDetection": "Standardní detekce verze",
|
||||
"groupByCategory": "Seskupit podle kategorie",
|
||||
"autoApkFilterByArch": "Pokud je to možné, pokuste se filtrovat soubory APK podle architektury procesoru",
|
||||
"overrideSource": "Přepsat zdroj",
|
||||
"dontShowAgain": "Nezobrazovat znovu",
|
||||
"dontShowTrackOnlyWarnings": "Nezobrazovat varování pro 'Jen sledované'",
|
||||
"dontShowAPKOriginWarnings": "Nezobrazovat varování pro původ APK",
|
||||
"moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
|
||||
"gitlabPATLabel": "Osobní přístupový token GitLab",
|
||||
"about": "O",
|
||||
"requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)",
|
||||
"checkOnStart": "Zkontrolovat jednou při spuštění",
|
||||
"tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu",
|
||||
"removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace",
|
||||
"pickHighestVersionCode": "Automaticky vybrat nejvyšší verzi APK",
|
||||
"checkUpdateOnDetailPage": "Zkontrolovat aktualizaci při otevření stránky s podrobnostmi aplikace",
|
||||
"disablePageTransitions": "Zakázat animace pro přechody stránek",
|
||||
"reversePageTransitions": "Obrátit animace pro přechody stránek",
|
||||
"minStarCount": "Minimální počet hvězdiček",
|
||||
"addInfoBelow": "Přidat tuto informaci na konec stránky.",
|
||||
"addInfoInSettings": "Přidat tuto informaci do nastavení.",
|
||||
"githubSourceNote": "Omezení rychlosti GitHub lze obejít pomocí klíče API.",
|
||||
"sortByLastLinkSegment": "Seřadit pouze podle poslední části odkazu",
|
||||
"filterReleaseNotesByRegEx": "Filtrovat poznámky k vydání podle regulárního výrazu",
|
||||
"customLinkFilterRegex": "Vlastní filtr odkazů APK podle regulárního výrazu (výchozí '.apk$')",
|
||||
"appsPossiblyUpdated": "Byly provedeny pokusy o aktualizaci aplikací",
|
||||
"appsPossiblyUpdatedNotifDescription": "Upozorňuje uživatele, že na pozadí mohly být provedeny aktualizace jedné nebo více aplikací",
|
||||
"xWasPossiblyUpdatedToY": "{} mohlo být aktualizováno na {}.",
|
||||
"enableBackgroundUpdates": "Povolit aktualizace na pozadí",
|
||||
"backgroundUpdateReqsExplanation": "Aktualizace na pozadí nemusí být možná pro všechny aplikace.",
|
||||
"backgroundUpdateLimitsExplanation": "Úspěšnost instalace na pozadí lze určit pouze v případě, že je otevřeno Obtainium.",
|
||||
"verifyLatestTag": "Zkontrolovat značku latest",
|
||||
"intermediateLinkRegex": "Filtrovat mezipropojení, které by mělo být navštíveno jako první",
|
||||
"filterByLinkText": "Filtrovat odkazy podle textu odkazu",
|
||||
"intermediateLinkNotFound": "Připojený odkaz nenalezen",
|
||||
"intermediateLink": "Připojený odkaz",
|
||||
"exemptFromBackgroundUpdates": "Vyloučit z aktualizací na pozadí (je-li povoleno)",
|
||||
"bgUpdatesOnWiFiOnly": "Deaktivovat aktualizace na pozadí, pokud není k dispozici Wi-Fi",
|
||||
"autoSelectHighestVersionCode": "Automaticky vybrat nejvyšší verzi APK",
|
||||
"versionExtractionRegEx": "Extrakce verze pomocí RegEx",
|
||||
"matchGroupToUse": "Odpovídá použité skupině",
|
||||
"highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku",
|
||||
"pickExportDir": "Vybrat adresář pro export",
|
||||
"autoExportOnChanges": "Automatický export při změně",
|
||||
"includeSettings": "Zahrnout nastavení",
|
||||
"filterVersionsByRegEx": "Filtrovat verze podle regulárních výrazů",
|
||||
"trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovanou verzi APK",
|
||||
"dontSortReleasesList": "Seřadit vydání z rozhraní API",
|
||||
"reverseSort": "Obrácené třídění",
|
||||
"takeFirstLink": "Použít první odkaz",
|
||||
"skipSort": "Přeskočit třídění",
|
||||
"debugMenu": "Nabídka ladění",
|
||||
"bgTaskStarted": "Spuštěna úloha na pozadí - zkontrolujte protokoly.",
|
||||
"runBgCheckNow": "Spustit kontrolu aktualizací na pozadí nyní",
|
||||
"versionExtractWholePage": "Použít extrakci verze pomocí RegEx na celou stránku",
|
||||
"installing": "Instaluji",
|
||||
"skipUpdateNotifications": "Neposkytovat oznámení o aktualizaci",
|
||||
"updatesAvailableNotifChannel": "Dostupné aktualizace",
|
||||
"appsUpdatedNotifChannel": "Apky aktualizovány",
|
||||
"appsPossiblyUpdatedNotifChannel": "Byly provedeny pokusy o aktualizace aplikací",
|
||||
"errorCheckingUpdatesNotifChannel": "Chyba při kontrole aktualizací",
|
||||
"appsRemovedNotifChannel": "Odstraněné apky",
|
||||
"downloadingXNotifChannel": "Stáhnout {}",
|
||||
"completeAppInstallationNotifChannel": "Dokončit instalaci aplikace",
|
||||
"checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Na aktualizace kontrolovat pouze nainstalované aplikace a aplikace označené Track only",
|
||||
"supportFixedAPKURL": "Odhadnout novější verzi na základě prvních třiceti číslic kontrolního součtu adresy URL APK, pokud není podporována jinak",
|
||||
"selectX": "Vybrat {}",
|
||||
"parallelDownloads": "Povolit souběžné stahování",
|
||||
"useShizuku": "K instalaci použijte Shizuku nebo Sui",
|
||||
"shizukuBinderNotFound": "Shizuku neběží",
|
||||
"shizukuOld": "Stará verze Shizuku (<11) - aktualizujte ji",
|
||||
"shizukuOldAndroidWithADB": "Shizuku běží na Androidu < 8.1 s ADB - aktualizujte Android nebo místo toho použijte Sui",
|
||||
"shizukuPretendToBeGooglePlay": "Nastavení Google Play jako zdroje instalace (pokud se používá Shizuku)",
|
||||
"useSystemFont": "Použít systémové písmo",
|
||||
"useVersionCodeAsOSVersion": "Použít kód verze aplikace jako verzi zjištěnou OS",
|
||||
"requestHeader": "Hlavička požadavku",
|
||||
"useLatestAssetDateAsReleaseDate": "Použít poslední nahrané dílo jako datum vydání",
|
||||
"defaultPseudoVersioningMethod": "Výchozí metoda pseudoverze",
|
||||
"partialAPKHash": "Částečný hash APK",
|
||||
"APKLinkHash": "Odkaz APK Hash",
|
||||
"directAPKLink": "Přímý odkaz APK",
|
||||
"pseudoVersionInUse": "Pseudoverze se používá",
|
||||
"installed": "Instalováno",
|
||||
"latest": "Nejnovější",
|
||||
"invertRegEx": "Invertovat regulární výraz",
|
||||
"note": "Poznámka",
|
||||
"selfHostedNote": "Rozbalovací seznam \"{}\" lze použít k dosažení vlastních/obvyklých instancí libovolného zdroje.",
|
||||
"badDownload": "APK nelze analyzovat (nekompatibilní nebo částečné stažení)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Sdílení nových aplikací s aplikací AppVerifier (pokud je k dispozici)",
|
||||
"appVerifierInstructionToast": "Sdílejte do aplikace AppVerifier a po dokončení se sem vraťte.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Odstranit Apku?",
|
||||
"other": "Odstranit Apky?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Příliš mnoho požadavků (omezená rychlost) - zkuste to znovu za {} minutu",
|
||||
"other": "Příliš mnoho požadavků (omezená rychlost) - zkuste to znovu za {} minut"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Při kontrole aktualizace na pozadí byla zjištěna chyba {}, opakování bude naplánováno za {} minut",
|
||||
"other": "Při kontrole aktualizací na pozadí byla zjištěna chyba {}, opakování bude naplánováno za {} minut"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Při kontrole aktualizací na pozadí nalezena {}aktualizace - v případě potřeby upozorní uživatele",
|
||||
"other": "Kontrola aktualizací na pozadí nalezla {} aktualizací - v případě potřeby upozorní uživatele"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} Apka",
|
||||
"other": "{} Apky"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} Adresa",
|
||||
"other": "{} Adres"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Minuta",
|
||||
"other": "{} Minut"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Hodina",
|
||||
"other": "{} Hodin"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} Den",
|
||||
"other": "{} Dnů"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "{n} Záznam vymazán (před = {before}, po = {after})",
|
||||
"other": "{n} Záznamů vymazáno (před = {before}, po = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} a 1 další aplikace mají aktualizace.",
|
||||
"other": "{} a {} další aplikace mají aktualizace."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} a 1 další aplikace mají aktualizace.",
|
||||
"other": "{} a {} další aplikace byly aktualizovány."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Nepodařilo se aktualizovat {} a 1 další aplikaci.",
|
||||
"other": "Nepodařilo se aktualizovat {} a {} další aplikace."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} a 1 další aplikace možno aktualizovat",
|
||||
"other": "{} a {} další aplikace mohou být aktualizovány."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK"
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,9 @@
|
||||
"placeholder": "Platzhalter",
|
||||
"someErrors": "Es traten einige Fehler auf",
|
||||
"unexpectedError": "Unerwarteter Fehler",
|
||||
"ok": "Okay",
|
||||
"ok": "OK",
|
||||
"and": "und",
|
||||
"startedBgUpdateTask": "Hintergrundaktualisierungsprüfung gestartet",
|
||||
"bgUpdateIgnoreAfterIs": "Hintergrundaktualisierung 'ignoreAfter' ist {}",
|
||||
"startedActualBGUpdateCheck": "Überprüfung der Hintergrundaktualisierung gestartet",
|
||||
"bgUpdateTaskFinished": "Hintergrundaktualisierungsprüfung abgeschlossen",
|
||||
"firstRun": "Dies ist der erste Start von Obtainium überhaupt",
|
||||
"settingUpdateCheckIntervalTo": "Aktualisierungsintervall auf {} stellen",
|
||||
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
|
||||
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
|
||||
"githubPATFormat": "Benutzername:Token",
|
||||
"githubPATLinkText": "Über GitHub PATs",
|
||||
"includePrereleases": "Vorabversionen einbeziehen",
|
||||
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
|
||||
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
|
||||
@@ -28,18 +19,21 @@
|
||||
"noDescription": "Keine Beschreibung",
|
||||
"cancel": "Abbrechen",
|
||||
"continue": "Weiter",
|
||||
"requiredInBrackets": "(Benötigt)",
|
||||
"requiredInBrackets": "(wird benötigt)",
|
||||
"dropdownNoOptsError": "FEHLER: DROPDOWN MUSS MINDESTENS EINE OPTION HABEN",
|
||||
"colour": "Farbe",
|
||||
"standard": "Standard",
|
||||
"custom": "Benutzerdefiniert",
|
||||
"useMaterialYou": "Verwende Material You",
|
||||
"githubStarredRepos": "GitHub Starred Repos",
|
||||
"uname": "Benutzername",
|
||||
"wrongArgNum": "Falsche Anzahl von Argumenten übermittelt",
|
||||
"wrongArgNum": "Falsche Anzahl von Argumenten (Parametern) übermittelt",
|
||||
"xIsTrackOnly": "{} ist nur zur Nachverfolgung",
|
||||
"source": "Quelle",
|
||||
"app": "App",
|
||||
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind 'Nur Nachverfolgen'.",
|
||||
"youPickedTrackOnly": "Sie haben die Option 'Nur Nachverfolgen' gewählt.",
|
||||
"trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
|
||||
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zum Nachverfolgen.",
|
||||
"youPickedTrackOnly": "Sie haben die Option „Nur Nachverfolgen“ gewählt.",
|
||||
"trackOnlyAppDescription": "Die App wird auf Aktualisierungen überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
|
||||
"cancelled": "Abgebrochen",
|
||||
"appAlreadyAdded": "App bereits hinzugefügt",
|
||||
"alreadyUpToDateQuestion": "App bereits auf dem neuesten Stand?",
|
||||
@@ -47,10 +41,10 @@
|
||||
"appSourceURL": "Quell-URL der App",
|
||||
"error": "Fehler",
|
||||
"add": "Hinzufügen",
|
||||
"searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)",
|
||||
"searchSomeSourcesLabel": "Suche (nur für bestimmte Quellen)",
|
||||
"search": "Suchen",
|
||||
"additionalOptsFor": "Zusatzoptionen für {}",
|
||||
"supportedSourcesBelow": "Unterstützte Quellen:",
|
||||
"supportedSources": "Unterstützte Quellen",
|
||||
"trackOnlyInBrackets": "(Nur Nachverfolgen)",
|
||||
"searchableInBrackets": "(Durchsuchbar)",
|
||||
"appsString": "Apps",
|
||||
@@ -60,18 +54,17 @@
|
||||
"percentProgress": "Fortschritt: {}%",
|
||||
"pleaseWait": "Bitte warten",
|
||||
"updateAvailable": "Aktualisierung verfügbar",
|
||||
"estimateInBracketsShort": "(ca.)",
|
||||
"notInstalled": "Nicht installiert",
|
||||
"estimateInBrackets": "(Ungefähr)",
|
||||
"pseudoVersion": "Pseudoversion",
|
||||
"selectAll": "Alle auswählen",
|
||||
"deselectN": "{} abgewählt",
|
||||
"deselectX": "{} abgewählt",
|
||||
"xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.",
|
||||
"removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?",
|
||||
"removeSelectedApps": "Ausgewählte Apps entfernen",
|
||||
"updateX": "Aktualisiere {}",
|
||||
"installX": "Installiere {}",
|
||||
"markXTrackOnlyAsUpdated": "Markiere {}\n(Nur Nachverfolgen)\nals aktualisiert",
|
||||
"changeX": "Ändern {}",
|
||||
"changeX": "Ändere {}",
|
||||
"installUpdateApps": "Apps installieren/aktualisieren",
|
||||
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
||||
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
||||
@@ -79,16 +72,17 @@
|
||||
"yes": "Ja",
|
||||
"markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell",
|
||||
"pinToTop": "Oben anheften",
|
||||
"unpinFromTop": "'Oben anheften' aufheben",
|
||||
"unpinFromTop": "„Oben anheften“ aufheben",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?",
|
||||
"installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.",
|
||||
"customLinkMessage": "Diese Links funktionieren auf Geräten, wo Obtainium installiert ist",
|
||||
"shareAppConfigLinks": "Teile die Appkonfiguration als HTML-Link",
|
||||
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
|
||||
"resetInstallStatus": "Installationsstatus zurücksetzen",
|
||||
"more": "Mehr",
|
||||
"removeOutdatedFilter": "App-Filter 'Nicht aktuell' entfernen",
|
||||
"removeOutdatedFilter": "App-Filter ‚Nicht aktuell‘ entfernen",
|
||||
"showOutdatedOnly": "Nur nicht aktuelle Apps anzeigen",
|
||||
"filter": "Filter",
|
||||
"filterActive": "Filter *",
|
||||
"filterApps": "Apps filtern",
|
||||
"appName": "App Name",
|
||||
"author": "Autor",
|
||||
@@ -97,22 +91,21 @@
|
||||
"importExport": "Import/Export",
|
||||
"settings": "Einstellungen",
|
||||
"exportedTo": "Exportiert zu {}",
|
||||
"obtainiumExport": "Obtainium Export",
|
||||
"obtainiumExport": "Obtainium-Export",
|
||||
"invalidInput": "Ungültige Eingabe",
|
||||
"importedX": "Importiert {}",
|
||||
"obtainiumImport": "Obtainium Import",
|
||||
"obtainiumImport": "Obtainium-Import",
|
||||
"importFromURLList": "Importieren aus URL-Liste",
|
||||
"searchQuery": "Suchanfrage",
|
||||
"appURLList": "App URL-Liste",
|
||||
"line": "Linie",
|
||||
"searchX": "Suche {}",
|
||||
"noResults": "Keine Ergebnisse gefunden",
|
||||
"importX": "Import {}",
|
||||
"importX": "Importieren {}",
|
||||
"importedAppsIdDisclaimer": "Importierte Apps werden möglicherweise fälschlicherweise als \"Nicht installiert\" angezeigt. Um dies zu beheben, installieren Sie sie erneut über Obtainium. Dies hat keine Auswirkungen auf App-Daten. Es betrifft nur URL- und Drittanbieter-Importmethoden.",
|
||||
"importErrors": "Importfehler",
|
||||
"importedXOfYApps": "{} von {} Apps importiert.",
|
||||
"followingURLsHadErrors": "Bei folgenden URLs traten Fehler auf:",
|
||||
"okay": "Okay",
|
||||
"selectURL": "URL auswählen",
|
||||
"selectURLs": "URLs auswählen",
|
||||
"pick": "Auswählen",
|
||||
@@ -120,30 +113,28 @@
|
||||
"dark": "Dunkel",
|
||||
"light": "Hell",
|
||||
"followSystem": "System folgen",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "Verwende Pure Black Dark Theme",
|
||||
"appSortBy": "App sortieren nach",
|
||||
"authorName": "Autor/Name",
|
||||
"nameAuthor": "Name/Autor",
|
||||
"asAdded": "Wie hinzugefügt",
|
||||
"appSortOrder": "App Sortierung nach",
|
||||
"appSortOrder": "App sortieren nach",
|
||||
"ascending": "Aufsteigend",
|
||||
"descending": "Absteigend",
|
||||
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
|
||||
"neverManualOnly": "Nie - nur manuell",
|
||||
"neverManualOnly": "Nie – nur manuell",
|
||||
"appearance": "Aussehen",
|
||||
"showWebInAppView": "Quellwebseite in der App-Ansicht anzeigen",
|
||||
"pinUpdates": "Apps mit Aktualisierungen oben anheften",
|
||||
"updates": "Aktualisierungen",
|
||||
"sourceSpecific": "Quellenspezifisch",
|
||||
"appSource": "App-Quelle",
|
||||
"noLogs": "Keine Protokolle",
|
||||
"appLogs": "App Protokolle",
|
||||
"noLogs": "Keine Logs",
|
||||
"appLogs": "App-Logs",
|
||||
"close": "Schließen",
|
||||
"share": "Teilen",
|
||||
"appNotFound": "App nicht gefunden",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"obtainiumExportHyphenatedLowercase": "Obtainium-Export",
|
||||
"pickAnAPK": "APK auswählen",
|
||||
"appHasMoreThanOnePackage": "{} verfügt über mehr als ein Paket:",
|
||||
"deviceSupportsXArch": "Ihr Gerät unterstützt die CPU-Architektur {}.",
|
||||
@@ -155,8 +146,10 @@
|
||||
"noNewUpdates": "Keine neuen Aktualisierungen.",
|
||||
"xHasAnUpdate": "{} hat eine Aktualisierung.",
|
||||
"appsUpdated": "Apps aktualisiert",
|
||||
"appsNotUpdated": "Aktualisierung der Anwendungen fehlgeschlagen",
|
||||
"appsUpdatedNotifDescription": "Benachrichtigt den Benutzer, dass Aktualisierungen für eine oder mehrere Apps im Hintergrund durchgeführt wurden",
|
||||
"xWasUpdatedToY": "{} wurde auf {} aktualisiert.",
|
||||
"xWasNotUpdatedToY": "Die Aktualisierung von {} auf {} ist fehlgeschlagen.",
|
||||
"errorCheckingUpdates": "Fehler beim Prüfen auf Aktualisierungen",
|
||||
"errorCheckingUpdatesNotifDescription": "Eine Benachrichtigung, die angezeigt wird, wenn die Prüfung der Hintergrundaktualisierung fehlschlägt",
|
||||
"appsRemoved": "Apps entfernt",
|
||||
@@ -179,14 +172,14 @@
|
||||
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
|
||||
"remove": "Entfernen",
|
||||
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
||||
"fdroid": "F-Droid",
|
||||
"fdroid": "offizielles F-Droid-Repo",
|
||||
"appIdOrName": "App ID oder Name",
|
||||
"appId": "App ID",
|
||||
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
|
||||
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
|
||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||
"steam": "Steam",
|
||||
"fdroidThirdPartyRepo": "F-Droid Drittparteienrepo",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"steamChat": "Steam-Chat",
|
||||
"install": "Installieren",
|
||||
"markInstalled": "Als Installiert markieren",
|
||||
"update": "Aktualisieren",
|
||||
@@ -195,6 +188,9 @@
|
||||
"disableVersionDetection": "Versionsermittlung deaktivieren",
|
||||
"noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.",
|
||||
"downloadingX": "Lade {} herunter",
|
||||
"downloadX": "{} herunterladen",
|
||||
"downloadedX": "{} heruntergeladen",
|
||||
"releaseAsset": "release Asset",
|
||||
"downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App",
|
||||
"noAPKFound": "Keine APK gefunden",
|
||||
"noVersionDetection": "Keine Versionserkennung",
|
||||
@@ -208,7 +204,7 @@
|
||||
"addCategory": "Kategorie hinzufügen",
|
||||
"label": "Bezeichnung",
|
||||
"language": "Sprache",
|
||||
"copiedToClipboard": "Copied to Clipboard",
|
||||
"copiedToClipboard": "In die Zwischenablage kopiert",
|
||||
"storagePermissionDenied": "Speicherberechtigung verweigert",
|
||||
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
|
||||
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
|
||||
@@ -219,38 +215,127 @@
|
||||
"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)",
|
||||
"importFromURLsInFile": "Importieren von URLs aus Datei (z. B. OPML)",
|
||||
"versionDetectionExplanation": "Abgleich der Versionsnummer mit der vom Betriebssystem erkannten Version",
|
||||
"versionDetection": "Versionserkennung",
|
||||
"standardVersionDetection": "Standardversionserkennung",
|
||||
"groupByCategory": "Nach Kategorie gruppieren",
|
||||
"autoApkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
|
||||
"overrideSource": "Quelle überschreiben",
|
||||
"dontShowAgain": "Nicht noch einmal zeigen",
|
||||
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
||||
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
||||
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "Über",
|
||||
"requiresCredentialsInSettings": "{}: Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
||||
"checkOnStart": "Überprüfe einmalig beim Start",
|
||||
"tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln",
|
||||
"removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps",
|
||||
"pickHighestVersionCode": "Automatische Auswahl des APK mit höchstem Versionscode",
|
||||
"checkUpdateOnDetailPage": "Nach Aktualisierungen suchen, wenn eine App-Detailseite geöffnet wird",
|
||||
"disablePageTransitions": "Animationen für Seitenübergänge deaktivieren",
|
||||
"reversePageTransitions": "Umgekehrte Animationen für Seitenübergänge",
|
||||
"minStarCount": "Minimale Anzahl von Sternen",
|
||||
"addInfoBelow": "Fügen Sie diese Informationen unten hinzu.",
|
||||
"addInfoInSettings": "Fügen Sie diese Info in den Einstellungen hinzu.",
|
||||
"githubSourceNote": "Die GitHub-Ratenbegrenzung kann mit einem API-Schlüssel umgangen werden.",
|
||||
"sortByLastLinkSegment": "Sortiere nur nach dem letzten Teil des Links",
|
||||
"filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern",
|
||||
"customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
|
||||
"appsPossiblyUpdated": "App Aktualisierungen wurden versucht",
|
||||
"appsPossiblyUpdatedNotifDescription": "Benachrichtigt den Benutzer, dass Aktualisierungen für eine oder mehrere Apps möglicherweise im Hintergrund durchgeführt wurden",
|
||||
"xWasPossiblyUpdatedToY": "{} wurde möglicherweise aktualisiert auf {}.",
|
||||
"enableBackgroundUpdates": "Aktiviere Hintergrundaktualisierungen",
|
||||
"backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.",
|
||||
"backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
|
||||
"verifyLatestTag": "Überprüfe das „latest“ Tag",
|
||||
"intermediateLinkRegex": "Filter für einen „Zwischen“-Link, der zuerst besucht werden soll",
|
||||
"filterByLinkText": "Filtere Links durch Linktext",
|
||||
"intermediateLinkNotFound": "„Zwischen“-Link nicht gefunden",
|
||||
"intermediateLink": "„Zwischen“-Link",
|
||||
"exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
|
||||
"bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
|
||||
"autoSelectHighestVersionCode": "Automatisch höchste APK-Version auswählen",
|
||||
"versionExtractionRegEx": "Versions-Extraktion per RegEx",
|
||||
"matchGroupToUse": "zu verwendende Gruppe abgleichen",
|
||||
"highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
|
||||
"pickExportDir": "Export-Verzeichnis wählen",
|
||||
"autoExportOnChanges": "Automatischer Export bei Änderung(en)",
|
||||
"includeSettings": "Einstellungen einbeziehen",
|
||||
"filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
|
||||
"trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen",
|
||||
"dontSortReleasesList": "Freigaberelease von der API ordern",
|
||||
"reverseSort": "Umgekehrtes Sortieren",
|
||||
"takeFirstLink": "Verwende den ersten Link",
|
||||
"skipSort": "Überspringe Sortieren",
|
||||
"debugMenu": "Debug-Menü",
|
||||
"bgTaskStarted": "Hintergrundaufgabe gestartet – Logs prüfen.",
|
||||
"runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
|
||||
"versionExtractWholePage": "Versions-Extraktion per RegEx auf die gesamte Seite anwenden",
|
||||
"installing": "Installiere",
|
||||
"skipUpdateNotifications": "Keine Benachrichtigung zu App-Aktualisierungen geben",
|
||||
"updatesAvailableNotifChannel": "Aktualisierungen verfügbar",
|
||||
"appsUpdatedNotifChannel": "Apps aktualisiert",
|
||||
"appsPossiblyUpdatedNotifChannel": "App Aktualisierungen wurden versucht",
|
||||
"errorCheckingUpdatesNotifChannel": "Fehler beim Prüfen auf Aktualisierungen",
|
||||
"appsRemovedNotifChannel": "Apps entfernt",
|
||||
"downloadingXNotifChannel": "Lade {} herunter",
|
||||
"completeAppInstallationNotifChannel": "App Installation abschließen",
|
||||
"checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen",
|
||||
"supportFixedAPKURL": "neuere Version anhand der ersten dreißig Zahlen der Checksumme der APK URL erraten, wenn anderweitig nicht unterstützt",
|
||||
"selectX": "Wähle {}",
|
||||
"parallelDownloads": "Erlaube parallele Downloads",
|
||||
"useShizuku": "Verwenden Sie Shizuku oder Sui zur Installation",
|
||||
"shizukuBinderNotFound": "Kompatibler Shizukudienst wurde nicht gefunden",
|
||||
"shizukuOld": "Alte Shizuku-Version (<11) - aktualisieren Sie sie",
|
||||
"shizukuOldAndroidWithADB": "Shizuku läuft auf Android < 8.1 mit ADB - aktualisieren Sie Android oder verwenden Sie stattdessen Sui",
|
||||
"shizukuPretendToBeGooglePlay": "Google Play als Installationsquelle festlegen (wenn Shizuku verwendet wird)",
|
||||
"useSystemFont": "Verwende die Systemschriftart",
|
||||
"useVersionCodeAsOSVersion": "Verwende die Appversion als erkannte Version vom Betriebssystem",
|
||||
"requestHeader": "Request Header",
|
||||
"useLatestAssetDateAsReleaseDate": "Den letzten Asset-Upload als Veröffentlichungsdatum verwenden",
|
||||
"defaultPseudoVersioningMethod": "Standardmäßiges Verfahren zur Pseudo-Versionierung",
|
||||
"partialAPKHash": "partieller APK-Hash",
|
||||
"APKLinkHash": "APK-Link-Hash",
|
||||
"directAPKLink": "Direkter APK-Link",
|
||||
"pseudoVersionInUse": "Es werden Pseudoversionen verwendet",
|
||||
"installed": "Installiert",
|
||||
"latest": "Neueste Version",
|
||||
"invertRegEx": "Regulären Ausdruck invertieren",
|
||||
"note": "Hinweis",
|
||||
"selfHostedNote": "Das „{}“-Dropdown-Menü kann verwendet werden, um selbst gehostete/angepasste Instanzen einer beliebigen Quelle zu erreichen.",
|
||||
"badDownload": "Die APK konnte nicht geparst werden (inkompatibler oder teilweiser Download)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Neue Apps mit AppVerifier teilen (falls verfügbar)",
|
||||
"appVerifierInstructionToast": "Geben Sie die Daten an AppVerifier weiter und kehren Sie dann hierher zurück, wenn Sie fertig sind.",
|
||||
"removeAppQuestion": {
|
||||
"one": "App entfernen?",
|
||||
"other": "Apps entfernen?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
||||
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
|
||||
"one": "Zu viele Anfragen (Rate begrenzt) – versuche es in {} Minute erneut",
|
||||
"other": "Zu viele Anfragen (Rate begrenzt) – versuche es in {} Minuten erneut"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minute geplant",
|
||||
"other": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minuten geplant"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Hintergrundaktualisierungsprüfung fand {} Aktualisierung - benachrichtigt den Benutzer, falls erforderlich",
|
||||
"other": "Hintergrundaktualisierungsprüfung fand {} Aktualisierungen - benachrichtigt den Benutzer, falls erforderlich"
|
||||
"one": "Die Hintergrundaktualisierungsprüfung fand {} Aktualisierung – benachrichtigt den Benutzer, falls erforderlich",
|
||||
"other": "Die Hintergrundaktualisierungsprüfung fand {} Aktualisierungen – benachrichtigt den Benutzer, falls erforderlich"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} App",
|
||||
"other": "{} Apps"
|
||||
"eine": "{} App",
|
||||
"andere": "{} Apps"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URLs"
|
||||
"eine": "{} URL",
|
||||
"andere": "{} URLs"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Minute",
|
||||
"other": "{} Minutes"
|
||||
"other": "{} Minuten"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Stunde",
|
||||
@@ -261,8 +346,8 @@
|
||||
"other": "{} Tage"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "{n} Protokoll gelöscht (vorher = {vorher}, nachher = {nachher})",
|
||||
"other": "{n} Protokolle gelöscht (vorher = {vorher}, nachher = {nachher})"
|
||||
"one": "{n} Log gelöscht (vorher = {before}, nachher = {after})",
|
||||
"other": "{n} Logs gelöscht (vorher = {before}, nachher = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} und 1 weitere App haben Aktualisierungen.",
|
||||
@@ -271,5 +356,17 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} und 1 weitere Anwendung wurden aktualisiert.",
|
||||
"other": "{} und {} weitere Anwendungen wurden aktualisiert."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Aktualisierung fehlgeschlagen {} und 1 weitere Anwendung.",
|
||||
"other": "Die Aktualisierung von {} und {} weiteren Anwendungen ist fehlgeschlagen."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.",
|
||||
"other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,7 @@
|
||||
"unexpectedError": "Unexpected Error",
|
||||
"ok": "Okay",
|
||||
"and": "and",
|
||||
"startedBgUpdateTask": "Started BG update check task",
|
||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||
"startedActualBGUpdateCheck": "Started actual BG update checking",
|
||||
"bgUpdateTaskFinished": "Finished BG update check task",
|
||||
"firstRun": "This is the first ever run of Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Setting update interval to {}",
|
||||
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
|
||||
"githubPATHint": "PAT must be in this format: username:token",
|
||||
"githubPATFormat": "username:token",
|
||||
"githubPATLinkText": "About GitHub PATs",
|
||||
"includePrereleases": "Include prereleases",
|
||||
"fallbackToOlderReleases": "Fallback to older releases",
|
||||
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
|
||||
@@ -31,6 +22,9 @@
|
||||
"requiredInBrackets": "(Required)",
|
||||
"dropdownNoOptsError": "ERROR: DROPDOWN MUST HAVE AT LEAST ONE OPT",
|
||||
"colour": "Colour",
|
||||
"standard": "Standard",
|
||||
"custom": "Custom",
|
||||
"useMaterialYou": "Use Material You",
|
||||
"githubStarredRepos": "GitHub Starred Repos",
|
||||
"uname": "Username",
|
||||
"wrongArgNum": "Wrong number of arguments provided",
|
||||
@@ -50,7 +44,7 @@
|
||||
"searchSomeSourcesLabel": "Search (Some Sources Only)",
|
||||
"search": "Search",
|
||||
"additionalOptsFor": "Additional Options for {}",
|
||||
"supportedSourcesBelow": "Supported Sources:",
|
||||
"supportedSources": "Supported Sources",
|
||||
"trackOnlyInBrackets": "(Track-Only)",
|
||||
"searchableInBrackets": "(Searchable)",
|
||||
"appsString": "Apps",
|
||||
@@ -60,11 +54,10 @@
|
||||
"percentProgress": "Progress: {}%",
|
||||
"pleaseWait": "Please Wait",
|
||||
"updateAvailable": "Update Available",
|
||||
"estimateInBracketsShort": "(Est.)",
|
||||
"notInstalled": "Not Installed",
|
||||
"estimateInBrackets": "(Estimate)",
|
||||
"pseudoVersion": "pseudo-version",
|
||||
"selectAll": "Select All",
|
||||
"deselectN": "Deselect {}",
|
||||
"deselectX": "Deselect {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.",
|
||||
"removeSelectedAppsQuestion": "Remove Selected Apps?",
|
||||
"removeSelectedApps": "Remove Selected Apps",
|
||||
@@ -82,13 +75,14 @@
|
||||
"unpinFromTop": "Unpin from top",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Reset Install Status for Selected Apps?",
|
||||
"installStatusOfXWillBeResetExplanation": "The install status of any selected Apps will be reset.\n\nThis can help when the App version shown in Obtainium is incorrect due to failed updates or other issues.",
|
||||
"customLinkMessage": "These links work on devices with Obtainium installed",
|
||||
"shareAppConfigLinks": "Share app configuration as HTML link",
|
||||
"shareSelectedAppURLs": "Share Selected App URLs",
|
||||
"resetInstallStatus": "Reset Install Status",
|
||||
"more": "More",
|
||||
"removeOutdatedFilter": "Remove Out-of-Date App Filter",
|
||||
"showOutdatedOnly": "Show Out-of-Date Apps Only",
|
||||
"filter": "Filter",
|
||||
"filterActive": "Filter *",
|
||||
"filterApps": "Filter Apps",
|
||||
"appName": "App Name",
|
||||
"author": "Author",
|
||||
@@ -112,7 +106,6 @@
|
||||
"importErrors": "Import Errors",
|
||||
"importedXOfYApps": "{} of {} Apps imported.",
|
||||
"followingURLsHadErrors": "The following URLs had errors:",
|
||||
"okay": "Okay",
|
||||
"selectURL": "Select URL",
|
||||
"selectURLs": "Select URLs",
|
||||
"pick": "Pick",
|
||||
@@ -120,8 +113,7 @@
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"followSystem": "Follow System",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"followSystemThemeExplanation": "Following system theme is possible only by using third-party applications",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"appSortBy": "App Sort By",
|
||||
"authorName": "Author/Name",
|
||||
@@ -133,8 +125,8 @@
|
||||
"bgUpdateCheckInterval": "Background Update Checking Interval",
|
||||
"neverManualOnly": "Never - Manual Only",
|
||||
"appearance": "Appearance",
|
||||
"showWebInAppView": "Show Source Webpage in App View",
|
||||
"pinUpdates": "Pin Updates to Top of Apps View",
|
||||
"showWebInAppView": "Show Source webpage in App view",
|
||||
"pinUpdates": "Pin updates to top of Apps view",
|
||||
"updates": "Updates",
|
||||
"sourceSpecific": "Source-Specific",
|
||||
"appSource": "App Source",
|
||||
@@ -155,8 +147,10 @@
|
||||
"noNewUpdates": "No new updates.",
|
||||
"xHasAnUpdate": "{} has an update.",
|
||||
"appsUpdated": "Apps Updated",
|
||||
"appsNotUpdated": "Failed to update applications",
|
||||
"appsUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were applied in the background",
|
||||
"xWasUpdatedToY": "{} was updated to {}.",
|
||||
"xWasNotUpdatedToY": "Failed to update {} to {}.",
|
||||
"errorCheckingUpdates": "Error Checking for Updates",
|
||||
"errorCheckingUpdatesNotifDescription": "A notification that shows when background update checking fails",
|
||||
"appsRemoved": "Apps Removed",
|
||||
@@ -174,17 +168,17 @@
|
||||
"unknown": "Unknown",
|
||||
"none": "None",
|
||||
"never": "Never",
|
||||
"latestVersionX": "Latest Version: {}",
|
||||
"installedVersionX": "Installed Version: {}",
|
||||
"latestVersionX": "Latest: {}",
|
||||
"installedVersionX": "Installed: {}",
|
||||
"lastUpdateCheckX": "Last Update Check: {}",
|
||||
"remove": "Remove",
|
||||
"yesMarkUpdated": "Yes, Mark as Updated",
|
||||
"fdroid": "F-Droid",
|
||||
"fdroid": "F-Droid Official",
|
||||
"appIdOrName": "App ID or Name",
|
||||
"appId": "App ID",
|
||||
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
||||
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"install": "Install",
|
||||
@@ -195,6 +189,9 @@
|
||||
"disableVersionDetection": "Disable Version Detection",
|
||||
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
|
||||
"downloadingX": "Downloading {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
|
||||
"noAPKFound": "No APK found",
|
||||
"noVersionDetection": "No version detection",
|
||||
@@ -215,15 +212,104 @@
|
||||
"removeFromObtainium": "Remove from Obtainium",
|
||||
"uninstallFromDevice": "Uninstall from Device",
|
||||
"onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.",
|
||||
"releaseDateAsVersion": "Use Release Date as Version",
|
||||
"releaseDateAsVersion": "Use release date as version string",
|
||||
"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)",
|
||||
"versionDetectionExplanation": "Reconcile version string with version detected from OS",
|
||||
"versionDetection": "Version Detection",
|
||||
"standardVersionDetection": "Standard version detection",
|
||||
"groupByCategory": "Group by Category",
|
||||
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
|
||||
"overrideSource": "Override Source",
|
||||
"dontShowAgain": "Don't show this again",
|
||||
"dontShowTrackOnlyWarnings": "Don't show 'Track-Only' warnings",
|
||||
"dontShowAPKOriginWarnings": "Don't show APK origin warnings",
|
||||
"moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "{} needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check for updates on startup",
|
||||
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
|
||||
"pickHighestVersionCode": "Auto-select highest version code APK",
|
||||
"checkUpdateOnDetailPage": "Check for updates on opening an App detail page",
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"sortByLastLinkSegment": "Sort by only the last segment of the link",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"enableBackgroundUpdates": "Enable background updates",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
|
||||
"filterByLinkText": "Filter links by link text",
|
||||
"intermediateLinkNotFound": "Intermediate link not found",
|
||||
"intermediateLink": "Intermediate link",
|
||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||
"versionExtractionRegEx": "Version String Extraction RegEx",
|
||||
"matchGroupToUse": "Match Group to Use for Version String Extraction RegEx",
|
||||
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||
"pickExportDir": "Pick Export Directory",
|
||||
"autoExportOnChanges": "Auto-export on changes",
|
||||
"includeSettings": "Include settings",
|
||||
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
|
||||
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
|
||||
"dontSortReleasesList": "Retain release order from API",
|
||||
"reverseSort": "Reverse sorting",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Debug Menu",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Run Background Update Check Now",
|
||||
"versionExtractWholePage": "Apply version string extraction Regex to entire page",
|
||||
"installing": "Installing",
|
||||
"skipUpdateNotifications": "Skip update notifications",
|
||||
"updatesAvailableNotifChannel": "Updates Available",
|
||||
"appsUpdatedNotifChannel": "Apps Updated",
|
||||
"appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
|
||||
"errorCheckingUpdatesNotifChannel": "Error Checking for Updates",
|
||||
"appsRemovedNotifChannel": "Apps Removed",
|
||||
"downloadingXNotifChannel": "Downloading {}",
|
||||
"completeAppInstallationNotifChannel": "Complete App Installation",
|
||||
"checkingForUpdatesNotifChannel": "Checking for Updates",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"useShizuku": "Use Shizuku or Sui to install",
|
||||
"shizukuBinderNotFound": "Shizuku service not running",
|
||||
"shizukuOld": "Old Shizuku version (<11) - update it",
|
||||
"shizukuOldAndroidWithADB": "Shizuku running on Android < 8.1 with ADB - update Android or use Sui instead",
|
||||
"shizukuPretendToBeGooglePlay": "Set Google Play as the installation source (if Shizuku is used)",
|
||||
"useSystemFont": "Use the system font",
|
||||
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||
"requestHeader": "Request header",
|
||||
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
|
||||
"defaultPseudoVersioningMethod": "Default Pseudo-Versioning Method",
|
||||
"partialAPKHash": "Partial APK Hash",
|
||||
"APKLinkHash": "APK Link Hash",
|
||||
"directAPKLink": "Direct APK Link",
|
||||
"pseudoVersionInUse": "A Pseudo-Version is in Use",
|
||||
"installed": "Installed",
|
||||
"latest": "Latest",
|
||||
"invertRegEx": "Invert regular expression",
|
||||
"note": "Note",
|
||||
"selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.",
|
||||
"badDownload": "The APK could not be parsed (incompatible or partial download)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)",
|
||||
"appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Remove App?",
|
||||
"other": "Remove Apps?"
|
||||
@@ -269,7 +355,19 @@
|
||||
"other": "{} and {} more apps have updates."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} and 1 more app were updated.",
|
||||
"one": "{} and 1 more app was updated.",
|
||||
"other": "{} and {} more apps were updated."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Failed to update {} and 1 more app.",
|
||||
"other": "Failed to update {} and {} more apps."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
372
assets/translations/es.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "El URL de la aplicación {} no es válido",
|
||||
"noReleaseFound": "No se ha encontrado una versión válida",
|
||||
"noVersionFound": "No se ha podido determinar la versión",
|
||||
"urlMatchesNoSource": "El URL no coincide con ninguna fuente conocida",
|
||||
"cantInstallOlderVersion": "No se puede instalar una versión previa de la aplicación",
|
||||
"appIdMismatch": "El ID del paquete descargado no coincide con el ID de la aplicación instalada",
|
||||
"functionNotImplemented": "Esta clase no ha implementado esta función",
|
||||
"placeholder": "Espacio reservado",
|
||||
"someErrors": "Han ocurrido algunos errores",
|
||||
"unexpectedError": "Error inesperado",
|
||||
"ok": "Aceptar",
|
||||
"and": "y",
|
||||
"githubPATLabel": "Token de acceso personal a GitHub\n(reduce tiempos de espera)",
|
||||
"includePrereleases": "Incluir versiones preliminares",
|
||||
"fallbackToOlderReleases": "Retroceder a versiones previas",
|
||||
"filterReleaseTitlesByRegEx": "Filtrar por título de versión",
|
||||
"invalidRegEx": "Expresión inválida",
|
||||
"noDescription": "Sin descripción",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"requiredInBrackets": "(Requerido)",
|
||||
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
|
||||
"colour": "Color",
|
||||
"standard": "Estándar",
|
||||
"custom": "A medida",
|
||||
"useMaterialYou": "Utilice el material que",
|
||||
"githubStarredRepos": "Repositorios favoritos en GitHub",
|
||||
"uname": "Nombre de usuario",
|
||||
"wrongArgNum": "Número de argumentos provistos inválido",
|
||||
"xIsTrackOnly": "{} es de 'sólo seguimiento'",
|
||||
"source": "Origen",
|
||||
"app": "Aplicación",
|
||||
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'solo seguimiento'.",
|
||||
"youPickedTrackOnly": "Debe seleccionar la opción de 'solo seguimiento'.",
|
||||
"trackOnlyAppDescription": "Se hará el seguimiento de actualizaciones para la aplicación, pero Obtainium no será capaz de descargar o actualizarla.",
|
||||
"cancelled": "Cancelado",
|
||||
"appAlreadyAdded": "Aplicación añadida anteriormente",
|
||||
"alreadyUpToDateQuestion": "¿Aplicación actualizada previamente?",
|
||||
"addApp": "Añadir aplicación",
|
||||
"appSourceURL": "URL de origen de la aplicación",
|
||||
"error": "Error",
|
||||
"add": "Añadir",
|
||||
"searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
|
||||
"search": "Buscar",
|
||||
"additionalOptsFor": "Opciones adicionales para {}",
|
||||
"supportedSources": "Fuentes admitidas",
|
||||
"trackOnlyInBrackets": "(Solo seguimiento)",
|
||||
"searchableInBrackets": "(permite búsqueda)",
|
||||
"appsString": "Aplicaciones",
|
||||
"noApps": "Sin Aplicaciones",
|
||||
"noAppsForFilter": "Sin aplicaciones para filtrar",
|
||||
"byX": "por: {}",
|
||||
"percentProgress": "Progreso: {} %",
|
||||
"pleaseWait": "Espere...",
|
||||
"updateAvailable": "Actualización disponible",
|
||||
"notInstalled": "No instalado",
|
||||
"pseudoVersion": "pseudoversión",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"deselectX": "Deseleccionar {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} se eliminará de Obtainium pero continuará instalada en el dispositivo.",
|
||||
"removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
|
||||
"removeSelectedApps": "Eliminar aplicaciones seleccionadas",
|
||||
"updateX": "Actualizar {}",
|
||||
"installX": "Instalar {}",
|
||||
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo seguimiento)\ncomo actualizada",
|
||||
"changeX": "Cambiar {}",
|
||||
"installUpdateApps": "Instalar/actualizar aplicaciones",
|
||||
"installUpdateSelectedApps": "Instalar/actualizar aplicaciones seleccionadas",
|
||||
"markXSelectedAppsAsUpdated": "¿Marcar {} aplicaciones seleccionadas como actualizadas?",
|
||||
"no": "No",
|
||||
"yes": "Sí",
|
||||
"markSelectedAppsUpdated": "Marcar aplicaciones seleccionadas como actualizadas",
|
||||
"pinToTop": "Anclar al principio",
|
||||
"unpinFromTop": "Desanclar del principio",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar estado de instalación para las aplicaciones seleccionadas?",
|
||||
"installStatusOfXWillBeResetExplanation": "Se restaurará el estado de instalación de las aplicaciones seleccionadas.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
|
||||
"customLinkMessage": "Estos enlaces funcionan en dispositivos con Obtainium instalado",
|
||||
"shareAppConfigLinks": "Compartir la configuración de la aplicación como enlace HTML",
|
||||
"shareSelectedAppURLs": "Compartir URL de las aplicaciones seleccionadas",
|
||||
"resetInstallStatus": "Restaurar estado de instalación",
|
||||
"more": "Más",
|
||||
"removeOutdatedFilter": "Eliminar filtro de aplicaciones desactualizado",
|
||||
"showOutdatedOnly": "Mostrar solo aplicaciones desactualizadas",
|
||||
"filter": "Filtrar",
|
||||
"filterApps": "Filtrar actualizaciones",
|
||||
"appName": "Nombre de la aplicación",
|
||||
"author": "Autor",
|
||||
"upToDateApps": "Aplicaciones actualizadas",
|
||||
"nonInstalledApps": "Aplicaciones no instaladas",
|
||||
"importExport": "Importar/exportar",
|
||||
"settings": "Ajustes",
|
||||
"exportedTo": "Exportado a {}",
|
||||
"obtainiumExport": "Exportar Obtainium",
|
||||
"invalidInput": "Entrada no válida",
|
||||
"importedX": "Importado {}",
|
||||
"obtainiumImport": "Importar Obtainium",
|
||||
"importFromURLList": "Importar desde lista de URL",
|
||||
"searchQuery": "Término de búsqueda",
|
||||
"appURLList": "Lista de URL de aplicaciones",
|
||||
"line": "Línea",
|
||||
"searchX": "Buscar {}",
|
||||
"noResults": "No se ha encontrado ningún resultado",
|
||||
"importX": "Importar desde {}",
|
||||
"importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como «No instalada».\nPara solucionarlo, reinstálelas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a los URL y a los métodos de importación mediante terceros.",
|
||||
"importErrors": "Errores de Importación",
|
||||
"importedXOfYApps": "{} de {} aplicaciones importadas.",
|
||||
"followingURLsHadErrors": "Los URL siguientes han tenido problemas:",
|
||||
"selectURL": "Seleccionar URL",
|
||||
"selectURLs": "Seleccionar URLs",
|
||||
"pick": "Escoger",
|
||||
"theme": "Tema",
|
||||
"dark": "Oscuro",
|
||||
"light": "Claro",
|
||||
"followSystem": "Seguir al sistema",
|
||||
"useBlackTheme": "Negro puro en tema oscuro",
|
||||
"appSortBy": "Ordenar aplicaciones por",
|
||||
"authorName": "Autor/nombre",
|
||||
"nameAuthor": "Nombre/Autor",
|
||||
"asAdded": "Según se Añadieron",
|
||||
"appSortOrder": "Orden de Clasificación",
|
||||
"ascending": "Ascendente",
|
||||
"descending": "Descendente",
|
||||
"bgUpdateCheckInterval": "Comprobación actualizaciones en segundo plano",
|
||||
"neverManualOnly": "Nunca, solo manual",
|
||||
"appearance": "Apariencia",
|
||||
"showWebInAppView": "Mostrar vista de la web de origen",
|
||||
"pinUpdates": "Anclar actualizaciones al principio",
|
||||
"updates": "Actualizaciones",
|
||||
"sourceSpecific": "Fuente específica",
|
||||
"appSource": "Obtainium en GitHub",
|
||||
"noLogs": "Ningún registro",
|
||||
"appLogs": "Registros",
|
||||
"close": "Cerrar",
|
||||
"share": "Compartir",
|
||||
"appNotFound": "Aplicación no encontrada",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-exportación",
|
||||
"pickAnAPK": "Seleccione una APK",
|
||||
"appHasMoreThanOnePackage": "{} tiene más de un paquete:",
|
||||
"deviceSupportsXArch": "Su dispositivo admite las siguientes arquitecturas de procesador: {}.",
|
||||
"deviceSupportsFollowingArchs": "Su dispositivo admite las siguientes arquitecturas de procesador:",
|
||||
"warning": "Aviso",
|
||||
"sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es «{}» pero el paquete de la actualización viene de «{}». ¿Desea continuar?",
|
||||
"updatesAvailable": "Actualizaciones Disponibles",
|
||||
"updatesAvailableNotifDescription": "Notifica al usuario de que hay actualizaciones para una o más aplicaciones monitoreadas por Obtainium",
|
||||
"noNewUpdates": "No hay nuevas actualizaciones.",
|
||||
"xHasAnUpdate": "{} tiene una actualización.",
|
||||
"appsUpdated": "Aplicaciones actualizadas",
|
||||
"appsNotUpdated": "Error al actualizar las aplicaciones",
|
||||
"appsUpdatedNotifDescription": "Notifica al usuario de que una o más aplicaciones han sido actualizadas en segundo plano",
|
||||
"xWasUpdatedToY": "{} ha sido actualizada a {}.",
|
||||
"xWasNotUpdatedToY": "Error al actualizar {} a {}.",
|
||||
"errorCheckingUpdates": "Error al buscar actualizaciones",
|
||||
"errorCheckingUpdatesNotifDescription": "Una notificación que muestra cuándo la comprobación de actualizaciones en segundo plano falla",
|
||||
"appsRemoved": "Aplicaciones eliminadas",
|
||||
"appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
|
||||
"xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
|
||||
"completeAppInstallation": "Instalación completa de la aplicación",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierto para instalar aplicaciones",
|
||||
"completeAppInstallationNotifDescription": "Le pide volver a Obtainium para terminar de instalar una aplicación",
|
||||
"checkingForUpdates": "Buscando actualizaciones...",
|
||||
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
|
||||
"pleaseAllowInstallPerm": "Permita que Obtainium instale aplicaciones",
|
||||
"trackOnly": "Solo para seguimiento",
|
||||
"errorWithHttpStatusCode": "Error {}",
|
||||
"versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
|
||||
"unknown": "Desconocido",
|
||||
"none": "Ninguno",
|
||||
"never": "Nunca",
|
||||
"latestVersionX": "Última versión: {}",
|
||||
"installedVersionX": "Versión instalada: {}",
|
||||
"lastUpdateCheckX": "Última comprobación: {}",
|
||||
"remove": "Eliminar",
|
||||
"yesMarkUpdated": "Sí, marcar como actualizada",
|
||||
"fdroid": "Repositorio oficial F-Droid",
|
||||
"appIdOrName": "ID o Nombre de la Aplicación",
|
||||
"appId": "ID de la Aplicación",
|
||||
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con ese ID o nombre",
|
||||
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
|
||||
"fdroidThirdPartyRepo": "Repositorio de tercera parte F-Droid",
|
||||
"steamMobile": "Steam para móviles",
|
||||
"steamChat": "Chat de Steam",
|
||||
"install": "Instalar",
|
||||
"markInstalled": "Marcar como instalada",
|
||||
"update": "Actualizar",
|
||||
"markUpdated": "Marcar como actualizada",
|
||||
"additionalOptions": "Opciones adicionales",
|
||||
"disableVersionDetection": "Desactivar la detección de versiones",
|
||||
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda que no funcionar correctamente.",
|
||||
"downloadingX": "Descargando {}",
|
||||
"downloadX": "Descargar {}",
|
||||
"downloadedX": "Descargado {}",
|
||||
"releaseAsset": "Liberar activos",
|
||||
"downloadNotifDescription": "Notifica al usuario del progreso de descarga de una aplicación",
|
||||
"noAPKFound": "No se ha encontrado el paquete de instalación APK",
|
||||
"noVersionDetection": "Sin detección de versiones",
|
||||
"categorize": "Catogorizar",
|
||||
"categories": "Categorías",
|
||||
"category": "Categoría",
|
||||
"noCategory": "Sin categoría",
|
||||
"noCategories": "Sin categorías",
|
||||
"deleteCategoriesQuestion": "¿Eliminar categorías?",
|
||||
"categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas se marcarán como «Sin categoría».",
|
||||
"addCategory": "Añadir categoría",
|
||||
"label": "Nombre",
|
||||
"language": "Idioma",
|
||||
"copiedToClipboard": "Copiado en el portapapeles",
|
||||
"storagePermissionDenied": "Permiso de almacenamiento rechazado",
|
||||
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaciones seleccionadas.",
|
||||
"filterAPKsByRegEx": "Filtrar por APK",
|
||||
"removeFromObtainium": "Eliminar de Obtainium",
|
||||
"uninstallFromDevice": "Desinstalar del dispositivo",
|
||||
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
|
||||
"releaseDateAsVersion": "Por fecha de publicación",
|
||||
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
|
||||
"changes": "Cambios",
|
||||
"releaseDate": "Fecha de publicación",
|
||||
"importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
|
||||
"versionDetectionExplanation": "Conciliar la cadena de versión con la versión detectada desde el sistema operativo",
|
||||
"versionDetection": "Detección de versiones",
|
||||
"standardVersionDetection": "Por versión",
|
||||
"groupByCategory": "Agrupar por categoría",
|
||||
"autoApkFilterByArch": "Filtrar APK por arquitectura del procesador (si es posible)",
|
||||
"overrideSource": "Forzar desde la fuente",
|
||||
"dontShowAgain": "No mostrar de nuevo",
|
||||
"dontShowTrackOnlyWarnings": "No mostrar avisos sobre apps en 'solo seguimiento'",
|
||||
"dontShowAPKOriginWarnings": "No mostrar avisos sobre las fuentes de las APKs",
|
||||
"moveNonInstalledAppsToBottom": "Mover Apps no instaladas al final",
|
||||
"gitlabPATLabel": "Token de acceso personal a GitLab",
|
||||
"about": "Acerca",
|
||||
"requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en ajustes)",
|
||||
"checkOnStart": "Comprobar actualizaciones al inicio",
|
||||
"tryInferAppIdFromCode": "Intentar deducir el ID de la app por el código fuente",
|
||||
"removeOnExternalUninstall": "Auto eliminar apps desinstaladas externamente",
|
||||
"pickHighestVersionCode": "Auto selección de versión superior del paquete APK",
|
||||
"checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la app",
|
||||
"disablePageTransitions": "Deshabilitar animaciones de transición",
|
||||
"reversePageTransitions": "Invertir animaciones de transición",
|
||||
"minStarCount": "Número Mínimo de Estrellas",
|
||||
"addInfoBelow": "Añadir esta información debajo.",
|
||||
"addInfoInSettings": "Puede añadir esta información en Ajustes.",
|
||||
"githubSourceNote": "La limitación de velocidad de GitHub puede evitarse con un 'token de acceso personal'.",
|
||||
"sortByLastLinkSegment": "Ordenar sólo por el último segmento del enlace",
|
||||
"filterReleaseNotesByRegEx": "Filtrar por notas de versión (release notes)",
|
||||
"customLinkFilterRegex": "Filtro personalizado de enlace APK (por defecto '.apk$')",
|
||||
"appsPossiblyUpdated": "Actualización de apps intentada",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifica al usuario que las actualizaciones en segundo plano podrían haberse realizado para una o más aplicaciones",
|
||||
"xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
|
||||
"enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
|
||||
"backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
|
||||
"backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede comprobar con Obtainium abierto.",
|
||||
"verifyLatestTag": "Comprobar la etiqueta 'Latest'",
|
||||
"intermediateLinkRegex": "Filtrar por enlace 'intermedio' para visitar primero",
|
||||
"filterByLinkText": "Filtrar enlaces por texto del enlace",
|
||||
"intermediateLinkNotFound": "Enlace intermedio no encontrado",
|
||||
"intermediateLink": "Enlace intermedio",
|
||||
"exemptFromBackgroundUpdates": "Exenta de actualizciones en segundo plano (si están habilitadas)",
|
||||
"bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto selección del paquete APK con versión más reciente",
|
||||
"versionExtractionRegEx": "Versión de extracción regex",
|
||||
"matchGroupToUse": "Grupo a usar para versión de extracción regex",
|
||||
"highlightTouchTargets": "Resaltar objetivos menos obvios",
|
||||
"pickExportDir": "Directorio para exportar",
|
||||
"autoExportOnChanges": "Auto exportar cuando haya cambios",
|
||||
"includeSettings": "Incluir ajustes",
|
||||
"filterVersionsByRegEx": "Filtrar por versiones",
|
||||
"trySelectingSuggestedVersionCode": "Pruebe seleccionando la versión del paquete APK sugerida",
|
||||
"dontSortReleasesList": "Mantener el order de publicación de la fuente original",
|
||||
"reverseSort": "Orden inverso",
|
||||
"takeFirstLink": "Usar primer enlace",
|
||||
"skipSort": "Omitir orden",
|
||||
"debugMenu": "Menu Depurar",
|
||||
"bgTaskStarted": "Iniciada tarea en segundo plano; revise los registros.",
|
||||
"runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
|
||||
"versionExtractWholePage": "Aplicar la versión de extracción regex a la página entera",
|
||||
"installing": "Instalando",
|
||||
"skipUpdateNotifications": "No notificar sobre actualizaciones",
|
||||
"updatesAvailableNotifChannel": "Actualizaciones disponibles",
|
||||
"appsUpdatedNotifChannel": "Aplicaciones actualizadas",
|
||||
"appsPossiblyUpdatedNotifChannel": "Se ha intentado actualizar la aplicación",
|
||||
"errorCheckingUpdatesNotifChannel": "Error buscando actualizaciones",
|
||||
"appsRemovedNotifChannel": "Aplicaciones eliminadas",
|
||||
"downloadingXNotifChannel": "Descargando {}",
|
||||
"completeAppInstallationNotifChannel": "Instalación completada",
|
||||
"checkingForUpdatesNotifChannel": "Buscando actualizaciones",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Comprobar actualizaciones solo para apps instaladas o en seguimiento",
|
||||
"supportFixedAPKURL": "Soporte para URLs fijas de APK",
|
||||
"selectX": "Selecciona {}",
|
||||
"parallelDownloads": "Permitir descargas paralelas",
|
||||
"useShizuku": "Utilice Shizuku o Sui para instalar",
|
||||
"shizukuBinderNotFound": "Shizuku no funciona",
|
||||
"shizukuOld": "Versión antigua de Shizuku (<11) - actualícela",
|
||||
"shizukuOldAndroidWithADB": "Shizuku corriendo en Android < 8.1 con ADB - actualiza Android o usa Sui en su lugar",
|
||||
"shizukuPretendToBeGooglePlay": "Establecer Google Play como fuente de instalación (si se utiliza Shizuku)",
|
||||
"useSystemFont": "Usar fuente del sistema",
|
||||
"useVersionCodeAsOSVersion": "Usar la versión de la aplicación como versión detectada por el sistema operativo",
|
||||
"requestHeader": "Encabezado de solicitud",
|
||||
"useLatestAssetDateAsReleaseDate": "Usar la última carga de recursos como fecha de lanzamiento",
|
||||
"defaultPseudoVersioningMethod": "Método de pseudoversionado predeterminado",
|
||||
"partialAPKHash": "Hash de APK parcial",
|
||||
"APKLinkHash": "Hash de enlace APK",
|
||||
"directAPKLink": "Enlace APK directo",
|
||||
"pseudoVersionInUse": "Se está utilizando una pseudoversión",
|
||||
"installed": "Instalado",
|
||||
"latest": "Versión más reciente",
|
||||
"invertRegEx": "Invertir expresión regular",
|
||||
"note": "Nota",
|
||||
"selfHostedNote": "El desplegable «{}» puede utilizarse para acceder a instancias autoalojadas/personalizadas de cualquier fuente.",
|
||||
"badDownload": "No se ha podido analizar el APK (incompatible o descarga parcial)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Compartir nuevas aplicaciones con AppVerifier (si está disponible)",
|
||||
"appVerifierInstructionToast": "Comparta con AppVerifier y vuelva aquí cuando esté listo.",
|
||||
"removeAppQuestion": {
|
||||
"one": "¿Eliminar aplicación?",
|
||||
"other": "¿Eliminar aplicaciones?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Muchas peticiones (limitado); pruebe de nuevo en {} minuto",
|
||||
"other": "Muchas peticiones (limitado); pruebe de nuevo en {} minutos"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}; se volverá a probar en {} minuto",
|
||||
"other": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}; se volverá a probar en {} minutos"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualización; se le notificará si es necesario",
|
||||
"other": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualizaciones; se le notificará si es necesario"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} Aplicación",
|
||||
"other": "{} Aplicaciones"
|
||||
},
|
||||
"url": {
|
||||
"uno": "{} URL",
|
||||
"otro": "{} URL"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} minuto",
|
||||
"other": "{} minutos"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} hora",
|
||||
"other": "{} horas"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} día",
|
||||
"other": "{} días"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Eliminado {n} registro (previo a = {before}, posterior a = {after})",
|
||||
"other": "Eliminados {n} registros (previos a = {before}, posteriores a = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} y 1 aplicación más tiene actualizaciones.",
|
||||
"other": "{} y {} aplicaciones más tienen actualizaciones."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} y 1 aplicación más se han actualizado.",
|
||||
"other": "{} y {} aplicaciones más se han actualizado."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Error al actualizar {} y 1 aplicación más.",
|
||||
"other": "No se han podido actualizar {} y {} aplicaciones más."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} y 1 aplicación más podría haber sido actualizada.",
|
||||
"other": "{} y {} aplicaciones más podrían haber sido actualizadas."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,7 @@
|
||||
"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": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
||||
@@ -31,6 +22,9 @@
|
||||
"requiredInBrackets": "(ضروری)",
|
||||
"dropdownNoOptsError": "خطا: کشویی باید حداقل یک گزینه داشته باشد",
|
||||
"colour": "رنگ",
|
||||
"standard": "Standard",
|
||||
"custom": "Custom",
|
||||
"useMaterialYou": "Use Material You",
|
||||
"githubStarredRepos": "مخازن ستاره دار گیتهاب",
|
||||
"uname": "نام کاربری",
|
||||
"wrongArgNum": "تعداد آرگومان های ارائه شده اشتباه است",
|
||||
@@ -50,7 +44,7 @@
|
||||
"searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
|
||||
"search": "جستجو کردن",
|
||||
"additionalOptsFor": "گزینه های اضافی برای {}",
|
||||
"supportedSourcesBelow": "منابع پشتیبانی شده:",
|
||||
"supportedSources": "منابع پشتیبانی شده",
|
||||
"trackOnlyInBrackets": "«فقط ردیابی»",
|
||||
"searchableInBrackets": "(قابل جستجو)",
|
||||
"appsString": "برنامه ها",
|
||||
@@ -60,11 +54,10 @@
|
||||
"percentProgress": "پیش رفتن: {}%",
|
||||
"pleaseWait": "لطفا صبر کنید",
|
||||
"updateAvailable": "بروزرسانی در دسترس",
|
||||
"estimateInBracketsShort": "(تخمین)",
|
||||
"notInstalled": "نصب نشده",
|
||||
"estimateInBrackets": "(تخمین زدن)",
|
||||
"pseudoVersion": "نسخه شبه",
|
||||
"selectAll": "انتخاب همه",
|
||||
"deselectN": "لغو انتخاب {}",
|
||||
"deselectX": "لغو انتخاب {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف میشود اما روی دستگاه نصب میماند.",
|
||||
"removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟",
|
||||
"removeSelectedApps": "حذف برنامه های انتخاب شده",
|
||||
@@ -82,37 +75,37 @@
|
||||
"unpinFromTop": "برداشتن پین از بالا",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "وضعیت نصب برنامههای انتخابی بازنشانی شود؟",
|
||||
"installStatusOfXWillBeResetExplanation": "وضعیت نصب برنامههای انتخابشده بازنشانی میشود.\n\nاگر نسخه برنامه نشاندادهشده در Obtainium به دلیل بهروزرسانیهای ناموفق یا مشکلات دیگر نادرست باشد، میتواند کمک کند.",
|
||||
"customLinkMessage": "این پیوندها بر روی دستگاه هایی با Obtainium نصب شده کار می کنند",
|
||||
"shareAppConfigLinks": "اشتراک گذاری پیکربندی برنامه به عنوان پیوند HTML",
|
||||
"shareSelectedAppURLs": "اشتراک گذاری آدرس اینترنتی برنامه های انتخاب شده",
|
||||
"resetInstallStatus": "بازنشانی وضعیت نصب",
|
||||
"more": "بیشتر",
|
||||
"removeOutdatedFilter": "فیلتر برنامه قدیمی را حذف کنید",
|
||||
"showOutdatedOnly": "فقط برنامه های قدیمی را نشان دهید",
|
||||
"filter": "فیلتر",
|
||||
"filterActive": "فیلتر *",
|
||||
"filterApps": "فیلتر کردن برنامه ها",
|
||||
"appName": "نام برنامه",
|
||||
"author": "سازنده",
|
||||
"upToDateApps": "برنامه های به روز",
|
||||
"nonInstalledApps": "برنامه های نصب نشده",
|
||||
"importExport": "وادر کردن/صادر کردن",
|
||||
"importExport": "درون ریزی/برون ریزی",
|
||||
"settings": "تنظیمات",
|
||||
"exportedTo": "صادر کردن به{}",
|
||||
"exportedTo": "برون ریزی به{}",
|
||||
"obtainiumExport": "صادرکردن Obtainium",
|
||||
"invalidInput": "ورودی نامعتبر",
|
||||
"importedX": "وارد شده {}",
|
||||
"obtainiumImport": "واردکردن Obtainium",
|
||||
"importFromURLList": "وارد کردن از فهرست آدرس اینترنتی",
|
||||
"importFromURLList": "درون ریزی از فهرست آدرس اینترنتی",
|
||||
"searchQuery": "جستجوی سوال",
|
||||
"appURLList": "فهرست آدرس اینترنتی برنامه",
|
||||
"line": "خط",
|
||||
"searchX": "جستجو {}",
|
||||
"noResults": "نتیجه ای پیدا نشد",
|
||||
"importX": "وارد کردن {}",
|
||||
"importedAppsIdDisclaimer": "ممکن است برنامههای وارد شده به اشتباه بهعنوان \"نصب نشده\" نشان داده شوند.\nبرای رفع این مشکل، آنها را دوباره از طریق Obtainium نصب کنید.\nاین نباید روی دادههای برنامه تأثیر بگذارد.\n\nفقط بر روی آدرس اینترنتی و روشهای وارد کردن شخص ثالث تأثیر میگذارد.",
|
||||
"importErrors": "خطاهای وارد کردن",
|
||||
"importX": "درون ریزی {}",
|
||||
"importedAppsIdDisclaimer": "ممکن است برنامههای وارد شده به اشتباه بهعنوان \"نصب نشده\" نشان داده شوند.\nبرای رفع این مشکل، آنها را دوباره از طریق Obtainium نصب کنید.\nاین نباید روی دادههای برنامه تأثیر بگذارد.\n\nفقط بر روی آدرس اینترنتی و روشهای درون ریزی شخص ثالث تأثیر میگذارد.",
|
||||
"importErrors": "خطاهای درون ریزی",
|
||||
"importedXOfYApps": "{} از {} برنامه وارد شد.",
|
||||
"followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
|
||||
"okay": "باشه",
|
||||
"selectURL": "آدرس اینترنتی انتخاب شده",
|
||||
"selectURLs": "آدرس های اینترنتی انتخاب شده",
|
||||
"pick": "انتخاب",
|
||||
@@ -120,9 +113,7 @@
|
||||
"dark": "تاریک",
|
||||
"light": "روشن",
|
||||
"followSystem": "هماهنگ با سیستم",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "استفاده از تم تیره سیاه خالص",
|
||||
"appSortBy": "مرتب سازی برنامه بر اساس",
|
||||
"authorName": "سازنده/اسم",
|
||||
"nameAuthor": "اسم/سازنده",
|
||||
@@ -143,7 +134,7 @@
|
||||
"close": "بستن",
|
||||
"share": "اشتراک گذاری",
|
||||
"appNotFound": "برنامه پیدا نشد",
|
||||
"obtainiumExportHyphenatedLowercase": "صادر کردن-obtainium",
|
||||
"obtainiumExportHyphenatedLowercase": "برون ریزی-obtainium",
|
||||
"pickAnAPK": "یک APK انتخاب کنید",
|
||||
"appHasMoreThanOnePackage": "{} بیش از یک بسته دارد:",
|
||||
"deviceSupportsXArch": "دستگاه شما از معماری پردازنده {} پشتیبانی میکند",
|
||||
@@ -155,8 +146,10 @@
|
||||
"noNewUpdates": "به روز رسانی جدیدی وجود ندارد.",
|
||||
"xHasAnUpdate": "{} یک به روز رسانی دارد.",
|
||||
"appsUpdated": "برنامه ها به روز شدند",
|
||||
"appsNotUpdated": "Failed to update applications",
|
||||
"appsUpdatedNotifDescription": "به کاربر اطلاع می دهد که به روز رسانی یک یا چند برنامه در پس زمینه اعمال شده است",
|
||||
"xWasUpdatedToY": "{} به {} به روز شد.",
|
||||
"xWasNotUpdatedToY": "Failed to update {} to {}.",
|
||||
"errorCheckingUpdates": "خطا در بررسی بهروزرسانیها",
|
||||
"errorCheckingUpdatesNotifDescription": "اعلانی که وقتی بررسی بهروزرسانی پسزمینه ناموفق است نشان میدهد",
|
||||
"appsRemoved": "برنامه ها حذف شدند",
|
||||
@@ -179,14 +172,14 @@
|
||||
"lastUpdateCheckX": "بررسی آخرین بهروزرسانی: {}",
|
||||
"remove": "حذف",
|
||||
"yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده",
|
||||
"fdroid": "F-Droid",
|
||||
"fdroid": "F-Droid Official",
|
||||
"appIdOrName": "شناسه یا نام برنامه",
|
||||
"appId": "شناسه برنامه",
|
||||
"appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد",
|
||||
"reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد",
|
||||
"fdroidThirdPartyRepo": "مخازن شخص ثالث F-Droid",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"steamMobile": "استیم موبایل",
|
||||
"steamChat": "چت استیم",
|
||||
"install": "نصب",
|
||||
"markInstalled": "علامت گذاری به عنوان نصب شده",
|
||||
"update": "به روز رسانی",
|
||||
@@ -195,6 +188,9 @@
|
||||
"disableVersionDetection": "غیرفعال کردن تشخیص نسخه",
|
||||
"noVersionDetectionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند.",
|
||||
"downloadingX": "در حال دانلود {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "کاربر را از پیشرفت دانلود یک برنامه مطلع می کند",
|
||||
"noAPKFound": "APK پیدا نشد فایل",
|
||||
"noVersionDetection": "بدون تشخیص نسخه",
|
||||
@@ -219,11 +215,100 @@
|
||||
"releaseDateAsVersionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند، اما تاریخ انتشار در دسترس است.",
|
||||
"changes": "تغییرات",
|
||||
"releaseDate": "تاریخ انتشار",
|
||||
"importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)",
|
||||
"importFromURLsInFile": "درون ریزی از آدرس های اینترنتی موجود در فایل (مانند OPML)",
|
||||
"versionDetectionExplanation": "تطبیق رشته نسخه با نسخه شناسایی شده از سیستم عامل",
|
||||
"versionDetection": "تشخیص نسخه",
|
||||
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
||||
"groupByCategory": "گروه بر اساس دسته",
|
||||
"autoApkFilterByArch": "در صورت امکان سعی کنید APKها را بر اساس معماری CPU فیلتر کنید",
|
||||
"overrideSource": "نادیده گرفتن منبع",
|
||||
"dontShowAgain": "دوباره این را نشان نده",
|
||||
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
|
||||
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
|
||||
"moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
|
||||
"gitlabPATLabel": "رمز دسترسی شخصی GitLab",
|
||||
"about": "درباره",
|
||||
"requiresCredentialsInSettings": "{}: این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
|
||||
"checkOnStart": "بررسی در شروع",
|
||||
"tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید",
|
||||
"removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی",
|
||||
"pickHighestVersionCode": "انتخاب خودکار بالاترین کد نسخه APK",
|
||||
"checkUpdateOnDetailPage": "برای باز کردن صفحه جزئیات برنامه، بهروزرسانیها را بررسی کنید",
|
||||
"disablePageTransitions": "غیرفعال کردن انیمیشن های انتقال صفحه",
|
||||
"reversePageTransitions": "انیمیشن های انتقال معکوس صفحه",
|
||||
"minStarCount": "حداقل تعداد ستاره",
|
||||
"addInfoBelow": "این اطلاعات را در زیر اضافه کنید",
|
||||
"addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
|
||||
"githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
|
||||
"sortByLastLinkSegment": "فقط بر اساس آخرین بخش پیوند مرتب کنید",
|
||||
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
|
||||
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
|
||||
"appsPossiblyUpdated": "بهروزرسانی برنامه انجام شد",
|
||||
"appsPossiblyUpdatedNotifDescription": "به کاربر اطلاع میدهد که بهروزرسانیهای یک یا چند برنامه به طور بالقوه در پسزمینه اعمال شده است",
|
||||
"xWasPossiblyUpdatedToY": "ممکن است {} به {} به روز شده باشد.",
|
||||
"enableBackgroundUpdates": "به روز رسانی پس زمینه را فعال کنید",
|
||||
"backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
|
||||
"backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
|
||||
"verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
|
||||
"intermediateLinkRegex": "برای بازدید از پیوند «میانگین» فیلتر کنید",
|
||||
"filterByLinkText": "لینک ها را بر اساس متن پیوند فیلتر کنید",
|
||||
"intermediateLinkNotFound": "لینک میانی پیدا نشد",
|
||||
"intermediateLink": "پیوند میانی",
|
||||
"exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
|
||||
"bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
|
||||
"autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
|
||||
"versionExtractionRegEx": "نسخه استخراج RegEx",
|
||||
"matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
|
||||
"highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
|
||||
"pickExportDir": "فهرست برون ریزی را انتخاب کنید",
|
||||
"autoExportOnChanges": "برون ریزی خودکار تغییرات",
|
||||
"includeSettings": "شامل تنظیمات",
|
||||
"filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
|
||||
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
|
||||
"dontSortReleasesList": "حفظ سفارش انتشار از API",
|
||||
"reverseSort": "مرتب سازی معکوس",
|
||||
"takeFirstLink": "لینک اول را بگیرید",
|
||||
"skipSort": "از مرتب سازی صرف نظر کنید",
|
||||
"debugMenu": "منوی اشکال زدایی",
|
||||
"bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
|
||||
"runBgCheckNow": "اکنون بهروزرسانی پسزمینه را بررسی کنید",
|
||||
"versionExtractWholePage": "نسخه Extraction Regex را در کل صفحه اعمال کنید",
|
||||
"installing": "در حال نصب",
|
||||
"skipUpdateNotifications": "رد شدن از اعلان های به روز رسانی",
|
||||
"updatesAvailableNotifChannel": "بروزرسانی در دسترس ",
|
||||
"appsUpdatedNotifChannel": "برنامه ها به روز شدند",
|
||||
"appsPossiblyUpdatedNotifChannel": "بهروزرسانی برنامه انجام شد",
|
||||
"errorCheckingUpdatesNotifChannel": "خطا در بررسی بهروزرسانیها",
|
||||
"appsRemovedNotifChannel": "برنامه ها حذف شدند",
|
||||
"downloadingXNotifChannel": "در حال دانلود {}",
|
||||
"completeAppInstallationNotifChannel": "نصب کامل برنامه",
|
||||
"checkingForUpdatesNotifChannel": "بررسی بهروزرسانیها",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
|
||||
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
|
||||
"selectX": "انتخاب کنید {}",
|
||||
"parallelDownloads": "اجازه دانلود موازی",
|
||||
"useShizuku": "Use Shizuku or Sui to install",
|
||||
"shizukuBinderNotFound": "Shizuku در حال اجرا نیست",
|
||||
"shizukuOld": "Old Shizuku version (<11) - update it",
|
||||
"shizukuOldAndroidWithADB": "Shizuku running on Android < 8.1 with ADB - update Android or use Sui instead",
|
||||
"shizukuPretendToBeGooglePlay": "Set Google Play as the installation source (if Shizuku is used)",
|
||||
"useSystemFont": "استفاده از فونت سیستم",
|
||||
"useVersionCodeAsOSVersion": "استفاده کد نسخه برنامه به جای نسخه شناسایی شده توسط سیستم عامل استفاده کنید",
|
||||
"requestHeader": "درخواست سطر بالایی",
|
||||
"useLatestAssetDateAsReleaseDate": "استفاده از آخرین بارگذاری دارایی به عنوان تاریخ انتشار",
|
||||
"defaultPseudoVersioningMethod": "روش پیش فرض شبه نسخه",
|
||||
"partialAPKHash": "هش جزئی APK",
|
||||
"APKLinkHash": "هش پیوند APK",
|
||||
"directAPKLink": "لینک مستقیم APK",
|
||||
"pseudoVersionInUse": "یک نسخه شبه در حال استفاده است",
|
||||
"installed": "نصب شده است",
|
||||
"latest": "آخرین",
|
||||
"invertRegEx": "معکوس کردن عبارت منظم",
|
||||
"note": "Note",
|
||||
"selfHostedNote": "The \"{}\" dropdown can be used to reach self-hosted/custom instances of any source.",
|
||||
"badDownload": "The APK could not be parsed (incompatible or partial download)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)",
|
||||
"appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.",
|
||||
"removeAppQuestion": {
|
||||
"one": "برنامه حذف شود؟",
|
||||
"other": "برنامه ها حذف شوند؟"
|
||||
@@ -271,5 +356,17 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} و 1 برنامه دیگر به روز شدند.",
|
||||
"other": "{} و {} برنامه دیگر به روز شدند."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Failed to update {} and 1 more app.",
|
||||
"other": "Failed to update {} and {} more apps."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} و 1 برنامه دیگر ممکن است به روز شده باشند.",
|
||||
"other": "ممکن است {} و {} برنامه های دیگر به روز شده باشند."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"invalidURLForSource": "URL d'application {} invalide",
|
||||
"noReleaseFound": "Impossible de trouver une version appropriée",
|
||||
"noVersionFound": "Impossible de déterminer la version de la version",
|
||||
"noReleaseFound": "Impossible de trouver une version adaptée",
|
||||
"noVersionFound": "Impossible de déterminer la variante 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",
|
||||
@@ -9,18 +9,9 @@
|
||||
"placeholder": "Espace réservé",
|
||||
"someErrors": "Des erreurs se sont produites",
|
||||
"unexpectedError": "Erreur inattendue",
|
||||
"ok": "Okay",
|
||||
"ok": "D'accord",
|
||||
"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",
|
||||
@@ -31,6 +22,9 @@
|
||||
"requiredInBrackets": "(Requis)",
|
||||
"dropdownNoOptsError": "ERREUR : LE DÉROULEMENT DOIT AVOIR AU MOINS UNE OPT",
|
||||
"colour": "Couleur",
|
||||
"standard": "Standard",
|
||||
"custom": "Sur mesure",
|
||||
"useMaterialYou": "Utiliser le matériel que vous",
|
||||
"githubStarredRepos": "Dépôts étoilés GitHub",
|
||||
"uname": "Nom d'utilisateur",
|
||||
"wrongArgNum": "Mauvais nombre d'arguments fournis",
|
||||
@@ -46,13 +40,13 @@
|
||||
"addApp": "Ajouter une application",
|
||||
"appSourceURL": "URL de la source de l'application",
|
||||
"error": "Erreur",
|
||||
"add": "Ajoutée",
|
||||
"add": "Ajouter",
|
||||
"searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)",
|
||||
"search": "Rechercher",
|
||||
"additionalOptsFor": "Options supplémentaires pour {}",
|
||||
"supportedSourcesBelow": "Sources prises en charge :",
|
||||
"supportedSources": "Sources prises en charge ",
|
||||
"trackOnlyInBrackets": "(Suivi uniquement)",
|
||||
"searchableInBrackets": "(Recherchable)",
|
||||
"searchableInBrackets": "(Intérrogeable)",
|
||||
"appsString": "Applications",
|
||||
"noApps": "Aucune application",
|
||||
"noAppsForFilter": "Aucune application pour le filtre",
|
||||
@@ -60,35 +54,35 @@
|
||||
"percentProgress": "Progrès: {}%",
|
||||
"pleaseWait": "Veuillez patienter",
|
||||
"updateAvailable": "Mise à jour disponible",
|
||||
"estimateInBracketsShort": "(Est.)",
|
||||
"notInstalled": "Pas installé",
|
||||
"estimateInBrackets": "(Estimation)",
|
||||
"notInstalled": "Non installé",
|
||||
"pseudoVersion": "pseudo-version",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"deselectN": "Déselectionner {}",
|
||||
"deselectX": "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",
|
||||
"markXTrackOnlyAsUpdated": "Marquer {}\n(Suivi uniquement)\n comme 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 ?",
|
||||
"markXSelectedAppsAsUpdated": "Marquer {} les applications sélectionnées comme étant à jour ?",
|
||||
"no": "Non",
|
||||
"yes": "Oui",
|
||||
"markSelectedAppsUpdated": "Marquer les applications sélectionnées comme mises à jour",
|
||||
"markSelectedAppsUpdated": "Marquer les applications sélectionnées comme étant à 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.",
|
||||
"unpinFromTop": "Désépingler du haut",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Réinitialiser le statu d'installation des applications sélectionnées ?",
|
||||
"installStatusOfXWillBeResetExplanation": "Le statu 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.",
|
||||
"customLinkMessage": "Ces liens fonctionnent sur les appareils sur lesquels Obtainium est installé",
|
||||
"shareAppConfigLinks": "Partager la configuration de l'application sous forme de lien HTML",
|
||||
"shareSelectedAppURLs": "Partager les URL d'application sélectionnées",
|
||||
"resetInstallStatus": "Réinitialiser le statut d'installation",
|
||||
"resetInstallStatus": "Réinitialiser le statu 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",
|
||||
@@ -97,12 +91,12 @@
|
||||
"importExport": "Importer/Exporter",
|
||||
"settings": "Paramètres",
|
||||
"exportedTo": "Exporté vers {}",
|
||||
"obtainiumExport": "Exportation d'Obtainium",
|
||||
"obtainiumExport": "Exporter d'Obtainium",
|
||||
"invalidInput": "Entrée invalide",
|
||||
"importedX": "Importé {}",
|
||||
"obtainiumImport": "Importation d'Obtainium",
|
||||
"obtainiumImport": "Importer d'Obtainium",
|
||||
"importFromURLList": "Importer à partir de la liste d'URL",
|
||||
"searchQuery": "Requête de recherche",
|
||||
"searchQuery": "Requête",
|
||||
"appURLList": "Liste d'URL d'application",
|
||||
"line": "Queue",
|
||||
"searchX": "Rechercher {}",
|
||||
@@ -112,7 +106,6 @@
|
||||
"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",
|
||||
@@ -120,16 +113,14 @@
|
||||
"dark": "Sombre",
|
||||
"light": "Clair",
|
||||
"followSystem": "Suivre le système",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "Utilisez le thème noir pur",
|
||||
"appSortBy": "Applications triées par",
|
||||
"authorName": "Auteur/Nom",
|
||||
"nameAuthor": "Nom/Auteur",
|
||||
"asAdded": "Comme ajouté",
|
||||
"appSortOrder": "Ordre de tri des applications",
|
||||
"ascending": "Ascendant",
|
||||
"descending": "Descendanr",
|
||||
"descending": "Descendant",
|
||||
"bgUpdateCheckInterval": "Intervalle de vérification des mises à jour en arrière-plan",
|
||||
"neverManualOnly": "Jamais - Manuel uniquement",
|
||||
"appearance": "Apparence",
|
||||
@@ -143,20 +134,22 @@
|
||||
"close": "Fermer",
|
||||
"share": "Partager",
|
||||
"appNotFound": "Application introuvable",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"obtainiumExportHyphenatedLowercase": "exportation d'Obtainium",
|
||||
"pickAnAPK": "Choisissez un APK",
|
||||
"appHasMoreThanOnePackage": "{} a plus d'un paquet :",
|
||||
"deviceSupportsXArch": "Votre appareil prend en charge l'architecture de processeur {}.",
|
||||
"deviceSupportsXArch": "Votre appareil prend en charge l'architecture CPU {}.",
|
||||
"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?",
|
||||
"sourceIsXButPackageFromYPrompt": "La source de l'application est '{}' mais la version du paquet 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",
|
||||
"appsNotUpdated": "Échec de la mise à jour des applications",
|
||||
"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 {}.",
|
||||
"xWasNotUpdatedToY": "Échec de la mise à jour de {} vers {}.",
|
||||
"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",
|
||||
@@ -179,51 +172,143 @@
|
||||
"lastUpdateCheckX": "Vérification de la dernière mise à jour : {}",
|
||||
"remove": "Retirer",
|
||||
"yesMarkUpdated": "Oui, marquer comme mis à jour",
|
||||
"fdroid": "F-Droid",
|
||||
"fdroid": "F-Droid Officiel",
|
||||
"appIdOrName": "ID ou nom de l'application",
|
||||
"appId": "ID de l'application",
|
||||
"appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
|
||||
"reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications",
|
||||
"fdroidThirdPartyRepo": "Dépôt tiers F-Droid",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"steamMobile": "Vapeur Mobile",
|
||||
"steamChat": "Chat sur Steam",
|
||||
"install": "Installer",
|
||||
"markInstalled": "Marquer installée",
|
||||
"update": "Mettre à jour",
|
||||
"markUpdated": "Marquer à jour",
|
||||
"additionalOptions": "Options additionelles",
|
||||
"additionalOptions": "Options additionnelles",
|
||||
"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 {}",
|
||||
"downloadX": "Télécharger {}",
|
||||
"downloadedX": "Téléchargé {}",
|
||||
"releaseAsset": "Actif libéré",
|
||||
"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",
|
||||
"noVersionDetection": "Aucune de détection de version",
|
||||
"categorize": "Catégoriser",
|
||||
"categories": "Catégories",
|
||||
"category": "Catégorie",
|
||||
"noCategory": "No Category",
|
||||
"noCategory": "Aucune catégorie",
|
||||
"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",
|
||||
"copiedToClipboard": "Copied to Clipboard",
|
||||
"copiedToClipboard": "Copié dans le presse-papier",
|
||||
"storagePermissionDenied": "Autorisation de stockage refusée",
|
||||
"selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.",
|
||||
"selectedCategorizeWarning": "Cela remplacera toutes les catégorie définies 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.",
|
||||
"releaseDateAsVersionExplanation": "Cette option ne doit être utilisée que pour les applications où la détection de version ne fonctionne pas correctement, mais dont une date de sortie est disponible.",
|
||||
"changes": "Changements",
|
||||
"releaseDate": "Date de sortie",
|
||||
"importFromURLsInFile": "Importer à partir d'URL dans un fichier (comme OPML)",
|
||||
"versionDetectionExplanation": "Réconcilier la chaîne de version avec la version détectée à partir du système d'exploitation",
|
||||
"versionDetection": "Détection des versions",
|
||||
"standardVersionDetection": "Détection de version standard",
|
||||
"groupByCategory": "Group by Category",
|
||||
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
|
||||
"groupByCategory": "Regrouper par catégorie",
|
||||
"autoApkFilterByArch": "Si possible, essayez de filtrer les APK par architecture CPU",
|
||||
"overrideSource": "Remplacer la source",
|
||||
"dontShowAgain": "Ne plus montrer",
|
||||
"dontShowTrackOnlyWarnings": "Ne pas afficher l'avertissement 'Track-Only'",
|
||||
"dontShowAPKOriginWarnings": "Ne pas afficher les avertissements sur l'origine de l'APK",
|
||||
"moveNonInstalledAppsToBottom": "Déplacer les applications non installées vers le bas de la vue Applications",
|
||||
"gitlabPATLabel": "Jeton d'accès personnel GitLab",
|
||||
"about": "À propos de",
|
||||
"requiresCredentialsInSettings": "{}: Cela nécessite des identifiants supplémentaires (dans Paramètres)",
|
||||
"checkOnStart": "Vérifier les mises à jour au démarrage",
|
||||
"tryInferAppIdFromCode": "Essayez de déduire l'ID de l'application à partir du code source",
|
||||
"removeOnExternalUninstall": "Supprimer automatiquement les applications désinstallées en externe",
|
||||
"pickHighestVersionCode": "Sélectionner automatiquement le code de version de l'APK la plus élevée",
|
||||
"checkUpdateOnDetailPage": "Vérifier les mises à jour lors de l'ouverture de la page détaillée d'une application",
|
||||
"disablePageTransitions": "Désactiver les animations de transition de page",
|
||||
"reversePageTransitions": "Inverser les animations de transition de page",
|
||||
"minStarCount": "Nombre minimum d'étoiles",
|
||||
"addInfoBelow": "Ajoutez ces informations ci-dessous.",
|
||||
"addInfoInSettings": "Ajoutez ces informations dans les paramètres.",
|
||||
"githubSourceNote": "La limite de débit GitHub peut être évitée à l'aide d'une clé API.",
|
||||
"sortByLastLinkSegment": "Trier uniquement sur le dernier segment du lien",
|
||||
"filterReleaseNotesByRegEx": "Filtrer les notes de version par expression régulière",
|
||||
"customLinkFilterRegex": "Filtre du lien APK personnalisé par expression régulière (par défaut '.apk$')",
|
||||
"appsPossiblyUpdated": "Tentative de mise à jour de l'application",
|
||||
"appsPossiblyUpdatedNotifDescription": "Avertit l'utilisateur que des mises à jour d'une ou plusieurs applications ont été potentiellement appliquées en arrière-plan",
|
||||
"xWasPossiblyUpdatedToY": "{} a peut-être été mis à jour vers {}.",
|
||||
"enableBackgroundUpdates": "Activer les mises à jour en arrière-plan",
|
||||
"backgroundUpdateReqsExplanation": "Les mises à jour en arrière-plan peuvent ne pas être possibles pour toutes les applications.",
|
||||
"backgroundUpdateLimitsExplanation": "Le succès d'une installation en arrière-plan ne peut être déterminé qu'à l'ouverture d'Obtainium.",
|
||||
"verifyLatestTag": "Vérifiez la balise 'Latest'",
|
||||
"intermediateLinkRegex": " Filtrer un lien \" intermédiaire \" à visiter ",
|
||||
"filterByLinkText": "Filtrer les liens par le texte du lien",
|
||||
"intermediateLinkNotFound": "Lien intermédiaire introuvable",
|
||||
"intermediateLink": "Lien intermédiaire",
|
||||
"exemptFromBackgroundUpdates": "Exempté des mises à jour en arrière-plan (si activé)",
|
||||
"bgUpdatesOnWiFiOnly": "Désactiver les mises à jour en arrière-plan lorsque vous n'êtes pas connecté au WiFi",
|
||||
"autoSelectHighestVersionCode": "Sélection automatique du code de version de l'APK la plus élevée",
|
||||
"versionExtractionRegEx": "Expression régulière d'extraction de version",
|
||||
"matchGroupToUse": "Groupe de correspondance pour l'expression régulière d'extraction de version",
|
||||
"highlightTouchTargets": "Mettre en évidence les cibles tactiles moins évidentes",
|
||||
"pickExportDir": "Choisir le répertoire d'exportation",
|
||||
"autoExportOnChanges": "Exporter automatiquement après modification",
|
||||
"includeSettings": "Inclure les paramètres",
|
||||
"filterVersionsByRegEx": "Filtrer les versions par expression régulière",
|
||||
"trySelectingSuggestedVersionCode": "Essayez de sélectionner le code de la version APK suggérée",
|
||||
"dontSortReleasesList": "Conserver l'ordre des version de l'API",
|
||||
"reverseSort": "Tri inversé",
|
||||
"takeFirstLink": "Prendre le premier lien",
|
||||
"skipSort": "Sauter le tri",
|
||||
"debugMenu": "Menu de débogage",
|
||||
"bgTaskStarted": "Tâche en arrière-plan démarrée - vérifier les journaux.",
|
||||
"runBgCheckNow": "Exécuter maintenant la vérification de la mise à jour en arrière-plan",
|
||||
"versionExtractWholePage": "Appliquer l'expression régulière d'extraction de version sur l'ensemble de la page",
|
||||
"installing": "Installation",
|
||||
"skipUpdateNotifications": "Ignorer les notifications de mise à jour",
|
||||
"updatesAvailableNotifChannel": "Mises à jour disponibles",
|
||||
"appsUpdatedNotifChannel": "Applications mises à jour",
|
||||
"appsPossiblyUpdatedNotifChannel": "Tentative de mise à jour de l'application",
|
||||
"errorCheckingUpdatesNotifChannel": "Erreur lors de la vérification des mises à jour",
|
||||
"appsRemovedNotifChannel": "Applications supprimées",
|
||||
"downloadingXNotifChannel": "Téléchargement {}",
|
||||
"completeAppInstallationNotifChannel": "Installation complète de l'application",
|
||||
"checkingForUpdatesNotifChannel": "Vérification des mises à jour",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Vérifiez uniquement les mises à jour des applications installées et 'Track-Only'",
|
||||
"supportFixedAPKURL": "Prise en charge des URL APK fixes",
|
||||
"selectX": "Sélectionner {}",
|
||||
"parallelDownloads": "Autoriser les téléchargements parallèles",
|
||||
"useShizuku": "Utiliser Shizuku ou Sui pour l'installation",
|
||||
"shizukuBinderNotFound": "Service Shizuku compatible non trouvé",
|
||||
"shizukuOld": "Ancienne version de Shizuku (<11) - la mettre à jour",
|
||||
"shizukuOldAndroidWithADB": "Shizuku fonctionne sur Android < 8.1 avec ADB - mettre à jour Android ou utiliser Sui à la place",
|
||||
"shizukuPretendToBeGooglePlay": "Définir Google Play comme source d'installation (si Shizuku est utilisé)",
|
||||
"useSystemFont": "Utiliser la police du système",
|
||||
"useVersionCodeAsOSVersion": "Utiliser le code de version de l'application comme version détectée par le système d'exploitation",
|
||||
"requestHeader": "En-tête de demande",
|
||||
"useLatestAssetDateAsReleaseDate": "Utiliser le dernier élément téléversé comme date de sortie",
|
||||
"defaultPseudoVersioningMethod": "Méthode de pseudo-version par défaut",
|
||||
"partialAPKHash": "Hash APK partiel",
|
||||
"APKLinkHash": "Hash de lien APK",
|
||||
"directAPKLink": "Lien APK direct",
|
||||
"pseudoVersionInUse": "Une pseudo-version est utilisée",
|
||||
"installed": "Installée",
|
||||
"latest": "Dernier",
|
||||
"invertRegEx": "Inverser l'expression régulière",
|
||||
"note": "Note",
|
||||
"selfHostedNote": "La liste déroulante \"{}\" peut être utilisée pour accéder aux instances auto-hébergées/personnalisées de n'importe quelle source.",
|
||||
"badDownload": "L'APK n'a pas pu être analysé (téléchargement incompatible ou partiel)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Partager les nouvelles applications avec AppVerifier (si disponible)",
|
||||
"appVerifierInstructionToast": "Partagez avec AppVerifier, puis revenez ici lorsque vous êtes prêt.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Supprimer l'application ?",
|
||||
"other": "Supprimer les applications ?"
|
||||
@@ -233,12 +318,12 @@
|
||||
"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"
|
||||
"one": "La vérification de la mise à jour en arrière-plan a rencontré un {}, une nouvelle tentative de vérification sera planifié dans {} minute",
|
||||
"other": "La vérification de la mise à jour en arrière-plan a rencontré un {}, une nouvelle tentative de vérification sera planifié dans {} minute"
|
||||
},
|
||||
"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"
|
||||
"one": "La vérification des mises à jour en arrière-plan a trouvée {} mise à jour - l'utilisateur sera notifié si nécessaire",
|
||||
"other": "La vérification des mises à jour en arrière-plan a trouvé {} mises à jour - l'utilisateur sera notifié si nécessaire"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} Application",
|
||||
@@ -246,10 +331,10 @@
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URLs"
|
||||
"other": "{} URL"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Minute",
|
||||
"one": "{} Minutes",
|
||||
"other": "{} Minutes"
|
||||
},
|
||||
"hour": {
|
||||
@@ -271,5 +356,17 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} et 1 autre application ont été mises à jour.",
|
||||
"other": "{} et {} autres applications ont été mises à jour."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Échec de la mise à jour de {} et d'une autre application.",
|
||||
"other": "Échec de la mise à jour de {} et {} autres applications."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"une": "{} et 1 application supplémentaire ont peut-être été mises à jour.",
|
||||
"other": "{} et {} autres applications peuvent avoir été mises à jour."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,7 @@
|
||||
"unexpectedError": "Váratlan hiba",
|
||||
"ok": "Oké",
|
||||
"and": "és",
|
||||
"startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva",
|
||||
"bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}",
|
||||
"startedActualBGUpdateCheck": "Elkezdődött a tényleges háttérfrissítés ellenőrzése",
|
||||
"bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött",
|
||||
"firstRun": "Ez az Obtainium első futása",
|
||||
"settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása erre: {}",
|
||||
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
||||
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
||||
"githubPATFormat": "felhasználónév:token",
|
||||
"githubPATLinkText": "A GitHub PAT-okról",
|
||||
"githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)",
|
||||
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
|
||||
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
|
||||
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
|
||||
@@ -31,12 +22,15 @@
|
||||
"requiredInBrackets": "(Kötelező)",
|
||||
"dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓHOZ KELL RENDELNI",
|
||||
"colour": "Szín",
|
||||
"standard": "Standard",
|
||||
"custom": "Custom",
|
||||
"useMaterialYou": "Használja az Ön által használt anyagot",
|
||||
"githubStarredRepos": "GitHub Csillagos Repo-k",
|
||||
"uname": "Felh.név",
|
||||
"wrongArgNum": "Rossz számú argumentumot adott meg",
|
||||
"xIsTrackOnly": "A(z) {} csak nyomonkövethető",
|
||||
"source": "Forrás",
|
||||
"app": "App",
|
||||
"app": "Alkalmazás",
|
||||
"appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.",
|
||||
"youPickedTrackOnly": "A 'Csak követés' opciót választotta.",
|
||||
"trackOnlyAppDescription": "Az alkalmazás frissítéseit nyomon követi, de az Obtainium nem tudja letölteni vagy telepíteni.",
|
||||
@@ -50,7 +44,7 @@
|
||||
"searchSomeSourcesLabel": "Keresés (csak egyes források)",
|
||||
"search": "Keresés",
|
||||
"additionalOptsFor": "További lehetőségek a következőhöz: {}",
|
||||
"supportedSourcesBelow": "Támogatott források:",
|
||||
"supportedSources": "Támogatott források",
|
||||
"trackOnlyInBrackets": "(Csak nyomonkövetés)",
|
||||
"searchableInBrackets": "(Kereshető)",
|
||||
"appsString": "Appok",
|
||||
@@ -60,11 +54,10 @@
|
||||
"percentProgress": "Folyamat: {}%",
|
||||
"pleaseWait": "Kis türelmet",
|
||||
"updateAvailable": "Frissítés érhető el",
|
||||
"estimateInBracketsShort": "(Becsült)",
|
||||
"notInstalled": "Nem telepített",
|
||||
"estimateInBrackets": "(Becslés)",
|
||||
"pseudoVersion": "ál-verzió",
|
||||
"selectAll": "Mindet kiválaszt",
|
||||
"deselectN": "Törölje {} kijelölését",
|
||||
"deselectX": "Törölje {} kijelölését",
|
||||
"xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.",
|
||||
"removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
|
||||
"removeSelectedApps": "Távolítsa el a kiválasztott appokat",
|
||||
@@ -82,37 +75,37 @@
|
||||
"unpinFromTop": "Eltávolít felülről",
|
||||
"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.",
|
||||
"customLinkMessage": "Ezek a hivatkozások a telepített Obtainium-mal rendelkező eszközökön működnek",
|
||||
"shareAppConfigLinks": "App konfiguráció megosztása HTML linkként",
|
||||
"shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit",
|
||||
"resetInstallStatus": "Telepítési állapot visszaállítása",
|
||||
"more": "További",
|
||||
"removeOutdatedFilter": "Távolítsa el az elavult app szűrőt",
|
||||
"showOutdatedOnly": "Csak az elavult appok megjelenítése",
|
||||
"filter": "Szűrő",
|
||||
"filterActive": "Szűrő *",
|
||||
"filterApps": "Appok szűrése",
|
||||
"appName": "App név",
|
||||
"author": "Szerző",
|
||||
"upToDateApps": "Naprakész appok",
|
||||
"nonInstalledApps": "Nem telepített appok",
|
||||
"importExport": "Import/Export",
|
||||
"importExport": "Import Export",
|
||||
"settings": "Beállítások",
|
||||
"exportedTo": "Exportálva ide {}",
|
||||
"obtainiumExport": "Obtainium Export",
|
||||
"obtainiumExport": "Obtainium Adat Exportálás",
|
||||
"invalidInput": "Hibás bemenet",
|
||||
"importedX": "Importálva innen {}",
|
||||
"obtainiumImport": "Obtainium Import",
|
||||
"obtainiumImport": "Obtainium Adat Importálás",
|
||||
"importFromURLList": "Importálás URL listából",
|
||||
"searchQuery": "Keresési lekérdezés",
|
||||
"appURLList": "App URL lista",
|
||||
"line": "Sor",
|
||||
"searchX": "Keresés {}",
|
||||
"noResults": "Nincs találat",
|
||||
"importX": "Import {}",
|
||||
"importX": "Importálás: {}",
|
||||
"importedAppsIdDisclaimer": "Előfordulhat, hogy az importált appok helytelenül \"Nincs telepítve\" jelzéssel jelennek meg.\nA probléma megoldásához telepítse újra őket az Obtainiumon keresztül.\nEz nem érinti az alkalmazásadatokat.\n\nCsak az URL-ekre és a harmadik féltől származó importálási módszerekre vonatkozik..",
|
||||
"importErrors": "Importálási hibák",
|
||||
"importedXOfYApps": "{}/{} app importálva.",
|
||||
"followingURLsHadErrors": "A következő URL-ek hibákat tartalmaztak:",
|
||||
"okay": "Oké",
|
||||
"selectURL": "Válassza ki az URL-t",
|
||||
"selectURLs": "Kiválasztott URL-ek",
|
||||
"pick": "Válasszon",
|
||||
@@ -120,9 +113,7 @@
|
||||
"dark": "Sötét",
|
||||
"light": "Világos",
|
||||
"followSystem": "Rendszer szerint",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "Használjon teljesen fekete sötét témát",
|
||||
"appSortBy": "App rendezés...",
|
||||
"authorName": "Szerző/Név",
|
||||
"nameAuthor": "Név/Szerző",
|
||||
@@ -140,8 +131,8 @@
|
||||
"appSource": "App forrás",
|
||||
"noLogs": "Nincsenek naplók",
|
||||
"appLogs": "App naplók",
|
||||
"close": "Bezár",
|
||||
"share": "Megoszt",
|
||||
"close": "Bezárás",
|
||||
"share": "Megosztás",
|
||||
"appNotFound": "App nem található",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Válasszon egy APK-t",
|
||||
@@ -155,8 +146,10 @@
|
||||
"noNewUpdates": "Nincsenek új frissítések.",
|
||||
"xHasAnUpdate": "A(z) {} frissítést kapott.",
|
||||
"appsUpdated": "Alkalmazások frissítve",
|
||||
"appsNotUpdated": "Nem sikerült frissíteni az alkalmazásokat",
|
||||
"appsUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy/több app frissítése megtörtént a háttérben",
|
||||
"xWasUpdatedToY": "{} frissítve a következőre: {}.",
|
||||
"xWasNotUpdatedToY": "A {} frissítése a {}-ra nem sikerült.",
|
||||
"errorCheckingUpdates": "Hiba a frissítések keresésekor",
|
||||
"errorCheckingUpdatesNotifDescription": "Értesítés, amely akkor jelenik meg, ha a háttérbeli frissítések ellenőrzése sikertelen",
|
||||
"appsRemoved": "Alkalmazások eltávolítva",
|
||||
@@ -179,13 +172,13 @@
|
||||
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
||||
"remove": "Eltávolítás",
|
||||
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
||||
"fdroid": "F-Droid",
|
||||
"fdroid": "F-Droid hivatalos",
|
||||
"appIdOrName": "App ID vagy név",
|
||||
"appId": "Alkalmazásazonosító",
|
||||
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
||||
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
||||
"fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamMobile": "Steam mobil",
|
||||
"steamChat": "Steam Chat",
|
||||
"install": "Telepít",
|
||||
"markInstalled": "Telepítettnek jelöl",
|
||||
@@ -195,6 +188,9 @@
|
||||
"disableVersionDetection": "Verzió érzékelés letiltása",
|
||||
"noVersionDetectionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzióérzékelés nem működik megfelelően.",
|
||||
"downloadingX": "{} letöltés",
|
||||
"downloadX": "Letöltés {}",
|
||||
"downloadedX": "Letöltés {}",
|
||||
"releaseAsset": "Kiadási tartalom",
|
||||
"downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról",
|
||||
"noAPKFound": "Nem található APK",
|
||||
"noVersionDetection": "Nincs verzió érzékelés",
|
||||
@@ -202,12 +198,13 @@
|
||||
"categories": "Kategóriák",
|
||||
"category": "Kategória",
|
||||
"noCategory": "Nincs kategória",
|
||||
"deleteCategoryQuestion": "Törli a kategóriát?",
|
||||
"noCategories": "Nincsenek kategóriák",
|
||||
"deleteCategoriesQuestion": "Törli a kategóriákat?",
|
||||
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
|
||||
"addCategory": "Új kategória",
|
||||
"label": "Címke",
|
||||
"language": "Nyelv",
|
||||
"copiedToClipboard": "Copied to Clipboard",
|
||||
"copiedToClipboard": "Másolva a vágólapra",
|
||||
"storagePermissionDenied": "Tárhely engedély megtagadva",
|
||||
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
|
||||
"filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
|
||||
@@ -219,13 +216,102 @@
|
||||
"changes": "Változtatások",
|
||||
"releaseDate": "Kiadás dátuma",
|
||||
"importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)",
|
||||
"versionDetectionExplanation": "A verzió karakterlánc egyeztetése az OS által észlelt verzióval",
|
||||
"versionDetection": "Verzió érzékelés",
|
||||
"standardVersionDetection": "Alapért. verzió érzékelés",
|
||||
"groupByCategory": "Csoportosítás Kategória alapján",
|
||||
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
|
||||
"overrideSource": "Forrás felülbírálása",
|
||||
"dontShowAgain": "Ne mutassa ezt újra",
|
||||
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
||||
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
|
||||
"moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
|
||||
"gitlabPATLabel": "GitLab személyes hozzáférési token",
|
||||
"about": "Rólunk",
|
||||
"requiresCredentialsInSettings": "{}: Ehhez további hitelesítő adatokra van szükség (a Beállításokban)",
|
||||
"checkOnStart": "Egyszer az alkalmazás indításakor is",
|
||||
"tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból",
|
||||
"removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása",
|
||||
"pickHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
|
||||
"checkUpdateOnDetailPage": "Frissítések keresése az app részleteit tartalmazó oldal megnyitásakor",
|
||||
"disablePageTransitions": "Lap áttűnési animációk letiltása",
|
||||
"reversePageTransitions": "Fordított lap áttűnési animációk",
|
||||
"minStarCount": "Minimális csillag szám",
|
||||
"addInfoBelow": "Adja hozzá ezt az infót alább.",
|
||||
"addInfoInSettings": "Adja hozzá ezt az infót a Beállításokban.",
|
||||
"githubSourceNote": "A GitHub sebességkorlátozás elkerülhető API-kulcs használatával.",
|
||||
"sortByLastLinkSegment": "Rendezés csak a link utolsó szegmense szerint",
|
||||
"filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel",
|
||||
"customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')",
|
||||
"appsPossiblyUpdated": "App frissítési kísérlet",
|
||||
"appsPossiblyUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több alkalmazás frissítése lehetséges a háttérben",
|
||||
"xWasPossiblyUpdatedToY": "{} frissítve lehet erre {}.",
|
||||
"enableBackgroundUpdates": "Frissítések a háttérben",
|
||||
"backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
|
||||
"backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
|
||||
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
|
||||
"intermediateLinkRegex": "Szűrés egy 'köztes' látogatási linkre",
|
||||
"filterByLinkText": "A hivatkozások szűrése linkszöveg alapján",
|
||||
"intermediateLinkNotFound": "Köztes link nem található",
|
||||
"intermediateLink": "Köztes link",
|
||||
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
|
||||
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
|
||||
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
|
||||
"versionExtractionRegEx": "Verzió kibontása reguláris kifejezéssel",
|
||||
"matchGroupToUse": "Párosítsa a csoportot a használathoz",
|
||||
"highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
|
||||
"pickExportDir": "Válassza az Exportálási könyvtárat",
|
||||
"autoExportOnChanges": "Auto-exportálás a változások után",
|
||||
"includeSettings": "Tartalmazza a beállításokat",
|
||||
"filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
|
||||
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
|
||||
"dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",
|
||||
"reverseSort": "Fordított rendezés",
|
||||
"takeFirstLink": "Vegye az első linket",
|
||||
"skipSort": "A válogatás kihagyása",
|
||||
"debugMenu": "Hibakereső menü",
|
||||
"bgTaskStarted": "A háttérfeladat elindult – ellenőrizze a naplókat.",
|
||||
"runBgCheckNow": "Futtassa a Háttérben frissítés ellenőrzését most",
|
||||
"versionExtractWholePage": "Alkalmazza a Version Extraction Regex-et az egész oldalra",
|
||||
"installing": "Telepítés",
|
||||
"skipUpdateNotifications": "A frissítési értesítések kihagyása",
|
||||
"updatesAvailableNotifChannel": "Frissítések érhetők el",
|
||||
"appsUpdatedNotifChannel": "Alkalmazások frissítve",
|
||||
"appsPossiblyUpdatedNotifChannel": "App frissítési kísérlet",
|
||||
"errorCheckingUpdatesNotifChannel": "Hiba a frissítések keresésekor",
|
||||
"appsRemovedNotifChannel": "Alkalmazások eltávolítva",
|
||||
"downloadingXNotifChannel": "{} letöltés",
|
||||
"completeAppInstallationNotifChannel": "Teljes app telepítés",
|
||||
"checkingForUpdatesNotifChannel": "Frissítések keresése",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért",
|
||||
"supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
|
||||
"selectX": "Kiválaszt {}",
|
||||
"parallelDownloads": "Párhuzamos letöltéseket enged",
|
||||
"useShizuku": "Használja Shizuku vagy Sui telepítéséhez",
|
||||
"shizukuBinderNotFound": "A Shizuku nem fut",
|
||||
"shizukuOld": "Régi Shizuku verzió (<11) - frissítsd!",
|
||||
"shizukuOldAndroidWithADB": "Shizuku fut Android < 8.1 ADB-vel - frissítse az Androidot vagy használja a Sui-t helyette",
|
||||
"shizukuPretendToBeGooglePlay": "Állítsa be a Google Play-t telepítési forrásként (ha Shizuku-t használ)",
|
||||
"useSystemFont": "Használja a rendszer betűtípusát",
|
||||
"useVersionCodeAsOSVersion": "Az app verziókód használata a rendszer által észlelt verzióként",
|
||||
"requestHeader": "Kérelem fejléc",
|
||||
"useLatestAssetDateAsReleaseDate": "Használja a legújabb tartalomfeltöltést megjelenési dátumként",
|
||||
"defaultPseudoVersioningMethod": "Alapértelmezett álverziós módszer",
|
||||
"partialAPKHash": "Részleges APK Hash",
|
||||
"APKLinkHash": "APK Link Hash",
|
||||
"directAPKLink": "Közvetlen APK Link",
|
||||
"pseudoVersionInUse": "Egy álverzió van használatban",
|
||||
"installed": "Telepített",
|
||||
"latest": "Legújabb",
|
||||
"invertRegEx": "Invertált reguláris kifejezés",
|
||||
"note": "Megjegyzés:",
|
||||
"selfHostedNote": "A \"{}\" legördülő menü használható bármely forrás saját üzemeltetésű/egyéni példányainak eléréséhez.",
|
||||
"badDownload": "Az APK-t nem lehetett elemezni (inkompatibilis vagy részleges letöltés)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Új alkalmazások megosztása az AppVerifierrel (ha elérhető)",
|
||||
"appVerifierInstructionToast": "Ossza meg az AppVerifierrel, majd térjen vissza ide, ha kész.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Eltávolítja az alkalmazást?",
|
||||
"other": "Eltávolítja az alkalmazást?"
|
||||
"other": "Eltávolítja az alkalmazásokat?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
||||
@@ -265,10 +351,22 @@
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "A(z) {} és 1 további alkalmazás frissítéseket kapott.",
|
||||
"other": "{} és további {} alkalmazás frissítéseket kapott."
|
||||
"other": "{} és {} további alkalmazás frissítéseket kapott."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "A(z) {} és 1 további alkalmazás frissítve.",
|
||||
"other": "{} és további {} alkalmazás frissítve."
|
||||
"other": "{} és {} további alkalmazás frissítve."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Nem sikerült frissíteni {} és még 1 alkalmazást.",
|
||||
"other": "Nem sikerült frissíteni {} és {} további alkalmazásokat."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} és 1 további alkalmazás is frissült.",
|
||||
"other": "{} és {} további alkalmazás is frissült."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK-k"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
{
|
||||
"invalidURLForSource": "URL dell'App da {} non valido",
|
||||
"invalidURLForSource": "URL dell'app {} non valido",
|
||||
"noReleaseFound": "Impossibile trovare una release adatta",
|
||||
"noVersionFound": "Impossibile determinare la versione della release",
|
||||
"urlMatchesNoSource": "L'URL non corrisponde ad alcuna fonte conosciuta",
|
||||
"cantInstallOlderVersion": "Impossibile installare una versione precedente di un'App",
|
||||
"appIdMismatch": "L'ID del pacchetto scaricato non corrisponde all'ID dell'App esistente",
|
||||
"cantInstallOlderVersion": "Impossibile installare una versione precedente di un'app",
|
||||
"appIdMismatch": "L'ID del pacchetto scaricato non corrisponde all'ID dell'app esistente",
|
||||
"functionNotImplemented": "Questa classe non ha implementato questa funzione",
|
||||
"placeholder": "Segnaposto",
|
||||
"someErrors": "Si sono verificati degli errori",
|
||||
"unexpectedError": "Errore imprevisto",
|
||||
"ok": "Va bene",
|
||||
"and": "e",
|
||||
"startedBgUpdateTask": "Avviata l'attività di controllo degli aggiornamenti in background",
|
||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||
"startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in background",
|
||||
"bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in background",
|
||||
"firstRun": "Questo è il primo avvio di sempre di Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Fissato intervallo di aggiornamento a {}",
|
||||
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
||||
"githubPATHint": "PAT deve seguire questo formato: username:token",
|
||||
"githubPATFormat": "username:token",
|
||||
"githubPATLinkText": "Informazioni su GitHub PAT",
|
||||
"githubPATLabel": "GitHub Personal Access Token (aumenta limite di traffico)",
|
||||
"includePrereleases": "Includi prerelease",
|
||||
"fallbackToOlderReleases": "Ripiega su release precedenti",
|
||||
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
|
||||
@@ -31,66 +22,69 @@
|
||||
"requiredInBrackets": "(richiesto)",
|
||||
"dropdownNoOptsError": "ERRORE: LA TENDINA DEVE AVERE ALMENO UN'OPZIONE",
|
||||
"colour": "Colore",
|
||||
"standard": "Standard",
|
||||
"custom": "Personalizzato",
|
||||
"useMaterialYou": "Utilizzate il materiale che avete a disposizione",
|
||||
"githubStarredRepos": "repository stellati da GitHub",
|
||||
"uname": "Username",
|
||||
"uname": "Nome utente",
|
||||
"wrongArgNum": "Numero di argomenti forniti errato",
|
||||
"xIsTrackOnly": "{} è in modalità Solo-Monitoraggio",
|
||||
"source": "Fonte",
|
||||
"app": "App",
|
||||
"appsFromSourceAreTrackOnly": "Le App da questa fonte sono in modalità 'Solo-Monitoraggio'.",
|
||||
"appsFromSourceAreTrackOnly": "Le app da questa fonte sono in modalità 'Solo-Monitoraggio'.",
|
||||
"youPickedTrackOnly": "È stata selezionata l'opzione 'Solo-Monitoraggio'.",
|
||||
"trackOnlyAppDescription": "L'App sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.",
|
||||
"trackOnlyAppDescription": "L'app sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.",
|
||||
"cancelled": "Annullato",
|
||||
"appAlreadyAdded": "App già aggiunta",
|
||||
"alreadyUpToDateQuestion": "L'App è già aggiornata?",
|
||||
"addApp": "Aggiungi App",
|
||||
"appSourceURL": "URL della fonte dell'App",
|
||||
"alreadyUpToDateQuestion": "L'app è già aggiornata?",
|
||||
"addApp": "Aggiungi app",
|
||||
"appSourceURL": "URL della fonte dell'app",
|
||||
"error": "Errore",
|
||||
"add": "Aggiungi",
|
||||
"searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
|
||||
"search": "Cerca",
|
||||
"additionalOptsFor": "Opzioni aggiuntive per {}",
|
||||
"supportedSourcesBelow": "Fonti supportate:",
|
||||
"supportedSources": "Fonti supportate",
|
||||
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
||||
"searchableInBrackets": "(ricercabile)",
|
||||
"appsString": "App",
|
||||
"noApps": "Nessuna App",
|
||||
"noAppsForFilter": "Nessuna App per i filtri selezionati",
|
||||
"noApps": "Nessuna app",
|
||||
"noAppsForFilter": "Nessuna app per i filtri selezionati",
|
||||
"byX": "Di {}",
|
||||
"percentProgress": "Progresso: {}%",
|
||||
"percentProgress": "Avanzamento: {}%",
|
||||
"pleaseWait": "In attesa",
|
||||
"updateAvailable": "Aggiornamento disponibile",
|
||||
"estimateInBracketsShort": "(prev.)",
|
||||
"notInstalled": "Non installato",
|
||||
"estimateInBrackets": "(previsto)",
|
||||
"pseudoVersion": "pseudo-versione",
|
||||
"selectAll": "Seleziona tutto",
|
||||
"deselectN": "Deseleziona {}",
|
||||
"deselectX": "Deseleziona {}",
|
||||
"xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
|
||||
"removeSelectedAppsQuestion": "Rimuovere le App selezionate?",
|
||||
"removeSelectedApps": "Rimuovi le App selezionate",
|
||||
"removeSelectedAppsQuestion": "Rimuovere le app selezionate?",
|
||||
"removeSelectedApps": "Rimuovi le app selezionate",
|
||||
"updateX": "Aggiorna {}",
|
||||
"installX": "Installa {}",
|
||||
"markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornato",
|
||||
"markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornata",
|
||||
"changeX": "Modifica {}",
|
||||
"installUpdateApps": "Installa/Aggiorna App",
|
||||
"installUpdateSelectedApps": "Installa/Aggiorna le App selezionate",
|
||||
"markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?",
|
||||
"no": "No",
|
||||
"installUpdateApps": "Installa/Aggiorna app",
|
||||
"installUpdateSelectedApps": "Installa/Aggiorna le app selezionate",
|
||||
"markXSelectedAppsAsUpdated": "Contrassegnare le {} app selezionate come aggiornate?",
|
||||
"no": "NO",
|
||||
"yes": "Sì",
|
||||
"markSelectedAppsUpdated": "Contrassegna le App selezionate come aggiornate",
|
||||
"markSelectedAppsUpdated": "Contrassegna le app selezionate come aggiornate",
|
||||
"pinToTop": "Fissa in alto",
|
||||
"unpinFromTop": "Rimuovi dall'alto",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Ripristinare lo stato d'installazione delle App selezionate?",
|
||||
"installStatusOfXWillBeResetExplanation": "Lo stato d'installazione di ogni App selezionata sarà ripristinato.\n\nCiò può essere d'aiuto nel caso in cui la versione mostrata dell'App in Obtainium non è corretta a causa di un aggiornamento fallito o di altri problemi.",
|
||||
"shareSelectedAppURLs": "Condividi gli URL delle App selezionate",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Ripristinare lo stato d'installazione delle app selezionate?",
|
||||
"installStatusOfXWillBeResetExplanation": "Lo stato d'installazione di ogni app selezionata sarà ripristinato.\n\nCiò può essere d'aiuto nel caso in cui la versione mostrata dell'app in Obtainium non sia corretta a causa di un aggiornamento fallito o di altri problemi.",
|
||||
"customLinkMessage": "Questi collegamenti funzionano sui dispositivi con Obtainium installato",
|
||||
"shareAppConfigLinks": "Condividi la configurazione dell'app come collegamento HTML",
|
||||
"shareSelectedAppURLs": "Condividi gli URL delle app selezionate",
|
||||
"resetInstallStatus": "Ripristina lo stato d'installazione",
|
||||
"more": "Di più",
|
||||
"removeOutdatedFilter": "Rimuovi il filtro per le App non aggiornate",
|
||||
"showOutdatedOnly": "Mostra solo le App non aggiornate",
|
||||
"more": "Altro",
|
||||
"removeOutdatedFilter": "Rimuovi il filtro per le app non aggiornate",
|
||||
"showOutdatedOnly": "Mostra solo le app non aggiornate",
|
||||
"filter": "Filtri",
|
||||
"filterActive": "Filtri *",
|
||||
"filterApps": "Filtra App",
|
||||
"appName": "Nome dell'App",
|
||||
"filterApps": "Filtra app",
|
||||
"appName": "Nome dell'app",
|
||||
"author": "Autore",
|
||||
"upToDateApps": "App aggiornate",
|
||||
"nonInstalledApps": "App non installate",
|
||||
@@ -103,43 +97,40 @@
|
||||
"obtainiumImport": "Importa in Obtainium",
|
||||
"importFromURLList": "Importa da lista di URL",
|
||||
"searchQuery": "Stringa di ricerca",
|
||||
"appURLList": "Lista di URL delle App",
|
||||
"appURLList": "Lista di URL delle app",
|
||||
"line": "Linea",
|
||||
"searchX": "Cerca su {}",
|
||||
"noResults": "Nessun risultato trovato",
|
||||
"importX": "Importa {}",
|
||||
"importedAppsIdDisclaimer": "Le App importate potrebbero essere visualizzate erroneamente come \"Non installate\".\nPer risolvere il problema, reinstallale con Obtainium.\nQuesto non dovrebbe influire sui dati delle App.\n\nRiguarda solo l'URL e i metodi di importazione di terze parti.",
|
||||
"importErrors": "Errori dell'importazione",
|
||||
"importedXOfYApps": "{} App di {} importate.",
|
||||
"importedAppsIdDisclaimer": "Le app importate potrebbero essere visualizzate erroneamente come \"Non installate\".\nPer risolvere il problema, reinstallale con Obtainium.\nCiò non dovrebbe influire sui dati delle app.\n\nRiguarda solo l'URL e i metodi di importazione di terze parti.",
|
||||
"importErrors": "Errori di importazione",
|
||||
"importedXOfYApps": "{} app di {} importate.",
|
||||
"followingURLsHadErrors": "I seguenti URL contengono errori:",
|
||||
"okay": "Va bene",
|
||||
"selectURL": "Seleziona l'URL",
|
||||
"selectURLs": "Seleziona gli URL",
|
||||
"pick": "Seleziona",
|
||||
"theme": "Tema",
|
||||
"dark": "Scuro",
|
||||
"light": "Chiaro",
|
||||
"followSystem": "Segui sistema",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"followSystem": "Segui il sistema",
|
||||
"useBlackTheme": "Usa il tema nero puro",
|
||||
"appSortBy": "App ordinate per",
|
||||
"authorName": "Autore/Nome",
|
||||
"nameAuthor": "Nome/Autore",
|
||||
"asAdded": "Data di aggiunta",
|
||||
"appSortOrder": "Ordinamento",
|
||||
"appSortOrder": "Ordine",
|
||||
"ascending": "Ascendente",
|
||||
"descending": "Discendente",
|
||||
"bgUpdateCheckInterval": "Intervallo di controllo degli aggiornamenti in background",
|
||||
"bgUpdateCheckInterval": "Intervallo di controllo degli aggiornamenti in secondo piano",
|
||||
"neverManualOnly": "Mai - Solo manuale",
|
||||
"appearance": "Aspetto",
|
||||
"showWebInAppView": "Mostra pagina web dell'App se selezionata",
|
||||
"showWebInAppView": "Mostra pagina web dell'app se selezionata",
|
||||
"pinUpdates": "Fissa aggiornamenti disponibili in alto",
|
||||
"updates": "Aggiornamenti",
|
||||
"sourceSpecific": "Specifiche per la fonte",
|
||||
"appSource": "Sorgente dell'App",
|
||||
"appSource": "Codice dell'app",
|
||||
"noLogs": "Nessun log",
|
||||
"appLogs": "Log dell'App",
|
||||
"appLogs": "Log dell'app",
|
||||
"close": "Chiudi",
|
||||
"share": "Condividi",
|
||||
"appNotFound": "App non trovata",
|
||||
@@ -149,28 +140,30 @@
|
||||
"deviceSupportsXArch": "Il dispositivo in uso supporta l'architettura {} della CPU.",
|
||||
"deviceSupportsFollowingArchs": "Il dispositivo in uso supporta le seguenti architetture della CPU:",
|
||||
"warning": "Attenzione",
|
||||
"sourceIsXButPackageFromYPrompt": "L'origine dell'App è '{}' ma il pacchetto della release proviene da '{}'. Continuare?",
|
||||
"sourceIsXButPackageFromYPrompt": "L'origine dell'app è '{}' ma il pacchetto della release proviene da '{}'. Continuare?",
|
||||
"updatesAvailable": "Aggiornamenti disponibili",
|
||||
"updatesAvailableNotifDescription": "Notifica all'utente che sono disponibili gli aggiornamenti di una o più App monitorate da Obtainium",
|
||||
"updatesAvailableNotifDescription": "Notifica all'utente che sono disponibili gli aggiornamenti di una o più app monitorate da Obtainium",
|
||||
"noNewUpdates": "Nessun nuovo aggiornamento.",
|
||||
"xHasAnUpdate": "Aggiornamento disponibile per {}",
|
||||
"appsUpdated": "App aggiornate",
|
||||
"appsUpdatedNotifDescription": "Notifica all'utente che una o più App sono state aggiornate in background",
|
||||
"xWasUpdatedToY": "{} è stato aggiornato a {}.",
|
||||
"appsNotUpdated": "Impossibile aggiornare le applicazioni",
|
||||
"appsUpdatedNotifDescription": "Notifica all'utente che una o più app sono state aggiornate in secondo piano",
|
||||
"xWasUpdatedToY": "{} è stato aggiornato alla {}.",
|
||||
"xWasNotUpdatedToY": "Impossibile aggiornare {} a {}.",
|
||||
"errorCheckingUpdates": "Controllo degli errori per gli aggiornamenti",
|
||||
"errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in background fallisce",
|
||||
"errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in secondo piano fallisce",
|
||||
"appsRemoved": "App rimosse",
|
||||
"appsRemovedNotifDescription": "Notifica all'utente che una o più App sono state rimosse a causa di errori durante il caricamento",
|
||||
"appsRemovedNotifDescription": "Notifica all'utente che una o più app sono state rimosse a causa di errori durante il caricamento",
|
||||
"xWasRemovedDueToErrorY": "{} è stata rimosso a causa di questo errore: {}",
|
||||
"completeAppInstallation": "Completa l'installazione dell'App",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le App",
|
||||
"completeAppInstallationNotifDescription": "Chiede all'utente di riaprire Obtainium per terminare l'installazione di un App",
|
||||
"completeAppInstallation": "Completa l'installazione dell'app",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le app",
|
||||
"completeAppInstallationNotifDescription": "Chiede all'utente di riaprire Obtainium per terminare l'installazione di un'app",
|
||||
"checkingForUpdates": "Controllo degli aggiornamenti in corso",
|
||||
"checkingForUpdatesNotifDescription": "Notifica transitoria che appare durante la verifica degli aggiornamenti",
|
||||
"pleaseAllowInstallPerm": "Per favore permetti a Obtainium di installare le App",
|
||||
"pleaseAllowInstallPerm": "Per favore permetti a Obtainium di installare le app",
|
||||
"trackOnly": "Solo-Monitoraggio",
|
||||
"errorWithHttpStatusCode": "Errore {}",
|
||||
"versionCorrectionDisabled": "Correzione della versione disabilitata (il plugin non pare funzionare)",
|
||||
"versionCorrectionDisabled": "Correzione della versione disattivata (il plugin sembra non funzionare)",
|
||||
"unknown": "Sconosciuto",
|
||||
"none": "Nessuno",
|
||||
"never": "Mai",
|
||||
@@ -178,24 +171,27 @@
|
||||
"installedVersionX": "Versione installata: {}",
|
||||
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
||||
"remove": "Rimuovi",
|
||||
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
||||
"fdroid": "F-Droid",
|
||||
"appIdOrName": "ID o nome dell'App",
|
||||
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
|
||||
"reposHaveMultipleApps": "I repository possono contenere più App",
|
||||
"yesMarkUpdated": "Sì, contrassegna come aggiornata",
|
||||
"fdroid": "F-Droid ufficiale",
|
||||
"appIdOrName": "ID o nome dell'app",
|
||||
"appId": "ID dell'app",
|
||||
"appWithIdOrNameNotFound": "Non è stata trovata alcuna app con quell'ID o nome",
|
||||
"reposHaveMultipleApps": "I repository possono contenere più app",
|
||||
"fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"steamMobile": "Mobile a vapore",
|
||||
"steamChat": "Chat di vapore",
|
||||
"install": "Installa",
|
||||
"markInstalled": "Contrassegna come installato",
|
||||
"markInstalled": "Contrassegna come installata",
|
||||
"update": "Aggiorna",
|
||||
"markUpdated": "Contrassegna come aggiornato",
|
||||
"markUpdated": "Contrassegna come aggiornata",
|
||||
"additionalOptions": "Opzioni aggiuntive",
|
||||
"disableVersionDetection": "Disattiva il rilevamento della versione",
|
||||
"noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le App la cui versione non viene rilevata correttamente.",
|
||||
"noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le app la cui versione non viene rilevata correttamente.",
|
||||
"downloadingX": "Scaricamento di {} in corso",
|
||||
"downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'App",
|
||||
"downloadX": "Scarica {}",
|
||||
"downloadedX": "Scaricato {}",
|
||||
"releaseAsset": "Rilascio Asset",
|
||||
"downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'app",
|
||||
"noAPKFound": "Nessun APK trovato",
|
||||
"noVersionDetection": "Disattiva rilevamento di versione",
|
||||
"categorize": "Aggiungi a categoria",
|
||||
@@ -204,45 +200,134 @@
|
||||
"noCategory": "Nessuna categoria",
|
||||
"noCategories": "Nessuna categoria",
|
||||
"deleteCategoriesQuestion": "Eliminare le categorie?",
|
||||
"categoryDeleteWarning": "Tutte le App nelle categorie eliminate saranno impostate come non categorizzate.",
|
||||
"categoryDeleteWarning": "Tutte le app nelle categorie eliminate saranno impostate come non categorizzate.",
|
||||
"addCategory": "Aggiungi categoria",
|
||||
"label": "Etichetta",
|
||||
"language": "Lingua",
|
||||
"copiedToClipboard": "Copiato negli appunti",
|
||||
"storagePermissionDenied": "Accesso ai file non autorizzato",
|
||||
"selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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": "Importa da URL in file (come OPML)",
|
||||
"versionDetectionExplanation": "Riconcilia la stringa della versione con la versione rilevata dal sistema operativo",
|
||||
"versionDetection": "Rilevamento di versione",
|
||||
"standardVersionDetection": "Rilevamento di versione standard",
|
||||
"groupByCategory": "Raggruppa per categoria",
|
||||
"autoApkFilterByArch": "Tenta di filtrare gli APK in base all'architettura della CPU, se possibile",
|
||||
"overrideSource": "Sovrascrivi fonte",
|
||||
"dontShowAgain": "Non mostrarlo più",
|
||||
"dontShowTrackOnlyWarnings": "Non mostrare gli avvisi 'Solo-Monitoraggio'",
|
||||
"dontShowAPKOriginWarnings": "Non mostrare gli avvisi di origine dell'APK",
|
||||
"moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista",
|
||||
"gitlabPATLabel": "GitLab Token di accesso personale",
|
||||
"about": "Informazioni",
|
||||
"requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)",
|
||||
"checkOnStart": "Controlla una volta all'avvio",
|
||||
"tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente",
|
||||
"removeOnExternalUninstall": "Rimuovi automaticamente app disinstallate esternamente",
|
||||
"pickHighestVersionCode": "Auto-seleziona APK con version code più alto",
|
||||
"checkUpdateOnDetailPage": "Controlla aggiornamenti all'apertura dei dettagli dell'app",
|
||||
"disablePageTransitions": "Disattiva animazioni di transizione pagina",
|
||||
"reversePageTransitions": "Inverti animazioni di transizione pagina",
|
||||
"minStarCount": "Numero minimo di stelle",
|
||||
"addInfoBelow": "Aggiungi questa info sotto.",
|
||||
"addInfoInSettings": "Aggiungi questa info nelle impostazioni.",
|
||||
"githubSourceNote": "Il limite di ricerca GitHub può essere evitato usando una chiave API.",
|
||||
"sortByLastLinkSegment": "Ordina solo in base all'ultimo segmento del collegamento",
|
||||
"filterReleaseNotesByRegEx": "Filtra le note di rilascio con espressione regolare",
|
||||
"customLinkFilterRegex": "Filtra link APK personalizzato con espressione regolare (predefinito '.apk$')",
|
||||
"appsPossiblyUpdated": "Aggiornamenti app tentati",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifica all'utente che sono stati potenzialmente applicati in secondo piano aggiornamenti a una o più app",
|
||||
"xWasPossiblyUpdatedToY": "{} potrebbe essere stata aggiornata alla {}.",
|
||||
"enableBackgroundUpdates": "Attiva aggiornamenti in secondo piano",
|
||||
"backgroundUpdateReqsExplanation": "Gli aggiornamenti in secondo piano potrebbero non essere possibili per tutte le app.",
|
||||
"backgroundUpdateLimitsExplanation": "La riuscita di un'installazione in secondo piano può essere determinata solo quando viene aperto Obtainium.",
|
||||
"verifyLatestTag": "Verifica l'etichetta 'Latest'",
|
||||
"intermediateLinkRegex": "Filtro per un collegamento 'intermedio' da visitare",
|
||||
"filterByLinkText": "Filtra i collegamenti in base al testo del collegamento",
|
||||
"intermediateLinkNotFound": "Link intermedio non trovato",
|
||||
"intermediateLink": "Collegamento intermedio",
|
||||
"exemptFromBackgroundUpdates": "Esente da aggiornamenti in secondo piano (se attivo)",
|
||||
"bgUpdatesOnWiFiOnly": "Disattiva aggiornamenti in secondo piano quando non si usa il WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto-seleziona APK con versionCode più alto",
|
||||
"versionExtractionRegEx": "RegEx di estrazione versione",
|
||||
"matchGroupToUse": "Gruppo da usare",
|
||||
"highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
|
||||
"pickExportDir": "Scegli cartella esp.",
|
||||
"autoExportOnChanges": "Auto-esporta dopo modifiche",
|
||||
"includeSettings": "Includi impostazioni",
|
||||
"filterVersionsByRegEx": "Filtra versioni con espressione regolare",
|
||||
"trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito",
|
||||
"dontSortReleasesList": "Conserva l'ordine di release da API",
|
||||
"reverseSort": "Ordine inverso",
|
||||
"takeFirstLink": "Prendi il primo link",
|
||||
"skipSort": "Salta ordinamento",
|
||||
"debugMenu": "Menu di debug",
|
||||
"bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
|
||||
"runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
|
||||
"versionExtractWholePage": "Applica regex di estrazione versione a tutta la pagina",
|
||||
"installing": "Installazione",
|
||||
"skipUpdateNotifications": "Salta notifiche di aggiornamento",
|
||||
"updatesAvailableNotifChannel": "Aggiornamenti disponibili",
|
||||
"appsUpdatedNotifChannel": "App aggiornate",
|
||||
"appsPossiblyUpdatedNotifChannel": "Aggiornamenti app tentati",
|
||||
"errorCheckingUpdatesNotifChannel": "Controllo degli errori per gli aggiornamenti",
|
||||
"appsRemovedNotifChannel": "App rimosse",
|
||||
"downloadingXNotifChannel": "Scaricamento di {} in corso",
|
||||
"completeAppInstallationNotifChannel": "Completa l'installazione dell'app",
|
||||
"checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
|
||||
"supportFixedAPKURL": "Supporta URL fissi di APK",
|
||||
"selectX": "Seleziona {}",
|
||||
"parallelDownloads": "Permetti download paralleli",
|
||||
"useShizuku": "Utilizzare Shizuku o Sui per installare",
|
||||
"shizukuBinderNotFound": "Shizuku non è in esecuzione",
|
||||
"shizukuOld": "Vecchia versione di Shizuku (<11) - aggiornarla",
|
||||
"shizukuOldAndroidWithADB": "Shizuku funziona su Android < 8.1 con ADB - aggiornare Android o utilizzare Sui al suo posto",
|
||||
"shizukuPretendToBeGooglePlay": "Impostare Google Play come fonte di installazione (se si usa Shizuku)",
|
||||
"useSystemFont": "Usa i caratteri di sistema",
|
||||
"useVersionCodeAsOSVersion": "Usa il codice versione dell'app come versione rilevata dal sistema operativo",
|
||||
"requestHeader": "Intestazione della richiesta",
|
||||
"useLatestAssetDateAsReleaseDate": "Usa l'ultimo caricamento della risorsa come data di rilascio",
|
||||
"defaultPseudoVersioningMethod": "Metodo di pseudoversione predefinito",
|
||||
"partialAPKHash": "Hash APK parziale",
|
||||
"APKLinkHash": "Hash collegamento APK",
|
||||
"directAPKLink": "Collegamento APK diretto",
|
||||
"pseudoVersionInUse": "È in uso una pseudo-versione",
|
||||
"installed": "Installato",
|
||||
"latest": "Ultimo",
|
||||
"invertRegEx": "Inverti espressione regolare",
|
||||
"note": "Nota",
|
||||
"selfHostedNote": "Il menu a tendina \"{}\" può essere usato per raggiungere istanze autogestite/personali di qualsiasi fonte.",
|
||||
"badDownload": "Non è stato possibile analizzare l'APK (download incompatibile o parziale).",
|
||||
"beforeNewInstallsShareToAppVerifier": "Condividere le nuove applicazioni con AppVerifier (se disponibile)",
|
||||
"appVerifierInstructionToast": "Condividete con AppVerifier, quindi tornate qui quando siete pronti.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Rimuovere l'App?",
|
||||
"other": "Rimuovere le App?"
|
||||
"one": "Rimuovere l'app?",
|
||||
"other": "Rimuovere le app?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
||||
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuto",
|
||||
"other": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuti"
|
||||
"one": "Il controllo degli aggiornamenti in secondo piano ha riscontrato un {}, nuovo tentativo tra {} minuto",
|
||||
"other": "Il controllo degli aggiornamenti in secondo piano ha riscontrato un {}, nuovo tentativo tra {} minuti"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamento - notificherà l'utente se necessario",
|
||||
"other": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamenti - notificherà l'utente se necessario"
|
||||
"one": "Il controllo degli aggiornamenti in secondo piano ha trovato {} aggiornamento - notificherà l'utente se necessario",
|
||||
"other": "Il controllo degli aggiornamenti in secondo piano ha trovato {} aggiornamenti - notificherà l'utente se necessario"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} App",
|
||||
"other": "{} App"
|
||||
"one": "{} app",
|
||||
"other": "{} app"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
@@ -261,15 +346,27 @@
|
||||
"other": "{} giorni"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Pulito {n} log (prima = {before}, dopo = {after})",
|
||||
"other": "Puliti {n} log (prima = {before}, dopo = {after})"
|
||||
"one": "Rimosso {n} log (prima = {before}, dopo = {after})",
|
||||
"other": "Rimossi {n} log (prima = {before}, dopo = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} e un'altra App hanno aggiornamenti disponibili.",
|
||||
"other": "{} e altre {} App hanno aggiornamenti disponibili."
|
||||
"one": "{} e un'altra app hanno aggiornamenti disponibili.",
|
||||
"other": "{} e altre {} app hanno aggiornamenti disponibili."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} e un'altra App sono state aggiornate.",
|
||||
"other": "{} e altre {} App sono state aggiornate."
|
||||
"one": "{} e un'altra app sono state aggiornate.",
|
||||
"other": "{} e altre {} app sono state aggiornate."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Non è riuscito ad aggiornare {} e altre 1 app.",
|
||||
"other": "Non è riuscito ad aggiornare {} e {} altre applicazioni."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} e un'altra app potrebbero essere state aggiornate.",
|
||||
"other": "{} e altre {} app potrebbero essere state aggiornate."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,10 @@
|
||||
"unexpectedError": "予期せぬエラーが発生しました",
|
||||
"ok": "OK",
|
||||
"and": "と",
|
||||
"startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始",
|
||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
|
||||
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
|
||||
"firstRun": "これがObtainiumの最初の実行です",
|
||||
"settingUpdateCheckIntervalTo": "確認間隔を{}に設定する",
|
||||
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
||||
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
||||
"githubPATFormat": "ユーザー名:トークン",
|
||||
"githubPATLinkText": "GitHub PATsについて",
|
||||
"includePrereleases": "プレリリースを含む",
|
||||
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする",
|
||||
"invalidRegEx": "無効な正規表現",
|
||||
"noDescription": "説明はありません",
|
||||
"cancel": "キャンセル",
|
||||
@@ -31,6 +22,9 @@
|
||||
"requiredInBrackets": "(必須)",
|
||||
"dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です",
|
||||
"colour": "カラー",
|
||||
"standard": "スタンダード",
|
||||
"custom": "カスタム",
|
||||
"useMaterialYou": "使用素材",
|
||||
"githubStarredRepos": "Githubでスターしたリポジトリ",
|
||||
"uname": "ユーザー名",
|
||||
"wrongArgNum": "提供する引数の数が間違っています",
|
||||
@@ -50,7 +44,7 @@
|
||||
"searchSomeSourcesLabel": "検索 (一部ソースのみ)",
|
||||
"search": "検索",
|
||||
"additionalOptsFor": "{}の追加オプション",
|
||||
"supportedSourcesBelow": "対応するソース:",
|
||||
"supportedSources": "対応するソース",
|
||||
"trackOnlyInBrackets": "(追跡のみ)",
|
||||
"searchableInBrackets": "(検索可能)",
|
||||
"appsString": "アプリ",
|
||||
@@ -60,11 +54,10 @@
|
||||
"percentProgress": "ダウンロード中: {}%",
|
||||
"pleaseWait": "しばらくお待ちください",
|
||||
"updateAvailable": "アップデートが利用可能",
|
||||
"estimateInBracketsShort": "(推定)",
|
||||
"notInstalled": "未インストール",
|
||||
"estimateInBrackets": "(推定)",
|
||||
"pseudoVersion": "疑似バージョン",
|
||||
"selectAll": "すべて選択",
|
||||
"deselectN": "{}件の選択を解除",
|
||||
"deselectX": "{}件の選択を解除",
|
||||
"xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
|
||||
"removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
|
||||
"removeSelectedApps": "選択したアプリを削除する",
|
||||
@@ -82,14 +75,15 @@
|
||||
"unpinFromTop": "トップから固定解除",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
||||
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。",
|
||||
"customLinkMessage": "これらのリンクはObtainiumがインストールされたデバイスで動作します",
|
||||
"shareAppConfigLinks": "アプリの設定をHTMLリンクとして共有する",
|
||||
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
||||
"resetInstallStatus": "インストール状態をリセットする",
|
||||
"more": "もっと見る",
|
||||
"removeOutdatedFilter": "アップデートが存在するアプリのフィルターを解除",
|
||||
"showOutdatedOnly": "アップデートが存在するアプリのみ表示する",
|
||||
"filter": "フィルター",
|
||||
"filterActive": "フィルター *",
|
||||
"filterApps": "アプリを絞り込む",
|
||||
"filterApps": "アプリをフィルタリングする",
|
||||
"appName": "アプリ名",
|
||||
"author": "作者",
|
||||
"upToDateApps": "最新のアプリ",
|
||||
@@ -112,7 +106,6 @@
|
||||
"importErrors": "インポートエラー",
|
||||
"importedXOfYApps": "{} / {} アプリをインポートしました",
|
||||
"followingURLsHadErrors": "以下のURLでエラーが発生しました:",
|
||||
"okay": "OK",
|
||||
"selectURL": "URLを選択",
|
||||
"selectURLs": "URLを選択",
|
||||
"pick": "選択",
|
||||
@@ -120,9 +113,7 @@
|
||||
"dark": "ダーク",
|
||||
"light": "ライト",
|
||||
"followSystem": "システムに従う",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "ピュアブラックダークテーマを使用する",
|
||||
"appSortBy": "アプリの並び方",
|
||||
"authorName": "作者名/アプリ名",
|
||||
"nameAuthor": "アプリ名/作者名",
|
||||
@@ -136,7 +127,7 @@
|
||||
"showWebInAppView": "アプリページにソースのWebページを表示する",
|
||||
"pinUpdates": "アップデートがあるアプリをトップに固定する",
|
||||
"updates": "アップデート",
|
||||
"sourceSpecific": "Github アクセストークン",
|
||||
"sourceSpecific": "ソース別の設定",
|
||||
"appSource": "アプリのソース",
|
||||
"noLogs": "ログはありません",
|
||||
"appLogs": "アプリのログ",
|
||||
@@ -153,18 +144,20 @@
|
||||
"updatesAvailable": "アップデートが利用可能",
|
||||
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
|
||||
"noNewUpdates": "新しいアップデートはありません",
|
||||
"xHasAnUpdate": "{} のアップデートが利用可能です",
|
||||
"xHasAnUpdate": "{} のアップデートが利用可能です。",
|
||||
"appsUpdated": "アプリをアップデートしました",
|
||||
"appsNotUpdated": "アプリケーションの更新に失敗",
|
||||
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
|
||||
"xWasUpdatedToY": "{} が {} にアップデートされました",
|
||||
"xWasNotUpdatedToY": "への更新に失敗しました。",
|
||||
"errorCheckingUpdates": "アップデート確認中のエラー",
|
||||
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
|
||||
"appsRemoved": "削除されたアプリ",
|
||||
"appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する",
|
||||
"xWasRemovedDueToErrorY": "このエラーのため、{} は削除されました: {}",
|
||||
"completeAppInstallation": "アプリのインストールを完了する",
|
||||
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開いている必要があります。",
|
||||
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります。",
|
||||
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumが開いている必要があります",
|
||||
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります",
|
||||
"checkingForUpdates": "アップデートを確認中",
|
||||
"checkingForUpdatesNotifDescription": "アップデートを確認する際に表示される一時的な通知",
|
||||
"pleaseAllowInstallPerm": "Obtainiumによるアプリのインストールを許可してください。",
|
||||
@@ -179,14 +172,14 @@
|
||||
"lastUpdateCheckX": "最終アップデート確認: {}",
|
||||
"remove": "削除",
|
||||
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
||||
"fdroid": "F-Droid",
|
||||
"fdroid": "F-Droid公式",
|
||||
"appIdOrName": "アプリのIDまたは名前",
|
||||
"appId": "アプリID",
|
||||
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
||||
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
||||
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"steamMobile": "Steamモバイル",
|
||||
"steamChat": "Steamチャット",
|
||||
"install": "インストール",
|
||||
"markInstalled": "インストール済みとしてマークする",
|
||||
"update": "アップデート",
|
||||
@@ -195,6 +188,9 @@
|
||||
"disableVersionDetection": "バージョン検出を無効にする",
|
||||
"noVersionDetectionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリにのみ使用する必要があります。",
|
||||
"downloadingX": "{} をダウンロード中",
|
||||
"downloadX": "ダウンロード",
|
||||
"downloadedX": "ダウンロード",
|
||||
"releaseAsset": "リリース資産",
|
||||
"downloadNotifDescription": "アプリのダウンロード状況を通知する",
|
||||
"noAPKFound": "APKが見つかりません",
|
||||
"noVersionDetection": "バージョン検出を行わない",
|
||||
@@ -211,7 +207,7 @@
|
||||
"copiedToClipboard": "クリップボードにコピーしました",
|
||||
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
||||
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
||||
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
|
||||
"filterAPKsByRegEx": "正規表現でAPKをフィルタリングする",
|
||||
"removeFromObtainium": "Obtainiumから削除する",
|
||||
"uninstallFromDevice": "デバイスからアンインストールする",
|
||||
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
|
||||
@@ -220,17 +216,106 @@
|
||||
"changes": "変更点",
|
||||
"releaseDate": "リリース日",
|
||||
"importFromURLsInFile": "ファイル(OPMLなど)内のURLからインポート",
|
||||
"versionDetectionExplanation": "バージョン文字列とOSから検出されたバージョンを照合する",
|
||||
"versionDetection": "バージョン検出",
|
||||
"standardVersionDetection": "標準のバージョン検出",
|
||||
"groupByCategory": "カテゴリ別にグループ化する",
|
||||
"autoApkFilterByArch": "可能であれば,CPUアーキテクチャによるAPKのフィルタリングを試みる",
|
||||
"autoApkFilterByArch": "可能であれば、CPUアーキテクチャによるAPKのフィルタリングを試みる",
|
||||
"overrideSource": "ソースの上書き",
|
||||
"dontShowAgain": "二度と表示しない",
|
||||
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
|
||||
"dontShowAPKOriginWarnings": "APKのダウンロード元の警告を表示しない",
|
||||
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
|
||||
"gitlabPATLabel": "GitLab パーソナルアクセストークン",
|
||||
"about": "概要",
|
||||
"requiresCredentialsInSettings": "{}: これには追加の認証が必要です (設定にて)",
|
||||
"checkOnStart": "起動時にアップデートを確認する",
|
||||
"tryInferAppIdFromCode": "ソースコードからApp IDを推測する",
|
||||
"removeOnExternalUninstall": "外部でアンインストールされたアプリを自動的に削除する",
|
||||
"pickHighestVersionCode": "最も高いバージョンコードのAPKを自動的に選択する",
|
||||
"checkUpdateOnDetailPage": "アプリの詳細ページを開く際にアップデートを確認する",
|
||||
"disablePageTransitions": "ページ遷移アニメーションを無効化する",
|
||||
"reversePageTransitions": "ページ遷移アニメーションを反転する",
|
||||
"minStarCount": "最小スター数",
|
||||
"addInfoBelow": "下部でこの情報を追加してください。",
|
||||
"addInfoInSettings": "設定でこの情報を追加してください。",
|
||||
"githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。",
|
||||
"sortByLastLinkSegment": "リンクの最後のセグメントのみでソートする",
|
||||
"filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする",
|
||||
"customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
|
||||
"appsPossiblyUpdated": "アプリのアップデートを試行",
|
||||
"appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する",
|
||||
"xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります。",
|
||||
"enableBackgroundUpdates": "バックグラウンドアップデートを有効化する",
|
||||
"backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
|
||||
"backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
|
||||
"verifyLatestTag": "'latest'タグを確認する",
|
||||
"intermediateLinkRegex": "訪問する「中間」リンクのフィルター",
|
||||
"filterByLinkText": "テキストでリンクをフィルタリングする",
|
||||
"intermediateLinkNotFound": "中間リンクが見つかりませんでした",
|
||||
"intermediateLink": "中間リンク",
|
||||
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
|
||||
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合、バックグラウンドアップデートを無効にする",
|
||||
"autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
|
||||
"versionExtractionRegEx": "バージョン抽出の正規表現",
|
||||
"matchGroupToUse": "使用するマッチしたグループ",
|
||||
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
|
||||
"pickExportDir": "エクスポートディレクトリを選択",
|
||||
"autoExportOnChanges": "変更があった際に自動でエクスポートする",
|
||||
"includeSettings": "設定を含む",
|
||||
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
|
||||
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
|
||||
"dontSortReleasesList": "APIからのリリース順を保持する",
|
||||
"reverseSort": "逆順ソート",
|
||||
"takeFirstLink": "最初のリンクを取得する",
|
||||
"skipSort": "ソートをスキップする",
|
||||
"debugMenu": "デバッグメニュー",
|
||||
"bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。",
|
||||
"runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する",
|
||||
"versionExtractWholePage": "バージョン抽出の正規表現をページ全体に適用する",
|
||||
"installing": "インストール中",
|
||||
"skipUpdateNotifications": "アップデート通知を行わない",
|
||||
"updatesAvailableNotifChannel": "アップデートが利用可能",
|
||||
"appsUpdatedNotifChannel": "アプリをアップデートしました",
|
||||
"appsPossiblyUpdatedNotifChannel": "アプリのアップデートを試行",
|
||||
"errorCheckingUpdatesNotifChannel": "アップデート確認中のエラー",
|
||||
"appsRemovedNotifChannel": "削除されたアプリ",
|
||||
"downloadingXNotifChannel": "{} をダウンロード中",
|
||||
"completeAppInstallationNotifChannel": "アプリのインストールを完了する",
|
||||
"checkingForUpdatesNotifChannel": "アップデートを確認中",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
|
||||
"supportFixedAPKURL": "固定されたAPKのURLをサポートする",
|
||||
"selectX": "{} 選択",
|
||||
"parallelDownloads": "並行ダウンロードを許可する",
|
||||
"useShizuku": "シズクまたはスイを使って設置する",
|
||||
"shizukuBinderNotFound": "Shizukuが起動していません",
|
||||
"shizukuOld": "古い雫バージョン (<11) - アップデートしてください。",
|
||||
"shizukuOldAndroidWithADB": "雫、Android < 8.1でADB動作 - Androidをアップデートするか、代わりにSuiを使うか",
|
||||
"shizukuPretendToBeGooglePlay": "インストール元をGoogle Playに設定する(雫を使用する場合)",
|
||||
"useSystemFont": "システムフォントを使用する",
|
||||
"useVersionCodeAsOSVersion": "アプリのバージョンコードをOSで検出されたバージョンとして使用する",
|
||||
"requestHeader": "リクエストヘッダー",
|
||||
"useLatestAssetDateAsReleaseDate": "最新のアセットアップロードをリリース日として使用する",
|
||||
"defaultPseudoVersioningMethod": "デフォルトの疑似バージョン管理方法",
|
||||
"partialAPKHash": "部分的なAPKのハッシュ",
|
||||
"APKLinkHash": "APKリンクのハッシュ",
|
||||
"directAPKLink": "APKのダイレクトリンク",
|
||||
"pseudoVersionInUse": "疑似バージョンが使用されています",
|
||||
"installed": "インストール済み",
|
||||
"latest": "最新",
|
||||
"invertRegEx": "正規表現を反転",
|
||||
"note": "注",
|
||||
"selfHostedNote": "ドロップダウン\"{}\"を使用すると、あらゆるソースのセルフホスト/カスタムインスタンスにアクセスできます。",
|
||||
"badDownload": "APK を解析できませんでした(互換性がないか、部分的にダウンロードされています)。",
|
||||
"beforeNewInstallsShareToAppVerifier": "AppVerifierで新しいアプリを共有する(利用可能な場合)",
|
||||
"appVerifierInstructionToast": "AppVerifierに共有し、準備ができたらここに戻ってください。",
|
||||
"removeAppQuestion": {
|
||||
"one": "アプリを削除しますか?",
|
||||
"other": "アプリを削除しますか?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
||||
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
||||
"one": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください",
|
||||
"other": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します",
|
||||
@@ -241,35 +326,47 @@
|
||||
"other": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{}個のアプリ",
|
||||
"other": "{}個のアプリ"
|
||||
"one": "{} 個のアプリ",
|
||||
"other": "{} 個のアプリ"
|
||||
},
|
||||
"url": {
|
||||
"one": "{}個のURL",
|
||||
"other": "{}個のURL"
|
||||
"one": "{} 個のURL",
|
||||
"other": "{} 個のURL"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{}分",
|
||||
"other": "{}分"
|
||||
"one": "{} 分",
|
||||
"other": "{} 分"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{}時間",
|
||||
"other": "{}時間"
|
||||
"one": "{} 時間",
|
||||
"other": "{} 時間"
|
||||
},
|
||||
"day": {
|
||||
"one": "{}日",
|
||||
"other": "{}日"
|
||||
"one": "{} 日",
|
||||
"other": "{} 日"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "{n}個のログをクリアしました (前 = {before}, 後 = {after})",
|
||||
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
|
||||
"one": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})",
|
||||
"other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
|
||||
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
|
||||
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です。",
|
||||
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です。"
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} とさらに {} 個のアプリがアップデートされました",
|
||||
"other": "{} とさらに {} 個のアプリがアップデートされました"
|
||||
"one": "{} とさらに {} 個のアプリがアップデートされました。",
|
||||
"other": "{} とさらに {} 個のアプリがアップデートされました。"
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "更新に失敗しました。",
|
||||
"other": "アプリのアップデートに失敗しました。"
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります。",
|
||||
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります。"
|
||||
},
|
||||
"apk": {
|
||||
"one": "{}APK",
|
||||
"other": "{}APK"
|
||||
}
|
||||
}
|
||||
|
||||
372
assets/translations/nl.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "Geen valide {} app URL",
|
||||
"noReleaseFound": "Kan geen geschikte release vinden",
|
||||
"noVersionFound": "Kan de versie niet bepalen",
|
||||
"urlMatchesNoSource": "URL komt niet overeen met bekende bron",
|
||||
"cantInstallOlderVersion": "Kan geen oudere versie van de app installeren",
|
||||
"appIdMismatch": "Gedownloade pakket-ID komt niet overeen met de bestaande app-ID",
|
||||
"functionNotImplemented": "Deze class heeft deze functie niet geïmplementeerd.",
|
||||
"placeholder": "Plaatshouder",
|
||||
"someErrors": "Er zijn enkele fouten opgetreden",
|
||||
"unexpectedError": "Onverwachte fout",
|
||||
"ok": "Ok",
|
||||
"and": "en",
|
||||
"githubPATLabel": "GitHub Personal Access Token\n(Verhoogt limiet aantal verzoeken)",
|
||||
"includePrereleases": "Bevat prereleases",
|
||||
"fallbackToOlderReleases": "Terugvallen op oudere releases",
|
||||
"filterReleaseTitlesByRegEx": "Filter release-titels met reguliere expressies.",
|
||||
"invalidRegEx": "Ongeldige reguliere expressie",
|
||||
"noDescription": "Geen omschrijving",
|
||||
"cancel": "Annuleer",
|
||||
"continue": "Ga verder",
|
||||
"requiredInBrackets": "(Verplicht)",
|
||||
"dropdownNoOptsError": "FOUTMELDING: DROPDOWN MOET TENMINSTE ÉÉN OPT HEBBEN",
|
||||
"colour": "Kleur",
|
||||
"standard": "Standaard",
|
||||
"custom": "Aangepast",
|
||||
"useMaterialYou": "Gebruik materiaal",
|
||||
"githubStarredRepos": "GitHub-repo's met ster",
|
||||
"uname": "Gebruikersnaam",
|
||||
"wrongArgNum": "Onjuist aantal argumenten verstrekt.",
|
||||
"xIsTrackOnly": "{} is alleen tracken",
|
||||
"source": "Bron",
|
||||
"app": "App",
|
||||
"appsFromSourceAreTrackOnly": "Apps van deze bron zijn 'Track-Only'.",
|
||||
"youPickedTrackOnly": "Je hebt de 'Track-Only' optie geselecteerd.",
|
||||
"trackOnlyAppDescription": "De app zal worden gevolgd voor updates, maar Obtainium zal niet in staat zijn om deze te downloaden of te installeren.",
|
||||
"cancelled": "Geannuleerd",
|
||||
"appAlreadyAdded": "App al toegevoegd",
|
||||
"alreadyUpToDateQuestion": "Is de app al up-to-date?",
|
||||
"addApp": "App toevoegen",
|
||||
"appSourceURL": "App bron URL",
|
||||
"error": "Foutmelding",
|
||||
"add": "Toevoegen",
|
||||
"searchSomeSourcesLabel": "Zoeken (Alleen sommige bronnen)",
|
||||
"search": "Zoeken",
|
||||
"additionalOptsFor": "Aanvullende opties voor {}",
|
||||
"supportedSources": "Ondersteunde bronnen",
|
||||
"trackOnlyInBrackets": "(Alleen track)",
|
||||
"searchableInBrackets": "(Doorzoekbaar)",
|
||||
"appsString": "Apps",
|
||||
"noApps": "Geen Apps",
|
||||
"noAppsForFilter": "Geen Apps voor filter",
|
||||
"byX": "Door {}",
|
||||
"percentProgress": "Vooruitgang: {}%",
|
||||
"pleaseWait": "Even geduld",
|
||||
"updateAvailable": "Update beschikbaar",
|
||||
"notInstalled": "Niet geinstalleerd",
|
||||
"pseudoVersion": "pseudo-versie",
|
||||
"selectAll": "Selecteer alles",
|
||||
"deselectX": "Deselecteer {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.",
|
||||
"removeSelectedAppsQuestion": "Geselecteerde apps verwijderen??",
|
||||
"removeSelectedApps": "Geselecteerde apps verwijderen",
|
||||
"updateX": "Update {}",
|
||||
"installX": "Installeer {}",
|
||||
"markXTrackOnlyAsUpdated": "Markeer {}\n(Track-Only)\nals up-to-date",
|
||||
"changeX": "Verander {}",
|
||||
"installUpdateApps": "Installeer/Update apps",
|
||||
"installUpdateSelectedApps": "Installeer/Update geselecteerde apps",
|
||||
"markXSelectedAppsAsUpdated": "{} geselecteerde apps markeren als up-to-date?",
|
||||
"no": "Nee",
|
||||
"yes": "Ja",
|
||||
"markSelectedAppsUpdated": "Markeer geselecteerde aps als up-to-date",
|
||||
"pinToTop": "Vastzetten aan de bovenkant",
|
||||
"unpinFromTop": "Losmaken van de bovenkant",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Installatiestatus resetten voor geselecteerde apps?",
|
||||
"installStatusOfXWillBeResetExplanation": "De installatiestatus van alle geselecteerde apps zal worden gereset.\n\nDit kan helpen wanneer de versie van de app die in Obtainium wordt weergegeven onjuist is vanwege mislukte updates of andere problemen.",
|
||||
"customLinkMessage": "Deze links werken op apparaten waarop Obtainium is geïnstalleerd",
|
||||
"shareAppConfigLinks": "App-configuratie delen als HTML-link",
|
||||
"shareSelectedAppURLs": "Deel geselecteerde app URL's",
|
||||
"resetInstallStatus": "Reset installatiestatus",
|
||||
"more": "Meer",
|
||||
"removeOutdatedFilter": "Verwijder out-of-date app filter",
|
||||
"showOutdatedOnly": "Toon alleen out-of-date apps",
|
||||
"filter": "Filter",
|
||||
"filterApps": "Filter apps",
|
||||
"appName": "App naam",
|
||||
"author": "Auteur",
|
||||
"upToDateApps": "Up-to-date apps",
|
||||
"nonInstalledApps": "Niet-geïnstalleerde apps",
|
||||
"importExport": "Importeren/Exporteren",
|
||||
"settings": "Instellingen",
|
||||
"exportedTo": "Geëxporteerd naar {}",
|
||||
"obtainiumExport": "Obtainium export",
|
||||
"invalidInput": "Ongeldige invoer",
|
||||
"importedX": "Geïmporteerd {}",
|
||||
"obtainiumImport": "Obtainium import",
|
||||
"importFromURLList": "Importeer van URL-lijsten",
|
||||
"searchQuery": "Zoekopdracht",
|
||||
"appURLList": "App URL-lijst",
|
||||
"line": "Lijn",
|
||||
"searchX": "Zoek {}",
|
||||
"noResults": "Geen resultaten gevonden",
|
||||
"importX": "Importeer {}",
|
||||
"importedAppsIdDisclaimer": "Geïmporteerde apps kunnen mogelijk onjuist worden weergegeven als \"Niet geïnstalleerd\".\nOm dit op te lossen, herinstalleer ze via Obtainium.\nDit zou geen invloed moeten hebben op app-gegevens.\n\nDit heeft alleen invloed op URL- en importmethoden van derden.",
|
||||
"importErrors": "Import foutmeldingen",
|
||||
"importedXOfYApps": "{} van {} apps geïmporteerd.",
|
||||
"followingURLsHadErrors": "De volgende URL's bevatten fouten:",
|
||||
"selectURL": "Selecteer URL",
|
||||
"selectURLs": "Selecteer URL's",
|
||||
"pick": "Kies",
|
||||
"theme": "Thema",
|
||||
"dark": "Donker",
|
||||
"light": "Licht",
|
||||
"followSystem": "Volg systeem",
|
||||
"useBlackTheme": "Gebruik zwart thema",
|
||||
"appSortBy": "App sorteren op",
|
||||
"authorName": "Auteur/Naam",
|
||||
"nameAuthor": "Naam/Auteur",
|
||||
"asAdded": "Zoals toegevoegd",
|
||||
"appSortOrder": "App sorteervolgorde",
|
||||
"ascending": "Oplopend",
|
||||
"descending": "Aflopend",
|
||||
"bgUpdateCheckInterval": "Frequentie voor achtergrondupdatecontrole",
|
||||
"neverManualOnly": "Nooit - Alleen handmatig",
|
||||
"appearance": "Weergave",
|
||||
"showWebInAppView": "Toon de bronwebpagina in app-weergave",
|
||||
"pinUpdates": "Updates bovenaan in de apps-weergave vastpinnen",
|
||||
"updates": "Updates",
|
||||
"sourceSpecific": "Bron-specifiek",
|
||||
"appSource": "App bron",
|
||||
"noLogs": "Geen logs",
|
||||
"appLogs": "App logs",
|
||||
"close": "Sluiten",
|
||||
"share": "Delen",
|
||||
"appNotFound": "App niet gevonden",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Kies een APK",
|
||||
"appHasMoreThanOnePackage": "{} heeft meer dan één package:",
|
||||
"deviceSupportsXArch": "Jouw apparaat support de {} CPU-architectuur.",
|
||||
"deviceSupportsFollowingArchs": "Je apparaat ondersteunt de volgende CPU-architecturen:",
|
||||
"warning": "Waarschuwing",
|
||||
"sourceIsXButPackageFromYPrompt": "De appbron is '{}' maar de release package komt van '{}'. Doorgaan?",
|
||||
"updatesAvailable": "Updates beschikbaar",
|
||||
"updatesAvailableNotifDescription": "Stelt de gebruiker op de hoogte dat er updates beschikbaar zijn voor één of meer apps die worden bijgehouden door Obtainium.",
|
||||
"noNewUpdates": "Geen nieuwe updates.",
|
||||
"xHasAnUpdate": "{} heeft een update.",
|
||||
"appsUpdated": "Apps bijgewerkt",
|
||||
"appsNotUpdated": "Applicaties konden niet worden bijgewerkt",
|
||||
"appsUpdatedNotifDescription": "Stelt de gebruiker op de hoogte dat updates voor één of meer apps in de achtergrond zijn toegepast.",
|
||||
"xWasUpdatedToY": "{} is bijgewerkt naar {}.",
|
||||
"xWasNotUpdatedToY": "Het bijwerken van {} naar {} is mislukt.",
|
||||
"errorCheckingUpdates": "Fout bij het controleren op updates",
|
||||
"errorCheckingUpdatesNotifDescription": "Een melding die verschijnt wanneer het controleren op updates in de achtergrond mislukt",
|
||||
"appsRemoved": "Apps verwijderd",
|
||||
"appsRemovedNotifDescription": "Stelt de gebruiker op de hoogte dat één of meer apps zijn verwijderd vanwege fouten tijdens het laden ervan",
|
||||
"xWasRemovedDueToErrorY": "{} is verwijderd vanwege deze foutmelding: {}",
|
||||
"completeAppInstallation": "Complete app installatie",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium moet geopend zijn om apps te installeren",
|
||||
"completeAppInstallationNotifDescription": "Vraagt de gebruiker om terug te keren naar Obtainium om de installatie van een app af te ronden",
|
||||
"checkingForUpdates": "Controleren op updates",
|
||||
"checkingForUpdatesNotifDescription": "Tijdelijke melding die verschijnt tijdens het controleren op updates",
|
||||
"pleaseAllowInstallPerm": "Sta Obtainium toe om apps te installeren",
|
||||
"trackOnly": "Alleen track",
|
||||
"errorWithHttpStatusCode": "Foutmelding {}",
|
||||
"versionCorrectionDisabled": "Versiecorrectie uitgeschakeld (de plug-in lijkt niet te werken)",
|
||||
"unknown": "Onbekend",
|
||||
"none": "Geen",
|
||||
"never": "Nooit",
|
||||
"latestVersionX": "Laatste versie: {}",
|
||||
"installedVersionX": "Geïnstalleerde versie: {}",
|
||||
"lastUpdateCheckX": "Laatste updatecontrole: {}",
|
||||
"remove": "Verwijderen",
|
||||
"yesMarkUpdated": "Ja, markeer als bijgewerkt",
|
||||
"fdroid": "F-Droid-ambtenaar",
|
||||
"appIdOrName": "App ID of naam",
|
||||
"appId": "App-ID",
|
||||
"appWithIdOrNameNotFound": "Er werd geen app gevonden met dat ID of die naam",
|
||||
"reposHaveMultipleApps": "Repositories kunnen meerdere apps bevatten",
|
||||
"fdroidThirdPartyRepo": "F-Droid Repository van derden",
|
||||
"steamMobile": "Stoommobiel",
|
||||
"steamChat": "Steamchat",
|
||||
"install": "Installeren",
|
||||
"markInstalled": "Als geïnstalleerd markere",
|
||||
"update": "Update",
|
||||
"markUpdated": "Markeren als bijgewerkt",
|
||||
"additionalOptions": "Aanvullende opties",
|
||||
"disableVersionDetection": "Versieherkenning uitschakelen",
|
||||
"noVersionDetectionExplanation": "Deze optie moet alleen worden gebruikt voor apps waar versieherkenning niet correct werkt.",
|
||||
"downloadingX": "Downloaden {}",
|
||||
"downloadX": "Downloaden",
|
||||
"downloadedX": "Gedownload {}",
|
||||
"releaseAsset": "Release Activa",
|
||||
"downloadNotifDescription": "Stelt de gebruiker op de hoogte van de voortgang bij het downloaden van een app",
|
||||
"noAPKFound": "Geen APK gevonden",
|
||||
"noVersionDetection": "Geen versieherkenning",
|
||||
"categorize": "Categoriseren",
|
||||
"categories": "Categorieën",
|
||||
"category": "Categorie",
|
||||
"noCategory": "Geen categorie",
|
||||
"noCategories": "Geen categorieën",
|
||||
"deleteCategoriesQuestion": "Categorieën verwijderen?",
|
||||
"categoryDeleteWarning": "Alle apps in verwijderde categorieën worden teruggezet naar 'ongecategoriseerd'.",
|
||||
"addCategory": "Categorie toevoegen",
|
||||
"label": "Etiket",
|
||||
"language": "Taal",
|
||||
"copiedToClipboard": "Gekopieerd naar klembord",
|
||||
"storagePermissionDenied": "Toegang tot opslag geweigerd",
|
||||
"selectedCategorizeWarning": "Dit zal eventuele bestaande categorie-instellingen voor de geselecteerde apps vervangen.",
|
||||
"filterAPKsByRegEx": "Filter APK's op reguliere expressie",
|
||||
"removeFromObtainium": "Verwijder van Obtainium",
|
||||
"uninstallFromDevice": "Verwijder van apparaat",
|
||||
"onlyWorksWithNonVersionDetectApps": "Werkt alleen voor apps waarbij versieherkenning is uitgeschakeld.",
|
||||
"releaseDateAsVersion": "Gebruik de releasedatum als versie",
|
||||
"releaseDateAsVersionExplanation": "Deze optie moet alleen worden gebruikt voor apps waar versieherkenning niet correct werkt, maar waar wel een releasedatum beschikbaar is.",
|
||||
"changes": "Veranderingen",
|
||||
"releaseDate": "Releasedatum",
|
||||
"importFromURLsInFile": "Importeren vanaf URL's in een bestand (zoals OPML)",
|
||||
"versionDetectionExplanation": "Versiereeks afstemmen met versie gedetecteerd door besturingssysteem",
|
||||
"versionDetection": "Versieherkenning",
|
||||
"standardVersionDetection": "Standaard versieherkenning",
|
||||
"groupByCategory": "Groepeer op categorie",
|
||||
"autoApkFilterByArch": "Poging om APK's te filteren op CPU-architectuur indien mogelijk",
|
||||
"overrideSource": "Bron overschrijven",
|
||||
"dontShowAgain": "Laat dit niet meer zien",
|
||||
"dontShowTrackOnlyWarnings": "Geen waarschuwingen voor 'Track-Only' weergeven",
|
||||
"dontShowAPKOriginWarnings": "APK-herkomstwaarschuwingen niet weergeven",
|
||||
"moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave",
|
||||
"gitlabPATLabel": "GitLab persoonlijk toegangskenmerk",
|
||||
"about": "Over",
|
||||
"requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)",
|
||||
"checkOnStart": "Controleren op updates bij opstarten",
|
||||
"tryInferAppIdFromCode": "Probeer de app-ID af te leiden uit de broncode",
|
||||
"removeOnExternalUninstall": "Automatisch extern verwijderde apps verwijderen",
|
||||
"pickHighestVersionCode": "Automatisch de APK met de hoogste versiecode selecteren",
|
||||
"checkUpdateOnDetailPage": "Controleren op updates bij het openen van een app-detailpagina",
|
||||
"disablePageTransitions": "Schakel overgangsanimaties tussen pagina's uit",
|
||||
"reversePageTransitions": "Omgekeerde overgangsanimaties tussen pagina's",
|
||||
"minStarCount": "Minimale Github Stars",
|
||||
"addInfoBelow": "Voeg deze informatie hieronder toe.",
|
||||
"addInfoInSettings": "Voeg deze informatie toe in de instellingen.",
|
||||
"githubSourceNote": "Beperkingen van GitHub kunnen worden vermeden door het gebruik van een API-sleutel.",
|
||||
"sortByLastLinkSegment": "Sorteren op alleen het laatste segment van de link",
|
||||
"filterReleaseNotesByRegEx": "Filter release-opmerkingen met een reguliere expressie.",
|
||||
"customLinkFilterRegex": "Aangepaste APK-linkfilter met een reguliere expressie (Standaard '.apk$').",
|
||||
"appsPossiblyUpdated": "Poging tot app-updates",
|
||||
"appsPossiblyUpdatedNotifDescription": "Stelt de gebruiker op de hoogte dat updates voor één of meer apps mogelijk in de achtergrond zijn toegepast",
|
||||
"xWasPossiblyUpdatedToY": "{} mogelijk bijgewerkt naar {}.",
|
||||
"enableBackgroundUpdates": "Achtergrondupdates inschakelen",
|
||||
"backgroundUpdateReqsExplanation": "Achtergrondupdates zijn mogelijk niet voor alle apps mogelijk.",
|
||||
"backgroundUpdateLimitsExplanation": "Het succes van een installatie in de achtergrond kan alleen worden bepaald wanneer Obtainium is geopend.",
|
||||
"verifyLatestTag": "Verifieer de 'Laatste'-tag",
|
||||
"intermediateLinkRegex": "Filter voor een 'Intermediaire' link om te bezoeken",
|
||||
"filterByLinkText": "Links filteren op linktekst",
|
||||
"intermediateLinkNotFound": "Tussenliggende link niet gevonden",
|
||||
"intermediateLink": "Intermediaire link",
|
||||
"exemptFromBackgroundUpdates": "Vrijgesteld van achtergrondupdates (indien ingeschakeld)",
|
||||
"bgUpdatesOnWiFiOnly": "Achtergrondupdates uitschakelen wanneer niet verbonden met WiFi",
|
||||
"autoSelectHighestVersionCode": "Automatisch de APK met de hoogste versiecode selecteren",
|
||||
"versionExtractionRegEx": "Reguliere expressie voor versie-extractie",
|
||||
"matchGroupToUse": "Overeenkomende groep om te gebruiken voor de reguliere expressie voor versie-extractie",
|
||||
"highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.",
|
||||
"pickExportDir": "Kies de exportmap",
|
||||
"autoExportOnChanges": "Automatisch exporteren bij wijzigingen",
|
||||
"includeSettings": "Instellingen opnemen",
|
||||
"filterVersionsByRegEx": "Filter versies met een reguliere expressie",
|
||||
"trySelectingSuggestedVersionCode": "Probeer de voorgestelde versiecode APK te selecteren",
|
||||
"dontSortReleasesList": "Volgorde van releases behouden vanuit de API",
|
||||
"reverseSort": "Sortering omkeren",
|
||||
"takeFirstLink": "Neem de eerste link",
|
||||
"skipSort": "Sorteren overslaan",
|
||||
"debugMenu": "Debug menu",
|
||||
"bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.",
|
||||
"runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit",
|
||||
"versionExtractWholePage": "De reguliere expressie voor versie-extractie toepassen op de hele pagina",
|
||||
"installing": "Installeren",
|
||||
"skipUpdateNotifications": "Updatemeldingen overslaan",
|
||||
"updatesAvailableNotifChannel": "Updates beschikbaar",
|
||||
"appsUpdatedNotifChannel": "Apps bijgewerkt",
|
||||
"appsPossiblyUpdatedNotifChannel": "Poging tot app-updates",
|
||||
"errorCheckingUpdatesNotifChannel": "Foutcontrole bij het zoeken naar updates",
|
||||
"appsRemovedNotifChannel": "Apps verwijderd",
|
||||
"downloadingXNotifChannel": "{} downloaden",
|
||||
"completeAppInstallationNotifChannel": "Voltooien van de app-installatie",
|
||||
"checkingForUpdatesNotifChannel": "Controleren op updates",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates",
|
||||
"supportFixedAPKURL": "Ondersteuning vaste APK URL's",
|
||||
"selectX": "Selecteer {}",
|
||||
"parallelDownloads": "Parallelle downloads toestaan",
|
||||
"useShizuku": "Gebruik Shizuku of Sui om te installeren",
|
||||
"shizukuBinderNotFound": "Shizuku draait niet",
|
||||
"shizukuOld": "Oude Shizuku-versie (<11) - bijwerken",
|
||||
"shizukuOldAndroidWithADB": "Shizuku draait op Android < 8.1 met ADB - update Android of gebruik Sui in plaats daarvan",
|
||||
"shizukuPretendToBeGooglePlay": "Google Play instellen als installatiebron (als Shizuku wordt gebruikt)",
|
||||
"useSystemFont": "Gebruik het systeemlettertype",
|
||||
"useVersionCodeAsOSVersion": "Gebruik app versieCode als door OS gedetecteerde versie",
|
||||
"requestHeader": "Verzoekkoptekst",
|
||||
"useLatestAssetDateAsReleaseDate": "Gebruik laatste upload als releasedatum",
|
||||
"defaultPseudoVersioningMethod": "Standaard pseudo-versiebeheermethode",
|
||||
"partialAPKHash": "Gedeeltelijke APK-hash",
|
||||
"APKLinkHash": "APK-link-hash",
|
||||
"directAPKLink": "Directe APK-link",
|
||||
"pseudoVersionInUse": "Er is een pseudo-versie in gebruik",
|
||||
"installed": "Geïnstalleerd",
|
||||
"latest": "Laatste",
|
||||
"invertRegEx": "Reguliere expressie omkeren",
|
||||
"note": "Opmerking",
|
||||
"selfHostedNote": "De \"{}\" dropdown kan gebruikt worden om zelf gehoste/aangepaste instanties van elke bron te bereiken.",
|
||||
"badDownload": "De APK kon niet worden verwerkt (incompatibele of gedeeltelijke download)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Nieuwe Apps delen met AppVerifier (indien beschikbaar)",
|
||||
"appVerifierInstructionToast": "Deel naar AppVerifier en keer hier terug als je klaar bent.",
|
||||
"removeAppQuestion": {
|
||||
"one": "App verwijderen?",
|
||||
"other": "Apps verwijderen?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Te veel verzoeken (aantal beperkt) - probeer het opnieuw in {} minuut",
|
||||
"other": "Te veel verzoeken (aantal beperkt) - probeer het opnieuw in {} minuten"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Achtergrondupdatecontrole heeft een {}, zal een hercontrole plannen over {} minuut",
|
||||
"other": "Achtergrondupdatecontrole heeft een {}, zal een hercontrole plannen over {} minuten"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Achtergrondupdatecontrole heeft {} update gevonden - zal de gebruiker op de hoogte stellen indien nodig",
|
||||
"other": "Achtergrondupdatecontrole heeft {} updates gevonden - zal de gebruiker op de hoogte stellen indien nodig"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} app",
|
||||
"other": "{} apps"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URL's"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} minuut",
|
||||
"other": "{} minuten"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} uur",
|
||||
"other": "{} uur"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} dag",
|
||||
"other": "{} dagen"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "{n} logboekitem gewist (voor = {before}, na = {after})",
|
||||
"other": "{n} logboekitems gewist (voor = {before}, na = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} en nog 1 app hebben updates.",
|
||||
"other": "{} en {} meer apps hebben updates."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} en nog 1 app is bijgewerkt.",
|
||||
"other": "{} en {} meer apps zijn bijgewerkt."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Bijwerken mislukt {} en nog 1 app.",
|
||||
"other": "Mislukt om {} en {} meer apps bij te werken."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} en nog 1 app zijn mogelijk bijgewerkt.",
|
||||
"other": "{} en {} meer apps zijn mogelijk bijgwerkt."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK's"
|
||||
}
|
||||
}
|
||||
398
assets/translations/pl.json
Normal file
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"invalidURLForSource": "Nieprawidłowy adres URL aplikacji {}",
|
||||
"noReleaseFound": "Nie można znaleźć odpowiedniego wydania",
|
||||
"noVersionFound": "Nie można określić wersji wydania",
|
||||
"urlMatchesNoSource": "Adres URL nie pasuje do znanego źródła",
|
||||
"cantInstallOlderVersion": "Nie można zainstalować starszej wersji aplikacji",
|
||||
"appIdMismatch": "Pobrane ID pakietu nie pasuje do istniejącego ID aplikacji",
|
||||
"functionNotImplemented": "Ta klasa nie zaimplementowała tej funkcji",
|
||||
"placeholder": "Zbiornik",
|
||||
"someErrors": "Wystąpiły pewne błędy",
|
||||
"unexpectedError": "Nieoczekiwany błąd",
|
||||
"ok": "Okej",
|
||||
"and": "i",
|
||||
"githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)",
|
||||
"includePrereleases": "Uwzględnij wersje wstępne",
|
||||
"fallbackToOlderReleases": "Powracaj do starszych wersji",
|
||||
"filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych",
|
||||
"invalidRegEx": "Nieprawidłowe wyrażenie regularne",
|
||||
"noDescription": "Brak opisu",
|
||||
"cancel": "Anuluj",
|
||||
"continue": "Kontynuuj",
|
||||
"requiredInBrackets": "(Wymagane)",
|
||||
"dropdownNoOptsError": "BŁĄD: LISTA ROZWIJANA MUSI MIEĆ CO NAJMNIEJ JEDNĄ OPCJĘ",
|
||||
"colour": "Kolor",
|
||||
"standard": "Standard",
|
||||
"custom": "Niestandardowe",
|
||||
"useMaterialYou": "Używaj materiałów",
|
||||
"githubStarredRepos": "Repozytoria GitHub oznaczone gwiazdką",
|
||||
"uname": "Nazwa użytkownika",
|
||||
"wrongArgNum": "Nieprawidłowa liczba podanych argumentów",
|
||||
"xIsTrackOnly": "{} jest tylko obserwowane",
|
||||
"source": "Źródło",
|
||||
"app": "Aplikacja",
|
||||
"appsFromSourceAreTrackOnly": "Aplikacje z tego źródła są tylko obserwowane.",
|
||||
"youPickedTrackOnly": "Wybrano opcję \"Tylko obserwuj\".",
|
||||
"trackOnlyAppDescription": "Aplikacja będzie obserwowana pod kątem aktualizacji, ale Obtainium nie będzie w stanie jej pobrać ani zainstalować.",
|
||||
"cancelled": "Anulowano",
|
||||
"appAlreadyAdded": "Aplikacja już została dodana",
|
||||
"alreadyUpToDateQuestion": "Aplikacja jest już aktualna?",
|
||||
"addApp": "Dodaj apkę",
|
||||
"appSourceURL": "Adres URL źródła aplikacji",
|
||||
"error": "Błąd",
|
||||
"add": "Dodaj",
|
||||
"searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)",
|
||||
"search": "Szukaj",
|
||||
"additionalOptsFor": "Dodatkowe opcje dla {}",
|
||||
"supportedSources": "Obsługiwane źródła",
|
||||
"trackOnlyInBrackets": "(tylko obserwowane)",
|
||||
"searchableInBrackets": "(wyszukiwalne)",
|
||||
"appsString": "Aplikacje",
|
||||
"noApps": "Brak aplikacji",
|
||||
"noAppsForFilter": "Brak aplikacji dla filtra",
|
||||
"byX": "Autorstwa {}",
|
||||
"percentProgress": "Postęp: {}%",
|
||||
"pleaseWait": "Proszę czekać",
|
||||
"updateAvailable": "Dostępna aktualizacja",
|
||||
"notInstalled": "Nie zainstalowano",
|
||||
"pseudoVersion": "pseudowersja",
|
||||
"selectAll": "Zaznacz wszystkie",
|
||||
"deselectX": "Odznacz {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.",
|
||||
"removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?",
|
||||
"removeSelectedApps": "Usuń wybrane aplikacje",
|
||||
"updateX": "Zaktualizuj {}",
|
||||
"installX": "Zainstaluj {}",
|
||||
"markXTrackOnlyAsUpdated": "Oznacz {}\n(tylko obserwowana)\njako zaktualizowaną",
|
||||
"changeX": "Zmień {}",
|
||||
"installUpdateApps": "Instaluj/aktualizuj aplikacje",
|
||||
"installUpdateSelectedApps": "Zainstaluj/zaktualizuj wybrane aplikacje",
|
||||
"markXSelectedAppsAsUpdated": "Oznaczyć {} wybranych aplikacji jako zaktualizowane?",
|
||||
"no": "Nie",
|
||||
"yes": "Tak",
|
||||
"markSelectedAppsUpdated": "Oznacz wybrane aplikacje jako zaktualizowane",
|
||||
"pinToTop": "Przypnij",
|
||||
"unpinFromTop": "Odepnij",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Zresetować status instalacji dla wybranych aplikacji?",
|
||||
"installStatusOfXWillBeResetExplanation": "Stan instalacji wybranych aplikacji zostanie zresetowany.\n\nMoże być to pomocne, gdy wersja aplikacji wyświetlana w Obtainium jest nieprawidłowa z powodu nieudanych aktualizacji lub innych problemów.",
|
||||
"customLinkMessage": "Te linki działają na urządzeniach z zainstalowanym Obtainium",
|
||||
"shareAppConfigLinks": "Udostępnij konfigurację aplikacji jako link HTML",
|
||||
"shareSelectedAppURLs": "Udostępnij wybrane adresy URL aplikacji",
|
||||
"resetInstallStatus": "Zresetuj stan instalacji",
|
||||
"more": "Więcej",
|
||||
"removeOutdatedFilter": "Usuń filtr nieaktualnych aplikacji",
|
||||
"showOutdatedOnly": "Pokaż tylko nieaktualne aplikacje",
|
||||
"filter": "FIltr",
|
||||
"filterApps": "Filtruj aplikacje",
|
||||
"appName": "Nazwa aplikacji",
|
||||
"author": "Autor",
|
||||
"upToDateApps": "Aktualne aplikacje",
|
||||
"nonInstalledApps": "Niezainstalowane aplikacje",
|
||||
"importExport": "Import/Eksport",
|
||||
"settings": "Ustawienia",
|
||||
"exportedTo": "Wyeksportowano do {}",
|
||||
"obtainiumExport": "Eksportuj Obtainium",
|
||||
"invalidInput": "Nieprawidłowe wprowadzenie",
|
||||
"importedX": "Zaimportowano {}",
|
||||
"obtainiumImport": "Import Obtainium",
|
||||
"importFromURLList": "Importuj z listy adresów URL",
|
||||
"searchQuery": "Wyszukiwane zapytanie",
|
||||
"appURLList": "Lista adresów URL aplikacji",
|
||||
"line": "Linia",
|
||||
"searchX": "Przeszukaj {}",
|
||||
"noResults": "Nie znaleziono wyników",
|
||||
"importX": "Importuj {}",
|
||||
"importedAppsIdDisclaimer": "Zaimportowane aplikacje mogą być wyświetlane jako niezainstalowane.\nAby to naprawić, przeinstaluj je za pomocą Obtainium.\nNie powinno to mieć wpływu na dane aplikacji.\n\nDotyczy tylko adresu URL i innych metod importu.",
|
||||
"importErrors": "Błędy importowania",
|
||||
"importedXOfYApps": "Zaimportowano {} z {} aplikacji.",
|
||||
"followingURLsHadErrors": "Następujące adresy URL zawierały błędy:",
|
||||
"selectURL": "Wybierz adres URL",
|
||||
"selectURLs": "Wybierz adresy URL",
|
||||
"pick": "Wybierz",
|
||||
"theme": "Motyw",
|
||||
"dark": "Ciemny",
|
||||
"light": "Jasny",
|
||||
"followSystem": "Zgodny z systemem",
|
||||
"useBlackTheme": "Użyj czarnego motywu",
|
||||
"appSortBy": "Sortuj aplikacje według",
|
||||
"authorName": "Autor/Nazwa",
|
||||
"nameAuthor": "Nazwa/Autor",
|
||||
"asAdded": "Dodania",
|
||||
"appSortOrder": "Kolejność sortowania aplikacji",
|
||||
"ascending": "Rosnąco",
|
||||
"descending": "Malejąco",
|
||||
"bgUpdateCheckInterval": "Częstotliwość sprawdzania aktualizacji w tle",
|
||||
"neverManualOnly": "Nigdy - tylko ręcznie",
|
||||
"appearance": "Wygląd",
|
||||
"showWebInAppView": "Pokaż stronę źródłową w widoku aplikacji",
|
||||
"pinUpdates": "Przypnij aktualizacje na górze widoku aplikacji",
|
||||
"updates": "Aktualizacje",
|
||||
"sourceSpecific": "Zależnie od źródła",
|
||||
"appSource": "Źródło aplikacji",
|
||||
"noLogs": "Brak logów",
|
||||
"appLogs": "Logi aplikacji",
|
||||
"close": "Zamknij",
|
||||
"share": "Udostępnij",
|
||||
"appNotFound": "Nie znaleziono aplikacji",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-eksport",
|
||||
"pickAnAPK": "Wybierz plik APK",
|
||||
"appHasMoreThanOnePackage": "{} ma więcej niż jeden pakiet:",
|
||||
"deviceSupportsXArch": "Urządzenie obsługuje architekturę procesora {}.",
|
||||
"deviceSupportsFollowingArchs": "Urządzenie obsługuje następujące architektury procesora:",
|
||||
"warning": "Uwaga",
|
||||
"sourceIsXButPackageFromYPrompt": "Źródłem aplikacji jest '{}', ale pakiet wydania pochodzi z '{}'. Kontynuować?",
|
||||
"updatesAvailable": "Dostępne aktualizacje",
|
||||
"updatesAvailableNotifDescription": "Informuje o dostępności aktualizacji dla jednej lub więcej aplikacji obserwowanych przez Obtainium",
|
||||
"noNewUpdates": "Brak nowych aktualizacji.",
|
||||
"xHasAnUpdate": "{} ma aktualizację.",
|
||||
"appsUpdated": "Zaktualizowano aplikacje",
|
||||
"appsNotUpdated": "Nie udało się zaktualizować aplikacji",
|
||||
"appsUpdatedNotifDescription": "Informuje, gdy co najmniej jedna aplikacja została zaktualizowana w tle",
|
||||
"xWasUpdatedToY": "{} zaktualizowano do {}.",
|
||||
"xWasNotUpdatedToY": "Nie udało się zaktualizować {} do {}.",
|
||||
"errorCheckingUpdates": "Błąd sprawdzania aktualizacji",
|
||||
"errorCheckingUpdatesNotifDescription": "Jest wyświetlane, gdy sprawdzanie aktualizacji w tle nie powiedzie się",
|
||||
"appsRemoved": "Usunięte aplikacje",
|
||||
"appsRemovedNotifDescription": "Informuje, gdy co najmniej jedna aplikacja została usunięta z powodu błędów podczas wczytywania",
|
||||
"xWasRemovedDueToErrorY": "Usunięto {} z powodu błędu: {}",
|
||||
"completeAppInstallation": "Ukończenie instalacji aplikacji",
|
||||
"obtainiumMustBeOpenToInstallApps": "Aby zainstalować aplikacje, Obtainium musi być otwarte",
|
||||
"completeAppInstallationNotifDescription": "Informuje o możliwości powrotu do Obtainium w celu dokończenia instalacji aplikacji",
|
||||
"checkingForUpdates": "Sprawdzanie aktualizacji",
|
||||
"checkingForUpdatesNotifDescription": "Tymczasowe powiadomienie pojawiające się podczas sprawdzania aktualizacji",
|
||||
"pleaseAllowInstallPerm": "Pozwól Obtainium instalować aplikacje",
|
||||
"trackOnly": "Tylko obserwuj",
|
||||
"errorWithHttpStatusCode": "Błąd {}",
|
||||
"versionCorrectionDisabled": "Korekta wersji wyłączona (wtyczka wydaje się nie działać)",
|
||||
"unknown": "Nieznane",
|
||||
"none": "Brak",
|
||||
"never": "Nigdy",
|
||||
"latestVersionX": "Najnowsza wersja: {}",
|
||||
"installedVersionX": "Zainstalowana wersja: {}",
|
||||
"lastUpdateCheckX": "Ostatnio sprawdzono: {}",
|
||||
"remove": "Usuń",
|
||||
"yesMarkUpdated": "Tak, oznacz jako zaktualizowane",
|
||||
"fdroid": "Oficjalny F-Droid",
|
||||
"appIdOrName": "ID aplikacji lub nazwa",
|
||||
"appId": "ID aplikacji",
|
||||
"appWithIdOrNameNotFound": "Nie znaleziono aplikacji o tym identyfikatorze lub nazwie",
|
||||
"reposHaveMultipleApps": "Repozytoria mogą zawierać wiele aplikacji",
|
||||
"fdroidThirdPartyRepo": "Zewnętrzne repo F-Droid",
|
||||
"steamMobile": "Mobilny Steam",
|
||||
"steamChat": "Czat Steam",
|
||||
"install": "Instaluj",
|
||||
"markInstalled": "Oznacz jako zainstalowane",
|
||||
"update": "Zaktualizuj",
|
||||
"markUpdated": "Oznacz jako zaktualizowane",
|
||||
"additionalOptions": "Dodatkowe opcje",
|
||||
"disableVersionDetection": "Wyłącz wykrywanie wersji",
|
||||
"noVersionDetectionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie.",
|
||||
"downloadingX": "Pobieranie {}",
|
||||
"downloadX": "Pobierz {}",
|
||||
"downloadedX": "Pobrano {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Informuje o postępach w pobieraniu aplikacji",
|
||||
"noAPKFound": "Nie znaleziono pakietu APK",
|
||||
"noVersionDetection": "Bez wykrywania wersji",
|
||||
"categorize": "Kategoryzuj",
|
||||
"categories": "Kategorie",
|
||||
"category": "Kategoria",
|
||||
"noCategory": "Bez kategorii",
|
||||
"noCategories": "Brak kategorii",
|
||||
"deleteCategoriesQuestion": "Usunąć kategorie?",
|
||||
"categoryDeleteWarning": "Wszystkie aplikacje w usuniętych kategoriach zostaną ustawione jako nieskategoryzowane.",
|
||||
"addCategory": "Dodaj kategorię",
|
||||
"label": "Etykieta",
|
||||
"language": "Język",
|
||||
"copiedToClipboard": "Skopiowano do schowka",
|
||||
"storagePermissionDenied": "Odmówiono zezwolenia dostępu do pamięci",
|
||||
"selectedCategorizeWarning": "Spowoduje to zastąpienie wszystkich istniejących ustawień kategorii dla wybranych aplikacji.",
|
||||
"filterAPKsByRegEx": "Filtruj pliki APK według wyrażeń regularnych",
|
||||
"removeFromObtainium": "Usuń z Obtainium",
|
||||
"uninstallFromDevice": "Odinstaluj z urządzenia",
|
||||
"onlyWorksWithNonVersionDetectApps": "Działa tylko w przypadku aplikacji z wyłączonym wykrywaniem wersji.",
|
||||
"releaseDateAsVersion": "Użyj daty wydania jako wersji",
|
||||
"releaseDateAsVersionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie, ale dostępna jest data wydania.",
|
||||
"changes": "Zmiany",
|
||||
"releaseDate": "Data wydania",
|
||||
"importFromURLsInFile": "Importuj z adresów URL w pliku (typu OPML)",
|
||||
"versionDetectionExplanation": "Uzgodnij ciąg wersji z wersją wykrytą w systemie operacyjnym",
|
||||
"versionDetection": "Wykrywanie wersji",
|
||||
"standardVersionDetection": "Standardowe wykrywanie wersji",
|
||||
"groupByCategory": "Grupuj według kategorii",
|
||||
"autoApkFilterByArch": "Spróbuj filtrować pliki APK według architektury procesora, jeśli to możliwe",
|
||||
"overrideSource": "Nadpisz źródło",
|
||||
"dontShowAgain": "Nie pokazuj tego ponownie",
|
||||
"dontShowTrackOnlyWarnings": "Nie pokazuj ostrzeżeń \"Tylko obserwowana\"",
|
||||
"dontShowAPKOriginWarnings": "Nie pokazuj ostrzeżeń o pochodzeniu APK",
|
||||
"moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji",
|
||||
"gitlabPATLabel": "Osobisty token dostępu GitLab",
|
||||
"about": "Więcej informacji",
|
||||
"requiresCredentialsInSettings": "{}: Wymaga to dodatkowych poświadczeń (w Ustawieniach)",
|
||||
"checkOnStart": "Sprawdź aktualizacje przy uruchomieniu",
|
||||
"tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego",
|
||||
"removeOnExternalUninstall": "Automatyczne usuń odinstalowane zewnętrznie aplikacje",
|
||||
"pickHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
|
||||
"checkUpdateOnDetailPage": "Sprawdzaj aktualizacje podczas otwierania strony szczegółów aplikacji",
|
||||
"disablePageTransitions": "Wyłącz animacje przejścia między stronami",
|
||||
"reversePageTransitions": "Odwróć animacje przejścia pomiędzy stronami",
|
||||
"minStarCount": "Minimalna ilość gwiazdek",
|
||||
"addInfoBelow": "Dodaj tę informację poniżej.",
|
||||
"addInfoInSettings": "Dodaj tę informację w Ustawieniach.",
|
||||
"githubSourceNote": "Limit żądań GitHub można ominąć za pomocą klucza API.",
|
||||
"sortByLastLinkSegment": "Sortuj tylko według ostatniego segmentu łącza",
|
||||
"filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego",
|
||||
"customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
|
||||
"appsPossiblyUpdated": "Aplikacje mogły zostać zaktualizowane",
|
||||
"appsPossiblyUpdatedNotifDescription": "Powiadamia, gdy co najmniej jedna aktualizacja aplikacji została potencjalnie zastosowana w tle",
|
||||
"xWasPossiblyUpdatedToY": "{} być może zaktualizowano do {}.",
|
||||
"enableBackgroundUpdates": "Włącz aktualizacje w tle",
|
||||
"backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.",
|
||||
"backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.",
|
||||
"verifyLatestTag": "Zweryfikuj najnowszy tag",
|
||||
"intermediateLinkRegex": "Filtruj link \"pośredni\" do odwiedzenia",
|
||||
"filterByLinkText": "Filtruj linki według tekstu linku",
|
||||
"intermediateLinkNotFound": "Nie znaleziono linku pośredniego",
|
||||
"intermediateLink": "Łącze pośrednie",
|
||||
"exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
|
||||
"bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
|
||||
"autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
|
||||
"versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję",
|
||||
"matchGroupToUse": "Dopasuj grupę do użycia dla wyrażenia regularnego wyodrębniania wersji",
|
||||
"highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
|
||||
"pickExportDir": "Wybierz katalog eksportu",
|
||||
"autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
|
||||
"includeSettings": "Dołącz ustawienia",
|
||||
"filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
|
||||
"trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
|
||||
"dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API",
|
||||
"reverseSort": "Odwrotne sortowanie",
|
||||
"takeFirstLink": "Weź pierwszy link",
|
||||
"skipSort": "Pomiń sortowanie",
|
||||
"debugMenu": "Menu debugowania",
|
||||
"bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.",
|
||||
"runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle",
|
||||
"versionExtractWholePage": "Zastosuj wyrażenie regularne wyodrębniania wersji dla całej strony",
|
||||
"installing": "Instalacja",
|
||||
"skipUpdateNotifications": "Pomiń powiadomienia o aktualizacjach",
|
||||
"updatesAvailableNotifChannel": "Dostępne aktualizacje aplikacji",
|
||||
"appsUpdatedNotifChannel": "Zaktualizowane aplikacje",
|
||||
"appsPossiblyUpdatedNotifChannel": "Informuj o próbach aktualizacji",
|
||||
"errorCheckingUpdatesNotifChannel": "Błędy sprawdzania aktualizacji",
|
||||
"appsRemovedNotifChannel": "Usunięte aplikacje",
|
||||
"downloadingXNotifChannel": "Pobieranie aplikacji",
|
||||
"completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji",
|
||||
"checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji",
|
||||
"supportFixedAPKURL": "Obsługuj stałe adresy URL APK",
|
||||
"selectX": "Wybierz {}",
|
||||
"parallelDownloads": "Zezwól na pobieranie równoległe",
|
||||
"useShizuku": "Użyj Shizuku lub Sui, aby zainstalować",
|
||||
"shizukuBinderNotFound": "Shizuku is not running",
|
||||
"shizukuOld": "Stara wersja Shizuku (<11) - zaktualizuj ją",
|
||||
"shizukuOldAndroidWithADB": "Shizuku działa na Androidzie < 8.1 z ADB - zaktualizuj Androida lub użyj zamiast tego Sui",
|
||||
"shizukuPretendToBeGooglePlay": "Ustaw Google Play jako źródło instalacji (jeśli używana jest aplikacja Shizuku).",
|
||||
"useSystemFont": "Użyj czcionki systemowej",
|
||||
"useVersionCodeAsOSVersion": "Użyj kodu wersji aplikacji jako wersji wykrytej przez system operacyjny",
|
||||
"requestHeader": "Nagłówek żądania",
|
||||
"useLatestAssetDateAsReleaseDate": "Użyj najnowszego przesłanego zasobu jako daty wydania",
|
||||
"defaultPseudoVersioningMethod": "Domyślna metoda pseudowersji",
|
||||
"partialAPKHash": "Częściowy skrót APK",
|
||||
"APKLinkHash": "Skrót łącza APK",
|
||||
"directAPKLink": "Bezpośredni link APK",
|
||||
"pseudoVersionInUse": "Pseudowersja jest w użyciu",
|
||||
"installed": "Zainstalowano",
|
||||
"latest": "Najnowszy",
|
||||
"invertRegEx": "Odwróć wyrażenie regularne",
|
||||
"note": "Uwaga",
|
||||
"selfHostedNote": "Lista rozwijana \"{}\" może być używana do uzyskiwania dostępu do samodzielnie hostowanych / niestandardowych instancji dowolnego źródła.",
|
||||
"badDownload": "Nie można przeanalizować pliku APK (niekompatybilny lub częściowo pobrany).",
|
||||
"beforeNewInstallsShareToAppVerifier": "Udostępnianie nowych aplikacji za pomocą AppVerifier (jeśli dostępne)",
|
||||
"appVerifierInstructionToast": "Udostępnij w AppVerifier, a następnie wróć tutaj, gdy będziesz gotowy.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Usunąć aplikację?",
|
||||
"few": "Usunąć aplikacje?",
|
||||
"many": "Usunąć aplikacje?",
|
||||
"other": "Usunąć aplikacje?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minutę",
|
||||
"few": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minuty",
|
||||
"many": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minut",
|
||||
"other": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minuty"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minutę",
|
||||
"few": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minuty",
|
||||
"many": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minut",
|
||||
"other": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minuty"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "W tle znaleziono {} aktualizację - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||
"few": "W tle znaleziono {} aktualizacje - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||
"many": "W tle znaleziono {} aktualizacji - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||
"other": "W tle znaleziono {} aktualizacje - w razie potrzeby użytkownik zostanie o tym powiadomiony"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} apkę",
|
||||
"few": "{} apki",
|
||||
"many": "{} apek",
|
||||
"other": "{} apki"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} adres URL",
|
||||
"few": "{} adresy URL",
|
||||
"many": "{} adresów URL",
|
||||
"other": "{} adresy URL"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} minuta",
|
||||
"few": "{} minuty",
|
||||
"many": "{} minut",
|
||||
"other": "{} minuty"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} godzina",
|
||||
"few": "{} godziny",
|
||||
"many": "{} godzin",
|
||||
"other": "{} godziny"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} dzień",
|
||||
"few": "{} dni",
|
||||
"many": "{} dni",
|
||||
"other": "{} dni"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Wyczyszczono {n} log (przed = {before}, po = {after})",
|
||||
"few": "Wyczyszczono {n} logi (przed = {before}, po = {after})",
|
||||
"many": "Wyczyszczono {n} logów (przed = {before}, po = {after})",
|
||||
"other": "Wyczyszczono {n} logi (przed = {before}, po = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} i 1 inna apka mają aktualizacje.",
|
||||
"few": "{} i {} inne apki mają aktualizacje.",
|
||||
"many": "{} i {} innych apek ma aktualizacje.",
|
||||
"other": "{} i {} inne apki mają aktualizacje."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "Zaktualizowano {} i 1 inną apkę.",
|
||||
"few": "{} i {} inne apki zostały zaktualizowane.",
|
||||
"many": "{} i {} innych apek zostało zaktualizowanych.",
|
||||
"other": "{} i {} inne apki zostały zaktualizowane."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Nie udało się zaktualizować {} i 1 innej aplikacji.",
|
||||
"other": "Nie udało się zaktualizować {} i {} więcej aplikacji."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} i 1 inna apka mogły zostać zaktualizowane.",
|
||||
"few": "{} i {} inne apki mogły zostać zaktualizowane.",
|
||||
"many": "{} i {} innych apek mogło zostać zaktualizowanych.",
|
||||
"other": "{} i {} inne apki mogły zostać zaktualizowane."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK"
|
||||
}
|
||||
}
|
||||
372
assets/translations/pt.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "URL {} inválida",
|
||||
"noReleaseFound": "Não foi possível encontrar uma versão adequada",
|
||||
"noVersionFound": "Não foi possível encontrar uma versão",
|
||||
"urlMatchesNoSource": "A URL não corresponde a uma fonte conhecida",
|
||||
"cantInstallOlderVersion": "Não é permitido instalar uma versão anterior de um aplicativo",
|
||||
"appIdMismatch": "ID do pacote baixado não é igual ao ID do aplicativo instalado",
|
||||
"functionNotImplemented": "Esta classe não implementou essa função",
|
||||
"placeholder": "Espaço reservado",
|
||||
"someErrors": "Alguns erros ocorreram",
|
||||
"unexpectedError": "Erro inesperado",
|
||||
"ok": "OK",
|
||||
"and": "e",
|
||||
"githubPATLabel": "Token de acesso pessoal do GitHub\n(Reduz tempos de espera)",
|
||||
"includePrereleases": "Incluir pré-lançamentos",
|
||||
"fallbackToOlderReleases": "Retornar para versões anteriores",
|
||||
"filterReleaseTitlesByRegEx": "Filtrar títulos de versões usando expressão regular",
|
||||
"invalidRegEx": "Expressão regular inválida",
|
||||
"noDescription": "Sem descrição",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"requiredInBrackets": "(Necessário)",
|
||||
"dropdownNoOptsError": "ERRO: O DROPDOWN DEVE TER PELO MENOS UMA OPÇÃO",
|
||||
"colour": "Cor",
|
||||
"standard": "Padrão",
|
||||
"custom": "Personalizado",
|
||||
"useMaterialYou": "Utilizar o material que",
|
||||
"githubStarredRepos": "repositórios favoritos no GitHub",
|
||||
"uname": "Nome de usuário",
|
||||
"wrongArgNum": "Número de argumentos errado",
|
||||
"xIsTrackOnly": "{} é 'Apenas monitorar'",
|
||||
"source": "Fonte",
|
||||
"app": "Aplicativo",
|
||||
"appsFromSourceAreTrackOnly": "Os aplicativos desta fonte são 'Apenas monitorar'.",
|
||||
"youPickedTrackOnly": "Você selecionou a opção 'Apenas monitorar'.",
|
||||
"trackOnlyAppDescription": "As atualizações desse aplicativo serão monitoradas, mas o Obtainium não poderá baixá-lo ou instalá-lo.",
|
||||
"cancelled": "Cancelado",
|
||||
"appAlreadyAdded": "Aplicativo já adicionado",
|
||||
"alreadyUpToDateQuestion": "Aplicativo já foi atualizado?",
|
||||
"addApp": "Adicionar aplicativo",
|
||||
"appSourceURL": "URL de origem do aplicativo",
|
||||
"error": "Erro",
|
||||
"add": "Adicionar",
|
||||
"searchSomeSourcesLabel": "Procurar (apenas algumas fontes)",
|
||||
"search": "Procurar",
|
||||
"additionalOptsFor": "Opções adicionais para {}",
|
||||
"supportedSources": "Fontes compatíveis",
|
||||
"trackOnlyInBrackets": "(apenas monitorar)",
|
||||
"searchableInBrackets": "(pesquisável)",
|
||||
"appsString": "Aplicativos",
|
||||
"noApps": "Não há aplicativos",
|
||||
"noAppsForFilter": "Sem aplicativos para filtrar",
|
||||
"byX": "Por {}",
|
||||
"percentProgress": "Progresso: {}%",
|
||||
"pleaseWait": "Por favor, espere",
|
||||
"updateAvailable": "Atualização disponível",
|
||||
"notInstalled": "Não instalado",
|
||||
"pseudoVersion": "pseudo-versão",
|
||||
"selectAll": "Selecionar todos",
|
||||
"deselectX": "Deselecionar {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} será removido do Obtainium mais permanecerá instalado no dispositivo.",
|
||||
"removeSelectedAppsQuestion": "Remover aplicativos selecionados?",
|
||||
"removeSelectedApps": "Remover aplicativos selecionados",
|
||||
"updateX": "Atualizar {}",
|
||||
"installX": "Instalar {}",
|
||||
"markXTrackOnlyAsUpdated": "Marcar {}\n(Apenas monitorar)\ncomo Atualizado",
|
||||
"changeX": "Mudar {}",
|
||||
"installUpdateApps": "Instalar/Atualizar aplicativos",
|
||||
"installUpdateSelectedApps": "Instalar/Atualizar aplicativos selecionados",
|
||||
"markXSelectedAppsAsUpdated": "Marcar {} aplicativos selecionados como atualizados?",
|
||||
"no": "Não",
|
||||
"yes": "Sim",
|
||||
"markSelectedAppsUpdated": "Marcar aplicativos selecionados como Atualizados",
|
||||
"pinToTop": "Fixar no topo",
|
||||
"unpinFromTop": "Desafixar do topo",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Reiniciar status de instalação nos aplicativos selecionados?",
|
||||
"installStatusOfXWillBeResetExplanation": "O status de instalação de todos os aplicativos selecionados será reiniciado.\n\nIsso pode ajudar quando uma versão de um aplicativo mostrada no Obtainium é incorreta devido a falhas ao atualizar ou outros problemas.",
|
||||
"customLinkMessage": "Esses links funcionam em dispositivos com o Obtainium instalado",
|
||||
"shareAppConfigLinks": "Compartilhar configuração do aplicativo como link HTML",
|
||||
"shareSelectedAppURLs": "Compartilhar URLs de aplicativos selecionados",
|
||||
"resetInstallStatus": "Reiniciar status de instalação",
|
||||
"more": "Mais",
|
||||
"removeOutdatedFilter": "Remover filtro de aplicativos desatualizados",
|
||||
"showOutdatedOnly": "Mostrar apenas aplicativos desatualizados",
|
||||
"filter": "Filtro",
|
||||
"filterApps": "Filtrar aplicativos",
|
||||
"appName": "Nome do aplicativo",
|
||||
"author": "Autor",
|
||||
"upToDateApps": "Aplicativos atualizados",
|
||||
"nonInstalledApps": "Aplicativos não instalados",
|
||||
"importExport": "Importar/Exportar",
|
||||
"settings": "Configurações",
|
||||
"exportedTo": "Exportado para {}",
|
||||
"obtainiumExport": "Exportar dados do Obtainium",
|
||||
"invalidInput": "Entrada inválida",
|
||||
"importedX": "Importado {}",
|
||||
"obtainiumImport": "Importar dados do Obtainium",
|
||||
"importFromURLList": "Importar de lista de URLs",
|
||||
"searchQuery": "Pesquisa",
|
||||
"appURLList": "Lista de URLs de aplicativos",
|
||||
"line": "Linha",
|
||||
"searchX": "Pesquisar na/o {}",
|
||||
"noResults": "Nenhum resultado encontrado",
|
||||
"importX": "Importar {}",
|
||||
"importedAppsIdDisclaimer": "Aplicativos Importados podem ser mostrados incorretamente como \"Não Instalado\".\nPara consertar, reinstale-os usando o Obtainium.\nIsso não deve afetar dados do aplicativo.\n\nAfeta apenas métodos de importação de URL e de terceiros.",
|
||||
"importErrors": "Erros de importação",
|
||||
"importedXOfYApps": "{} de {} aplicativos importados.",
|
||||
"followingURLsHadErrors": "As seguintes URLs apresentaram erros:",
|
||||
"selectURL": "Selecionar URL",
|
||||
"selectURLs": "Selecionar URLs",
|
||||
"pick": "Escolher",
|
||||
"theme": "Tema",
|
||||
"dark": "Escuro",
|
||||
"light": "Claro",
|
||||
"followSystem": "Padrão do sistema",
|
||||
"useBlackTheme": "Usar tema preto AMOLED",
|
||||
"appSortBy": "Classificar aplicativo por",
|
||||
"authorName": "Autor/Nome",
|
||||
"nameAuthor": "Nome/Autor",
|
||||
"asAdded": "Como adicionado",
|
||||
"appSortOrder": "Ordem de classificação de aplicativos",
|
||||
"ascending": "Ascendente",
|
||||
"descending": "Descendente",
|
||||
"bgUpdateCheckInterval": "Intervalo de verificação de atualizações em segundo-plano",
|
||||
"neverManualOnly": "Nunca - apenas manual",
|
||||
"appearance": "Aparência",
|
||||
"showWebInAppView": "Mostrar página web do aplicativo em informações do aplicativo",
|
||||
"pinUpdates": "Fixar atualizações no topo da janela de aplicativos",
|
||||
"updates": "Atualizações",
|
||||
"sourceSpecific": "Token de acesso",
|
||||
"appSource": "Fonte do aplicativo",
|
||||
"noLogs": "Sem logs",
|
||||
"appLogs": "Logs do aplicativo",
|
||||
"close": "Fechar",
|
||||
"share": "Compartilhar",
|
||||
"appNotFound": "Aplicativo não encontrado",
|
||||
"obtainiumExportHyphenatedLowercase": "exportação de obtainium",
|
||||
"pickAnAPK": "Selecionar um APK",
|
||||
"appHasMoreThanOnePackage": "{} tem mais de um pacote:",
|
||||
"deviceSupportsXArch": "Seu dispositivo suporta a arquitetura de CPU {}.",
|
||||
"deviceSupportsFollowingArchs": "Seu dispositivo suporta as seguintes arquiteturas de CPU:",
|
||||
"warning": "Aviso",
|
||||
"sourceIsXButPackageFromYPrompt": "A fonte do aplicativo é '{}' mas a origem do pacote é '{}'. Continuar?",
|
||||
"updatesAvailable": "Atualizações disponíveis",
|
||||
"updatesAvailableNotifDescription": "Notifica o usuário quando atualizações de um ou mais aplicativos monitorados pelo Obtainium estão disponíveis",
|
||||
"noNewUpdates": "Sem novas atualizações.",
|
||||
"xHasAnUpdate": "{} tem uma atualização.",
|
||||
"appsUpdated": "Aplicativos atualizados",
|
||||
"appsNotUpdated": "Falha na atualização das aplicações",
|
||||
"appsUpdatedNotifDescription": "Notifica o usuário quando atualizações foram aplicadas em segundo-plano para um ou mais aplicativos ",
|
||||
"xWasUpdatedToY": "{} foi atualizado para {}.",
|
||||
"xWasNotUpdatedToY": "Falha ao atualizar {} para {}.",
|
||||
"errorCheckingUpdates": "Erro ao procurar por atualizações",
|
||||
"errorCheckingUpdatesNotifDescription": "Uma notificação que mostra quando a checagem por atualizações em segundo-plano falha",
|
||||
"appsRemoved": "Aplicativos removidos",
|
||||
"appsRemovedNotifDescription": "Notifica o usuário quando um ou mais aplicativos foram removidos devido a erros de carregamento",
|
||||
"xWasRemovedDueToErrorY": "{} foi removido devido a este erro: {}",
|
||||
"completeAppInstallation": "Instalação do aplicativo concluída",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium deve estar aberto para instalar os aplicativos",
|
||||
"completeAppInstallationNotifDescription": "Pede ao usuário que retorne ao Obtainium para finalizar a instalação de um aplicativo",
|
||||
"checkingForUpdates": "Verificando atualizações",
|
||||
"checkingForUpdatesNotifDescription": "Notificação transiente que aparece quando o Obtainium está verificando se há atualizações",
|
||||
"pleaseAllowInstallPerm": "Por favor, permita que o Obtainium possa instalar aplicativos",
|
||||
"trackOnly": "Apenas monitorar",
|
||||
"errorWithHttpStatusCode": "Erro {}",
|
||||
"versionCorrectionDisabled": "Correção de versão desativada (plugin parece não funcionar)",
|
||||
"unknown": "Desconhecido",
|
||||
"none": "Nenhum",
|
||||
"never": "Nunca",
|
||||
"latestVersionX": "Última versão: {}",
|
||||
"installedVersionX": "Versão instalada: {}",
|
||||
"lastUpdateCheckX": "Última verificação de atualizações: {}",
|
||||
"remove": "Remover",
|
||||
"yesMarkUpdated": "Sim, marcar como atualizado",
|
||||
"fdroid": "Oficial F-Droid",
|
||||
"appIdOrName": "ID do aplicativo ou nome",
|
||||
"appId": "ID do aplicativo",
|
||||
"appWithIdOrNameNotFound": "Nenhum aplicativo foi encontrado com esse ID ou nome",
|
||||
"reposHaveMultipleApps": "Repositórios podem conter múltiplos aplicativos",
|
||||
"fdroidThirdPartyRepo": "Repositórios de terceiros F-Droid",
|
||||
"steamMobile": "Steam para celular",
|
||||
"steamChat": "Chat do Steam",
|
||||
"install": "Instalar",
|
||||
"markInstalled": "Marcar instalado",
|
||||
"update": "Atualizar",
|
||||
"markUpdated": "Marcar como atualizado",
|
||||
"additionalOptions": "Opções adicionais",
|
||||
"disableVersionDetection": "Desativar detecção de versão",
|
||||
"noVersionDetectionExplanation": "Essa opção deve apenas ser usada por aplicativos onde a detecção de versão não funciona corretamente.",
|
||||
"downloadingX": "Baixando {}",
|
||||
"downloadX": "Descarregar {}",
|
||||
"downloadedX": "Descarregado {}",
|
||||
"releaseAsset": "Libertação de activos",
|
||||
"downloadNotifDescription": "Notifica o usuário o progresso do download de um aplicativo",
|
||||
"noAPKFound": "APK não encontrado",
|
||||
"noVersionDetection": "Sem detecção de versão",
|
||||
"categorize": "Categorizar",
|
||||
"categories": "Categorias",
|
||||
"category": "Categoria",
|
||||
"noCategory": "Sem categoria",
|
||||
"noCategories": "Sem categorias",
|
||||
"deleteCategoriesQuestion": "Deletar categorias?",
|
||||
"categoryDeleteWarning": "Todos os aplicativos em categorias removidas serão descategorizados.",
|
||||
"addCategory": "Adicionar categoria",
|
||||
"label": "Etiqueta",
|
||||
"language": "Linguagem",
|
||||
"copiedToClipboard": "Copiado para a área de transferência",
|
||||
"storagePermissionDenied": "Permissão de armazenamento negada",
|
||||
"selectedCategorizeWarning": "Isso vai substituir qualquer configuração de categoria para os aplicativos selecionados.",
|
||||
"filterAPKsByRegEx": "Filtrar APKs usando expressão regular",
|
||||
"removeFromObtainium": "Remover do Obtainium",
|
||||
"uninstallFromDevice": "Desinstalar do dispositivo",
|
||||
"onlyWorksWithNonVersionDetectApps": "Apenas funciona para aplicativos com detecção de versão desativada.",
|
||||
"releaseDateAsVersion": "Usar data de lançamento como versão",
|
||||
"releaseDateAsVersionExplanation": "Esta opção só deve ser usada para aplicativos onde a detecção de versão não funciona corretamente, mas há uma data de lançamento disponível.",
|
||||
"changes": "Alterações",
|
||||
"releaseDate": "Data de lançamento",
|
||||
"importFromURLsInFile": "Importar de URLs em arquivo (formato OPML)",
|
||||
"versionDetectionExplanation": "Reconciliar string de versão com versão detectada no sistema operacional",
|
||||
"versionDetection": "Detecção de versão",
|
||||
"standardVersionDetection": "Detecção de versão padrão",
|
||||
"groupByCategory": "Agroupar por categoria",
|
||||
"autoApkFilterByArch": "Tente filtrar APKs por arquitetura de CPU, se possível",
|
||||
"overrideSource": "Substituir fonte",
|
||||
"dontShowAgain": "Não mostrar isso novamente",
|
||||
"dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas monitorar'",
|
||||
"dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK",
|
||||
"moveNonInstalledAppsToBottom": "Mover aplicativos não instalados para o fundo da lista de aplicativos",
|
||||
"gitlabPATLabel": "Token de acesso pessoal do Gitlab",
|
||||
"about": "Sobre",
|
||||
"requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)",
|
||||
"checkOnStart": "Verificar se há atualizações ao iniciar",
|
||||
"tryInferAppIdFromCode": "Tente inferir o ID do aplicativo pelo código-fonte",
|
||||
"removeOnExternalUninstall": "Remover automaticamente aplicativos desinstalados externamente",
|
||||
"pickHighestVersionCode": "Auto-selecionar o maior número de versão do APK",
|
||||
"checkUpdateOnDetailPage": "Checar por atualizações ao abrir a página de detalhes de um aplicativo",
|
||||
"disablePageTransitions": "Desativar animações de transição de página",
|
||||
"reversePageTransitions": "Animações de transição de página invertidas",
|
||||
"minStarCount": "Contagem mínima de estrelas",
|
||||
"addInfoBelow": "Adicionar essa informação abaixo.",
|
||||
"addInfoInSettings": "Adicionar essa informação nas configurações.",
|
||||
"githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.",
|
||||
"sortByLastLinkSegment": "Ordenar apenas usando o último segmento do link",
|
||||
"filterReleaseNotesByRegEx": "Filtrar notas de versão usando Regex",
|
||||
"customLinkFilterRegex": "Filtro de link personalizado usando expressão regular (Padrão '.apk$')",
|
||||
"appsPossiblyUpdated": "Tentativas de atualização de aplicativos",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifica o usuário de que atualizações de um ou mais aplicativos foram potencialmente aplicadas em segundo-plano",
|
||||
"xWasPossiblyUpdatedToY": "{} pode ter sido atualizado para {}.",
|
||||
"enableBackgroundUpdates": "Ativar atualizações em segundo-plano",
|
||||
"backgroundUpdateReqsExplanation": "Atualizações em segundo-plano podem não ser possíveis para todos os aplicativos.",
|
||||
"backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo-plano só pode ser determinado quando o Obtainium é aberto.",
|
||||
"verifyLatestTag": "Verifique a 'última' etiqueta",
|
||||
"intermediateLinkRegex": "Filtrar um link 'intermediário' para visitar",
|
||||
"filterByLinkText": "Filtrar links pelo texto do link",
|
||||
"intermediateLinkNotFound": "Link intermediário não encontrado",
|
||||
"intermediateLink": "Link intermediário",
|
||||
"exemptFromBackgroundUpdates": "Isento de atualizações em segundo-plano (se ativadas)",
|
||||
"bgUpdatesOnWiFiOnly": "Desative as atualizações em segundo-plano quando não estiver conectado no Wi-Fi",
|
||||
"autoSelectHighestVersionCode": "Auto-selecionar a versão mais recente",
|
||||
"versionExtractionRegEx": "Regex de extração de versão",
|
||||
"matchGroupToUse": "Grupo correspondente a ser usado no Regex de extração de versão",
|
||||
"highlightTouchTargets": "Realçar áreas sensíveis ao toque que são menos óbvias",
|
||||
"pickExportDir": "Escolher diretório para exportação",
|
||||
"autoExportOnChanges": "Auto-exportar em mudanças",
|
||||
"includeSettings": "Incluir configurações",
|
||||
"filterVersionsByRegEx": "Filtrar versões por expressão regular",
|
||||
"trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida",
|
||||
"dontSortReleasesList": "Reter a ordem de lançamento da API",
|
||||
"reverseSort": "Ordenação reversa",
|
||||
"takeFirstLink": "Obter primeiro link",
|
||||
"skipSort": "Ignorar ordenação",
|
||||
"debugMenu": "Menu debug",
|
||||
"bgTaskStarted": "Tarefa em segundo-plano iniciada - verifique os logs.",
|
||||
"runBgCheckNow": "Execute agora em segundo-plano a verificação de atualizações",
|
||||
"versionExtractWholePage": "Aplicar regex de extração de versão à página inteira",
|
||||
"installing": "Instalando",
|
||||
"skipUpdateNotifications": "Pular notificações de update",
|
||||
"updatesAvailableNotifChannel": "Atualizações disponíveis",
|
||||
"appsUpdatedNotifChannel": "Aplicativos atualizados",
|
||||
"appsPossiblyUpdatedNotifChannel": "Tentativas de atualização de aplicativos",
|
||||
"errorCheckingUpdatesNotifChannel": "Erro ao procurar por atualizações",
|
||||
"appsRemovedNotifChannel": "Aplicativos removidos",
|
||||
"downloadingXNotifChannel": "Baixando {}",
|
||||
"completeAppInstallationNotifChannel": "Instalação completa do aplicativo",
|
||||
"checkingForUpdatesNotifChannel": "Checando por atualizações",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Apenas verificar atualizações de aplicativos instalados e 'Apenas monitorar'",
|
||||
"supportFixedAPKURL": "Suporte a APK com URLs fixas",
|
||||
"selectX": "Selecionar {}",
|
||||
"parallelDownloads": "Permitir downloads paralelos",
|
||||
"useShizuku": "Utilizar Shizuku ou Sui para instalar",
|
||||
"shizukuBinderNotFound": "O Shizuku não está rodando",
|
||||
"shizukuOld": "Versão antiga do Shizuku (<11) - atualizar",
|
||||
"shizukuOldAndroidWithADB": "Shizuku a funcionar no Android < 8.1 com ADB - atualizar o Android ou utilizar o Sui",
|
||||
"shizukuPretendToBeGooglePlay": "Definir o Google Play como fonte de instalação (se for utilizado o Shizuku)",
|
||||
"useSystemFont": "Usar fonte padrão do sistema",
|
||||
"useVersionCodeAsOSVersion": "Usar versionCode do aplicativo como versão detectada pelo sistema operacional",
|
||||
"requestHeader": "Requisitar cabeçalho",
|
||||
"useLatestAssetDateAsReleaseDate": "Use o último upload de recursos como data de lançamento",
|
||||
"defaultPseudoVersioningMethod": "Método de pseudo-versionamento padrão",
|
||||
"partialAPKHash": "Hash APK parcial",
|
||||
"APKLinkHash": "Hash de links APK",
|
||||
"directAPKLink": "Link direto do APK",
|
||||
"pseudoVersionInUse": "Uma pseudo-versão está em uso",
|
||||
"installed": "Instalado",
|
||||
"latest": "Mais recente",
|
||||
"invertRegEx": "Inverter expressão regular",
|
||||
"note": "Nota",
|
||||
"selfHostedNote": "O menu suspenso \"{}\" pode ser usado para acessar instâncias auto-hospedadas/personalizadas de qualquer fonte.",
|
||||
"badDownload": "Não foi possível analisar o APK (transferência incompatível ou parcial)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Partilhar novas aplicações com o AppVerifier (se disponível)",
|
||||
"appVerifierInstructionToast": "Partilhe com o AppVerifier e, em seguida, regresse aqui quando estiver pronto.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Remover aplicativo?",
|
||||
"other": "Remover aplicativos?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Muitas solicitações (taxa de solicitações limitada) - tente novamente em {} minuto",
|
||||
"other": "Muitas solicitações (taxa limitada) - tente novamente em {} minutos"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "A verificação de atualizações em segundo-plano encontrou um {}, agendada uma nova verificação em {} minuto",
|
||||
"other": "A verificação de atualizações em segundo-plano encontrou um {}, agendada uma nova verificação em {} minutos"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "A verificação de atualizações em segundo-plano encontrou {} atualização, o usuário sera notificado caso necessário",
|
||||
"other": "A verificação de atualizações em segundo-plano encontrou {} atualizações, o usuário sera notificado caso necessário"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} Aplicativo",
|
||||
"other": "{} Aplicativos"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URLs"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Minuto",
|
||||
"other": "{} Minutos"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Hora",
|
||||
"other": "{} Horas"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} Dia",
|
||||
"other": "{} Dias"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Foi limpo {n} log (antes = {antes}, depois = {depois})",
|
||||
"other": "Foram limpos {n} logs (antes = {antes}, depois = {depois})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} e um outro aplicativo possui atualizações.",
|
||||
"other": "{} e {} outros aplicativo possuem atualizações."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} e um outro aplicativo foram atualizado.",
|
||||
"other": "{} e {} outros aplicativos foram atualizados."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Falha ao atualizar {} e mais 1 aplicação.",
|
||||
"other": "Falha ao atualizar {} e {} mais aplicações."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} e um outro aplicativo podem ter sido atualizados.",
|
||||
"other": "{} e {} outros aplicativos podem ter sido atualizados."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
373
assets/translations/ru.json
Normal file
@@ -0,0 +1,373 @@
|
||||
{
|
||||
"invalidURLForSource": "Неверный URL-адрес приложения: {}",
|
||||
"noReleaseFound": "Не удалось найти подходящий релиз",
|
||||
"noVersionFound": "Не удалось определить версию релиза",
|
||||
"urlMatchesNoSource": "URL-адрес не соответствует известному источнику",
|
||||
"cantInstallOlderVersion": "Невозможно установить более старую версию приложения",
|
||||
"appIdMismatch": "ID загруженного пакета не совпадает с существующим ID приложения",
|
||||
"functionNotImplemented": "Этот класс не реализовал эту функцию",
|
||||
"placeholder": "Заполнитель",
|
||||
"someErrors": "Возникли некоторые ошибки",
|
||||
"unexpectedError": "Неожиданная ошибка",
|
||||
"ok": "Ok",
|
||||
"and": "и",
|
||||
"githubPATLabel": "Персональный токен доступа GitHub\n(увеличивает лимит запросов)",
|
||||
"includePrereleases": "Включить предварительные релизы",
|
||||
"fallbackToOlderReleases": "Откатываться к предыдущей версии",
|
||||
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\n(регулярное выражение)",
|
||||
"invalidRegEx": "Неверное регулярное выражение",
|
||||
"noDescription": "Нет описания",
|
||||
"cancel": "Отмена",
|
||||
"continue": "Продолжить",
|
||||
"requiredInBrackets": "(обязательно)",
|
||||
"dropdownNoOptsError": "Ошибка: в выпадающем списке должна быть выбрана хотя бы одна настройка",
|
||||
"colour": "Цвет",
|
||||
"standard": "Стандартный",
|
||||
"custom": "Индивидуальный",
|
||||
"useMaterialYou": "Использовать Material You",
|
||||
"githubStarredRepos": "Избранные репозитории GitHub",
|
||||
"uname": "Имя пользователя",
|
||||
"wrongArgNum": "Неправильное количество предоставленных аргументов",
|
||||
"xIsTrackOnly": "{} только для отслеживания",
|
||||
"source": "Источник",
|
||||
"app": "Приложение",
|
||||
"appsFromSourceAreTrackOnly": "Приложения из этого источника настроены только для отслеживания",
|
||||
"youPickedTrackOnly": "Вы выбрали опцию 'Только для отслеживания'",
|
||||
"trackOnlyAppDescription": "Приложение будет отслеживаться на предмет обновлений, но Obtainium не сможет загрузить или установить его",
|
||||
"cancelled": "Отменено",
|
||||
"appAlreadyAdded": "Приложение уже добавлено",
|
||||
"alreadyUpToDateQuestion": "Приложение уже обновлено?",
|
||||
"addApp": "Добавить",
|
||||
"appSourceURL": "URL-источник приложения",
|
||||
"error": "Ошибка",
|
||||
"add": "Добавить",
|
||||
"searchSomeSourcesLabel": "Поиск (в некоторых источниках)",
|
||||
"search": "Поиск",
|
||||
"additionalOptsFor": "Дополнительные настройки для {}",
|
||||
"supportedSources": "Поддерживаемые источники",
|
||||
"trackOnlyInBrackets": "(только отслеживание)",
|
||||
"searchableInBrackets": "(поиск)",
|
||||
"appsString": "Приложения",
|
||||
"noApps": "Нет приложений",
|
||||
"noAppsForFilter": "Нет приложений для фильтра",
|
||||
"byX": "От {}",
|
||||
"percentProgress": "Прогресс: {}%",
|
||||
"pleaseWait": "Пожалуйста, подождите",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"notInstalled": "Не установлено",
|
||||
"pseudoVersion": "псевдоверсия",
|
||||
"selectAll": "Выбрать всё",
|
||||
"deselectX": "Отменить выбор {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве",
|
||||
"removeSelectedAppsQuestion": "Удалить выбранные приложения?",
|
||||
"removeSelectedApps": "Удалить выбранные приложения",
|
||||
"updateX": "Обновить {}",
|
||||
"installX": "Установить {}",
|
||||
"markXTrackOnlyAsUpdated": "Отметить {}\n(Только для отслеживания)\nкак обновленное",
|
||||
"changeX": "Изменить {}",
|
||||
"installUpdateApps": "Установить/Обновить приложения",
|
||||
"installUpdateSelectedApps": "Установить/Обновить выбранные приложения",
|
||||
"markXSelectedAppsAsUpdated": "Выбрано приложений: {}. Отметить как обновлённые?",
|
||||
"no": "Нет",
|
||||
"yes": "Да",
|
||||
"markSelectedAppsUpdated": "Отметить выбранные приложения как обновлённые",
|
||||
"pinToTop": "Закрепить сверху",
|
||||
"unpinFromTop": "Открепить",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Сбросить статус установки для выбранных приложений?",
|
||||
"installStatusOfXWillBeResetExplanation": "Статус установки для выбранных приложений будет сброшен.\n\nЭто может помочь, если версия приложения, отображаемая в Obtainium, некорректная — из-за неудачных обновлений или других проблем",
|
||||
"customLinkMessage": "Эти ссылки работают на устройствах с установленным Obtainium",
|
||||
"shareAppConfigLinks": "Поделиться конфигурацией приложения в виде HTML-ссылки",
|
||||
"shareSelectedAppURLs": "Поделиться выбранными URL-адресами приложений",
|
||||
"resetInstallStatus": "Сбросить статус установки",
|
||||
"more": "Ещё",
|
||||
"removeOutdatedFilter": "Удалить фильтр для устаревших приложений",
|
||||
"showOutdatedOnly": "Показывать только устаревшие приложения",
|
||||
"filter": "Фильтр",
|
||||
"filterApps": "Фильтровать приложения",
|
||||
"appName": "Название приложения",
|
||||
"author": "Автор",
|
||||
"upToDateApps": "Приложения со свежими обновлениями",
|
||||
"nonInstalledApps": "Неустановленные приложения",
|
||||
"importExport": "Данные",
|
||||
"settings": "Настройки",
|
||||
"exportedTo": "Экспортировано в {}",
|
||||
"obtainiumExport": "Экспорт из Obtainium",
|
||||
"invalidInput": "Неверный ввод",
|
||||
"importedX": "Импортировано {}",
|
||||
"obtainiumImport": "Импорт в Obtainium",
|
||||
"importFromURLList": "Импорт из списка URL-адресов",
|
||||
"searchQuery": "Поисковый запрос",
|
||||
"appURLList": "Список URL приложений",
|
||||
"line": "Строка",
|
||||
"searchX": "Поиск {}",
|
||||
"noResults": "Результатов не найдено",
|
||||
"importX": "Импорт {}",
|
||||
"importedAppsIdDisclaimer": "Импортированные приложения могут неверно отображаться как неустановленные.\nДля исправления этой проблемы повторно установите их через Obtainium.\nЭто не должно повлиять на данные приложения.\n\nПроблемы возникают только при импорте из URL-адреса и сторонних источников",
|
||||
"importErrors": "Ошибка импорта",
|
||||
"importedXOfYApps": "Импортировано приложений: {} из {}",
|
||||
"followingURLsHadErrors": "При импорте следующие URL-адреса содержали ошибки:",
|
||||
"selectURL": "Выбрать URL-адрес",
|
||||
"selectURLs": "Выбрать URL-адреса",
|
||||
"pick": "Выбрать",
|
||||
"theme": "Тема",
|
||||
"dark": "Тёмная",
|
||||
"light": "Светлая",
|
||||
"followSystem": "Системная",
|
||||
"followSystemThemeExplanation": "Следование системной теме возможно только при использовании сторонних приложений",
|
||||
"useBlackTheme": "Использовать чёрную тему",
|
||||
"appSortBy": "Сортировка приложений",
|
||||
"authorName": "Автор/Название",
|
||||
"nameAuthor": "Название/Автор",
|
||||
"asAdded": "В порядке добавления",
|
||||
"appSortOrder": "Порядок",
|
||||
"ascending": "По возрастанию",
|
||||
"descending": "По убыванию",
|
||||
"bgUpdateCheckInterval": "Интервал проверки обновлений в фоновом режиме",
|
||||
"neverManualOnly": "Никогда — только вручную",
|
||||
"appearance": "Внешний вид",
|
||||
"showWebInAppView": "Показывать исходную веб-страницу на странице приложения",
|
||||
"pinUpdates": "Отображать обновления приложений сверху списка",
|
||||
"updates": "Обновления",
|
||||
"sourceSpecific": "Настройки источников",
|
||||
"appSource": "Исходный код",
|
||||
"noLogs": "Нет журналов",
|
||||
"appLogs": "Логи",
|
||||
"close": "Закрыть",
|
||||
"share": "Поделиться",
|
||||
"appNotFound": "Приложение не найдено",
|
||||
"obtainiumExportHyphenatedLowercase": "получение-экспорт",
|
||||
"pickAnAPK": "Выберите APK-файл",
|
||||
"appHasMoreThanOnePackage": "{} имеет более одного пакета:",
|
||||
"deviceSupportsXArch": "Ваше устройство поддерживает архитектуру процессора {}",
|
||||
"deviceSupportsFollowingArchs": "Ваше устройство поддерживает следующие архитектуры процессора:",
|
||||
"warning": "Предупреждение",
|
||||
"sourceIsXButPackageFromYPrompt": "Источник приложения — '{}', но пакет для установки получен из '{}'. Продолжить?",
|
||||
"updatesAvailable": "Доступны обновления",
|
||||
"updatesAvailableNotifDescription": "Уведомляет о наличии обновлений для одного или нескольких приложений в Obtainium",
|
||||
"noNewUpdates": "Нет новых обновлений",
|
||||
"xHasAnUpdate": "{} есть обновление",
|
||||
"appsUpdated": "Приложения обновлены",
|
||||
"appsNotUpdated": "Не удалось обновить приложения",
|
||||
"appsUpdatedNotifDescription": "Уведомляет об обновлении одного или нескольких приложений в фоновом режиме",
|
||||
"xWasUpdatedToY": "{} была обновлена до версии {}",
|
||||
"xWasNotUpdatedToY": "Не удалось обновить {} до версии {}",
|
||||
"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": "ID или название приложения",
|
||||
"appId": "ID приложения",
|
||||
"appWithIdOrNameNotFound": "Приложение с таким ID или названием не было найдено",
|
||||
"reposHaveMultipleApps": "В хранилище несколько приложений",
|
||||
"fdroidThirdPartyRepo": "Сторонние репозитории F-Droid",
|
||||
"steamMobile": "Стим Мобайл",
|
||||
"steamChat": "Стим-чат",
|
||||
"install": "Установить",
|
||||
"markInstalled": "Пометить как установленное",
|
||||
"update": "Обновить",
|
||||
"markUpdated": "Отметить обновлённым",
|
||||
"additionalOptions": "Дополнительные настройки",
|
||||
"disableVersionDetection": "Отключить обнаружение версии",
|
||||
"noVersionDetectionExplanation": "Эта настройка должна использоваться только для приложений, где обнаружение версии не работает корректно",
|
||||
"downloadingX": "Загрузка {}",
|
||||
"downloadX": "Скачать {}",
|
||||
"downloadedX": "Загружено {}",
|
||||
"releaseAsset": "Освобождение актива",
|
||||
"downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения",
|
||||
"noAPKFound": "APK не найден",
|
||||
"noVersionDetection": "Обнаружение версий отключено",
|
||||
"categorize": "Категоризировать",
|
||||
"categories": "Категории",
|
||||
"category": "Категория",
|
||||
"noCategory": "Без категории",
|
||||
"noCategories": "Без категорий",
|
||||
"deleteCategoriesQuestion": "Удалить категории?",
|
||||
"categoryDeleteWarning": "Все приложения в удаленных категориях будут помечены как без категории",
|
||||
"addCategory": "Добавить категорию",
|
||||
"label": "Метка",
|
||||
"language": "Язык",
|
||||
"copiedToClipboard": "Скопировано в буфер обмена",
|
||||
"storagePermissionDenied": "Отказано в доступе к хранилищу",
|
||||
"selectedCategorizeWarning": "Это заменит все текущие настройки категорий для выбранных приложений",
|
||||
"filterAPKsByRegEx": "Отфильтровать APK-файлы\n(регулярное выражение)",
|
||||
"removeFromObtainium": "Удалить из Obtainium",
|
||||
"uninstallFromDevice": "Удалить с устройства",
|
||||
"onlyWorksWithNonVersionDetectApps": "Работает только для приложений с отключенным определением версии",
|
||||
"releaseDateAsVersion": "Дата выпуска вместо версии",
|
||||
"releaseDateAsVersionExplanation": "Этот параметр следует использовать только для приложений, в которых определение версии не работает правильно, но имеется дата выпуска",
|
||||
"changes": "Изменения",
|
||||
"releaseDate": "Дата выпуска",
|
||||
"importFromURLsInFile": "Импорт из файла URL-адресов (например: OPML)",
|
||||
"versionDetectionExplanation": "Согласовать строку версии с версией, обнаруженной в ОС",
|
||||
"versionDetection": "Определение версии",
|
||||
"standardVersionDetection": "Стандартное",
|
||||
"groupByCategory": "Группировать по категориям",
|
||||
"autoApkFilterByArch": "Попытаться отфильтровать APK-файлы по архитектуре процессора",
|
||||
"overrideSource": "Переопределить источник",
|
||||
"dontShowAgain": "Не показывать снова",
|
||||
"dontShowTrackOnlyWarnings": "Не показывать предупреждения о только отслеживаемых приложениях",
|
||||
"dontShowAPKOriginWarnings": "Не показывать предупреждения об отличающемся источнике APK-файлов",
|
||||
"moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка",
|
||||
"gitlabPATLabel": "Персональный токен доступа GitLab",
|
||||
"about": "Описание",
|
||||
"requiresCredentialsInSettings": "{}: Для этого требуются дополнительные учетные данные (в настройках)",
|
||||
"checkOnStart": "Проверять наличие обновлений при запуске",
|
||||
"tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода",
|
||||
"removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения",
|
||||
"pickHighestVersionCode": "Автовыбор актуальной версии кода APK",
|
||||
"checkUpdateOnDetailPage": "Проверять наличие обновлений при открытии страницы приложения",
|
||||
"disablePageTransitions": "Отключить анимацию перехода между страницами",
|
||||
"reversePageTransitions": "Реверс анимации перехода между страницами",
|
||||
"minStarCount": "Минимальное количество звёзд",
|
||||
"addInfoBelow": "Добавьте эту информацию ниже",
|
||||
"addInfoInSettings": "Добавьте эту информацию в Настройки",
|
||||
"githubSourceNote": "Используя ключ API можно обойти лимит запросов GitHub",
|
||||
"sortByLastLinkSegment": "Сортировать только по последнему сегменту ссылки",
|
||||
"filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
|
||||
"customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: '.apk$')",
|
||||
"appsPossiblyUpdated": "Попытки обновления приложений",
|
||||
"appsPossiblyUpdatedNotifDescription": "Уведомление о возможных обновлениях одного или нескольких приложений в фоновом режиме",
|
||||
"xWasPossiblyUpdatedToY": "{} возможно был обновлен до {}",
|
||||
"enableBackgroundUpdates": "Включить обновления в фоне",
|
||||
"backgroundUpdateReqsExplanation": "Фоновые обновления могут быть возможны не для всех приложений",
|
||||
"backgroundUpdateLimitsExplanation": "Успешность фоновой установки можно определить только после открытия Obtainium",
|
||||
"verifyLatestTag": "Проверять тег 'latest'",
|
||||
"intermediateLinkRegex": "Фильтр для \"промежуточной\" ссылки для посещения",
|
||||
"filterByLinkText": "Фильтрация ссылок по тексту ссылки",
|
||||
"intermediateLinkNotFound": "Промежуточная ссылка не найдена",
|
||||
"intermediateLink": "Промежуточное звено",
|
||||
"exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
|
||||
"bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет соединения с Wi-Fi",
|
||||
"autoSelectHighestVersionCode": "Автоматически выбирать APK с актуальной версией кода",
|
||||
"versionExtractionRegEx": "Регулярное выражение для извлечения версии",
|
||||
"matchGroupToUse": "Выберите группу для использования",
|
||||
"highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
|
||||
"pickExportDir": "Выбрать каталог для экспорта",
|
||||
"autoExportOnChanges": "Автоэкспорт при изменениях",
|
||||
"includeSettings": "Включить настройки",
|
||||
"filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
|
||||
"trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
|
||||
"dontSortReleasesList": "Сохранить порядок релизов от API",
|
||||
"reverseSort": "Обратная сортировка",
|
||||
"takeFirstLink": "Взять первую ссылку",
|
||||
"skipSort": "Пропустить сортировку",
|
||||
"debugMenu": "Меню отладки",
|
||||
"bgTaskStarted": "Фоновая задача начата — проверьте журналы",
|
||||
"runBgCheckNow": "Запустить проверку фонового обновления сейчас",
|
||||
"versionExtractWholePage": "Применить регулярное выражение версии ко всей странице",
|
||||
"installing": "Устанавливается",
|
||||
"skipUpdateNotifications": "Не оповещать об обновлениях",
|
||||
"updatesAvailableNotifChannel": "Доступны обновления",
|
||||
"appsUpdatedNotifChannel": "Приложения обновлены",
|
||||
"appsPossiblyUpdatedNotifChannel": "Попытки обновления приложений",
|
||||
"errorCheckingUpdatesNotifChannel": "Ошибка при проверке обновлений",
|
||||
"appsRemovedNotifChannel": "Приложение удалено",
|
||||
"downloadingXNotifChannel": "Загрузка {}",
|
||||
"completeAppInstallationNotifChannel": "Завершение установки приложения",
|
||||
"checkingForUpdatesNotifChannel": "Проверка обновлений",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
|
||||
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
|
||||
"selectX": "Выбрать {}",
|
||||
"parallelDownloads": "Разрешить параллельные загрузки",
|
||||
"useShizuku": "Использовать Shizuku или Sui для установки",
|
||||
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден, возможно он не запущен",
|
||||
"shizukuOld": "Устаревшая версия Shizuku (<11), обновите",
|
||||
"shizukuOldAndroidWithADB": "Shizuku работает на Android < 8.1 с ADB, обновите Android или используйте Sui",
|
||||
"shizukuPretendToBeGooglePlay": "Указать Google Play как источник установки (если используется Shizuku)",
|
||||
"useSystemFont": "Использовать системный шрифт",
|
||||
"useVersionCodeAsOSVersion": "Использовать код версии приложения как версию, обнаруженную ОС",
|
||||
"requestHeader": "Заголовок запроса",
|
||||
"useLatestAssetDateAsReleaseDate": "Использовать последнюю загрузку ресурса в качестве даты выпуска",
|
||||
"defaultPseudoVersioningMethod": "Метод псевдоверсионирования по умолчанию",
|
||||
"partialAPKHash": "Частичный хэш APK",
|
||||
"APKLinkHash": "Хеш ссылки APK",
|
||||
"directAPKLink": "Прямая ссылка на APK",
|
||||
"pseudoVersionInUse": "Используется псевдоверсия",
|
||||
"installed": "Установлен",
|
||||
"latest": "Последний",
|
||||
"invertRegEx": "Инвертировать регулярное выражение",
|
||||
"note": "Примечание",
|
||||
"selfHostedNote": "Выпадающий список \"{}\" можно использовать для доступа к самостоятельно размещенным/настроенным экземплярам любого источника.",
|
||||
"badDownload": "APK не удалось разобрать (несовместимая или неполная загрузка)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Поделитесь новыми приложениями с AppVerifier (если доступно)",
|
||||
"appVerifierInstructionToast": "Поделитесь с AppVerifier, а затем вернитесь сюда, когда будете готовы.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Удалить приложение?",
|
||||
"other": "Удалить приложения?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Слишком много запросов (ограничение скорости) — попробуйте снова через {} минуту",
|
||||
"other": "Слишком много запросов (ограничение скорости) — попробуйте снова через {} минуты"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет запланирована через {} минуту",
|
||||
"other": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет запланирована через {} минуты"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "В ходе проверки обновления в фоновом режиме было обнаружено {} обновление — Пользователю будет отправлено уведомление, если это необходимо",
|
||||
"other": "В ходе проверки обновления в фоновом режиме было обнаружено {} обновлений — Пользователю будет отправлено уведомление, если это необходимо"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} приложение",
|
||||
"other": "{} приложений"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL-адрес",
|
||||
"other": "{} URL-адреса"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} минута",
|
||||
"other": "{} минуты"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} час",
|
||||
"other": "{} часов"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} день",
|
||||
"other": "{} дней"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Очищен {n} журнал (до = {before}, после = {after})",
|
||||
"other": "Очищено {n} журналов (до = {before}, после = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "У {} и еще 1 приложения есть обновление",
|
||||
"other": "У {} и ещё {} приложений есть обновления"
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} и ещё 1 приложение были обновлены",
|
||||
"other": "{} и ещё {} приложений были обновлены"
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Не удалось обновить {} и ещё 1 приложение",
|
||||
"other": "Не удалось обновить {} и ещё {} приложений"
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} и ещё 1 приложение могли быть обновлены",
|
||||
"other": "{} и ещё {} приложений могли быть обновлены"
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKи"
|
||||
}
|
||||
}
|
||||
118
assets/translations/standardize.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// Take one (hardcoded) translation file and ensure that all other translation files have the same keys in the same order
|
||||
// Then report which other translation files have identical items (or auto-translate them if a DeepL API key is provided)
|
||||
|
||||
const fs = require('fs')
|
||||
const https = require('https')
|
||||
|
||||
const deeplAPIKey = process.argv[2]
|
||||
|
||||
const neverAutoTranslate = {
|
||||
steamMobile: ['*'],
|
||||
steamChat: ['*'],
|
||||
root: ['*'],
|
||||
obtainiumExportHyphenatedLowercase: ['*'],
|
||||
theme: ['de'],
|
||||
appId: ['de']
|
||||
}
|
||||
|
||||
const translateText = async (text, targetLang, authKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const postData = `text=${encodeURIComponent(text)}&target_lang=${encodeURIComponent(targetLang)}&source_lang=EN`
|
||||
const options = {
|
||||
hostname: 'api-free.deepl.com',
|
||||
port: 443,
|
||||
path: '/v2/translate',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `DeepL-Auth-Key ${authKey}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
}
|
||||
}
|
||||
const req = https.request(options, (res) => {
|
||||
let responseData = ''
|
||||
res.on('data', (chunk) => {
|
||||
responseData += chunk
|
||||
})
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const jsonResponse = JSON.parse(responseData)
|
||||
resolve(jsonResponse)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
req.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
req.write(postData)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const translationsDir = __dirname
|
||||
const templateFile = `${translationsDir}/en.json`
|
||||
const otherFiles = fs.readdirSync(translationsDir).map(f => {
|
||||
return `${translationsDir}/${f}`
|
||||
}).filter(f => f.endsWith('.json') && f != templateFile)
|
||||
|
||||
const templateTranslation = JSON.parse(fs.readFileSync(templateFile).toString())
|
||||
|
||||
otherFiles.forEach(file => {
|
||||
const thisTranslationOriginal = JSON.parse(fs.readFileSync((file).toString()))
|
||||
const thisTranslationNew = {}
|
||||
Object.keys(templateTranslation).forEach(k => {
|
||||
thisTranslationNew[k] = thisTranslationOriginal[k] || templateTranslation[k]
|
||||
})
|
||||
fs.writeFileSync(file, `${JSON.stringify(thisTranslationNew, null, ' ')}\n`)
|
||||
})
|
||||
|
||||
for (let i in otherFiles) {
|
||||
const file = otherFiles[i]
|
||||
const thisTranslation = JSON.parse(fs.readFileSync((file).toString()))
|
||||
const translationKeys = Object.keys(templateTranslation)
|
||||
for (let j in translationKeys) {
|
||||
const k = translationKeys[j]
|
||||
if (JSON.stringify(thisTranslation[k]) == JSON.stringify(templateTranslation[k])) {
|
||||
const lang = file.split('/').pop().split('.')[0]
|
||||
if (!neverAutoTranslate[k] || (neverAutoTranslate[k].indexOf('*') < 0 && neverAutoTranslate[k].indexOf(lang) < 0)) {
|
||||
const reportLine = `${file} :::: ${k} :::: ${JSON.stringify(thisTranslation[k])}`
|
||||
if (deeplAPIKey) {
|
||||
const translateFunc = async (str) => {
|
||||
const response = await translateText(str, lang, deeplAPIKey)
|
||||
if (response.translations) {
|
||||
return response.translations[0].text
|
||||
} else {
|
||||
throw JSON.stringify(response)
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (typeof templateTranslation[k] == 'string') {
|
||||
thisTranslation[k] = await translateFunc(thisTranslation[k])
|
||||
} else {
|
||||
const subKeys = Object.keys(templateTranslation[k])
|
||||
for (let n in subKeys) {
|
||||
const kk = subKeys[n]
|
||||
thisTranslation[k][kk] = await translateFunc(thisTranslation[k][kk])
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (typeof e == 'string') {
|
||||
console.log(`${reportLine} :::: ${e}`)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(reportLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(file, `${JSON.stringify(thisTranslation, null, ' ')}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => console.error)
|
||||
372
assets/translations/sv.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "Inte giltig {} App-URL",
|
||||
"noReleaseFound": "Kunde inte hitta en lämplig releaseversion",
|
||||
"noVersionFound": "Kunde inte bestämma releaseversion",
|
||||
"urlMatchesNoSource": "URL matchar inte känd källa",
|
||||
"cantInstallOlderVersion": "Kan inte installera en äldre version av en app",
|
||||
"appIdMismatch": "Nerladdat paket-ID matchar inte nuvarande App-ID",
|
||||
"functionNotImplemented": "Den här klassen har inte implementerat den här funktionen",
|
||||
"placeholder": "Platshållare",
|
||||
"someErrors": "Några fel uppstod",
|
||||
"unexpectedError": "Oväntat fel",
|
||||
"ok": "Okej",
|
||||
"and": "och",
|
||||
"githubPATLabel": "GitHub Personal Access Token (ökar hastighetsgräns)",
|
||||
"includePrereleases": "Inkludera förreleaser",
|
||||
"fallbackToOlderReleases": "Fall tillbaka till äldre releaser",
|
||||
"filterReleaseTitlesByRegEx": "Filtrera utgivningstitlar efter reguljärt uttryck",
|
||||
"invalidRegEx": "Ogiltigt reguljärt uttryck",
|
||||
"noDescription": "Ingen beskrivning",
|
||||
"cancel": "Avbryt",
|
||||
"continue": "Fortsätt",
|
||||
"requiredInBrackets": "(Kräver)",
|
||||
"dropdownNoOptsError": "FEL: DROPDOWN MÅSTE HA MINST ETT OPT",
|
||||
"colour": "Färg",
|
||||
"standard": "Standard",
|
||||
"custom": "Anpassad",
|
||||
"useMaterialYou": "Använd material Du",
|
||||
"githubStarredRepos": "GitHub Stjärnmärkta Förråd",
|
||||
"uname": "Användarnamn",
|
||||
"wrongArgNum": "Fel antal argument har angetts",
|
||||
"xIsTrackOnly": "{} är 'Följ-Endast'",
|
||||
"source": "Källa",
|
||||
"app": "App",
|
||||
"appsFromSourceAreTrackOnly": "Apparna från denna källa är 'Följ-Endast'.",
|
||||
"youPickedTrackOnly": "Du har markerat 'Följ-Endast'-alternativet",
|
||||
"trackOnlyAppDescription": "Appen kommer följas för uppdateringar men Obtainium kommer inte ladda ner eller installera den.",
|
||||
"cancelled": "Avbruten",
|
||||
"appAlreadyAdded": "App redan tillagd",
|
||||
"alreadyUpToDateQuestion": "App redan uppdaterad?",
|
||||
"addApp": "Lägg till App",
|
||||
"appSourceURL": "URL till Appkälla",
|
||||
"error": "Fel",
|
||||
"add": "Lägg till",
|
||||
"searchSomeSourcesLabel": "Sök (Bara några källor)",
|
||||
"search": "Sök",
|
||||
"additionalOptsFor": "Ytterligare Alternativ för {}",
|
||||
"supportedSources": "Stödda Källor",
|
||||
"trackOnlyInBrackets": "(Följ-Endast)",
|
||||
"searchableInBrackets": "(Sökbar)",
|
||||
"appsString": "Appar",
|
||||
"noApps": "Inga Appar",
|
||||
"noAppsForFilter": "Inga Appar för Filter",
|
||||
"byX": "Av {}",
|
||||
"percentProgress": "Framsteg: {}%",
|
||||
"pleaseWait": "Vänta",
|
||||
"updateAvailable": "Uppdatering Tillgänglig",
|
||||
"notInstalled": "Inte Installerad",
|
||||
"pseudoVersion": "pseudoversion",
|
||||
"selectAll": "Välj Alla",
|
||||
"deselectX": "Avmarkera {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.",
|
||||
"removeSelectedAppsQuestion": "Ta bort markerade Appar?",
|
||||
"removeSelectedApps": "Ta bort markerade Appar",
|
||||
"updateX": "Uppdatera {}",
|
||||
"installX": "Installera {}",
|
||||
"markXTrackOnlyAsUpdated": "Märk {}\n(Följ-Endast)\nsom Uppdaterad",
|
||||
"changeX": "Byt {}",
|
||||
"installUpdateApps": "Installera/Uppdatera Appar",
|
||||
"installUpdateSelectedApps": "Installera/Uppdatera Markerade Appar",
|
||||
"markXSelectedAppsAsUpdated": "Märk {} markerade Appar som Uppdaterade?",
|
||||
"no": "Nej",
|
||||
"yes": "Ja",
|
||||
"markSelectedAppsUpdated": "Märk Valda Appar som Uppdaterade",
|
||||
"pinToTop": "Nåla fast högst upp",
|
||||
"unpinFromTop": "Avnåla",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Återställ Installationsstatus för valda Appar?",
|
||||
"installStatusOfXWillBeResetExplanation": "Installationsstatusen för de markerade apparna kommer återställas.\n\n Detta kan hjälpa när appversionen visad i Obtanium är fel på grund av misslyckade uppdateringar eller andra orsaker.",
|
||||
"customLinkMessage": "Dessa länkar fungerar på enheter med Obtainium installerat",
|
||||
"shareAppConfigLinks": "Dela appkonfiguration som HTML-länk",
|
||||
"shareSelectedAppURLs": "Dela Valda Appars URL:er",
|
||||
"resetInstallStatus": "Återställ Installationstatus",
|
||||
"more": "Mer",
|
||||
"removeOutdatedFilter": "Ta bort Utgånga App-filtret",
|
||||
"showOutdatedOnly": "Visa Endast Utgånga Appar",
|
||||
"filter": "Filtrera",
|
||||
"filterApps": "Filtrera Appar",
|
||||
"appName": "Appnamn",
|
||||
"author": "Utvecklare",
|
||||
"upToDateApps": "Uppdaterade Appar",
|
||||
"nonInstalledApps": "Icke-Installerade Appar",
|
||||
"importExport": "Importera/Exportera",
|
||||
"settings": "Inställningar",
|
||||
"exportedTo": "Exporterad till {}",
|
||||
"obtainiumExport": "Obtainiumexport",
|
||||
"invalidInput": "Ogiltig inmatning",
|
||||
"importedX": "Importerad {}",
|
||||
"obtainiumImport": "Obtainium Import",
|
||||
"importFromURLList": "Importera från URL-lista",
|
||||
"searchQuery": "Sökförfrågan",
|
||||
"appURLList": "App URL-lista",
|
||||
"line": "Linje",
|
||||
"searchX": "Sök {}",
|
||||
"noResults": "Inga resultat",
|
||||
"importX": "Importera {}",
|
||||
"importedAppsIdDisclaimer": "Importerade Appar kan felaktigt visas som \"Inte Installerad\".\nFör att fixa detta återinstallera dem genom Obtainium.\nDetta skall inte påverka appdata.\n\n Påverkar endast URL:en och tredjepartsimportermetoder.",
|
||||
"importErrors": "Importfel",
|
||||
"importedXOfYApps": "{} av {} Appar importerade.",
|
||||
"followingURLsHadErrors": "Följande URL:er hade fel:",
|
||||
"selectURL": "Välj URL",
|
||||
"selectURLs": "Välj URL:er",
|
||||
"pick": "Välj",
|
||||
"theme": "Tema",
|
||||
"dark": "Mörkt",
|
||||
"light": "Ljust",
|
||||
"followSystem": "Följ System",
|
||||
"useBlackTheme": "Använd svart tema",
|
||||
"appSortBy": "Sortera Appar via",
|
||||
"authorName": "Utvecklare/Namn",
|
||||
"nameAuthor": "Namn/Utvecklare",
|
||||
"asAdded": "Som tillagd",
|
||||
"appSortOrder": "Appsorteringsordning",
|
||||
"ascending": "Stigande",
|
||||
"descending": "Fallande",
|
||||
"bgUpdateCheckInterval": "Bakgrundsuppdateringskollfrekvens",
|
||||
"neverManualOnly": "Aldrig - Endast manuell",
|
||||
"appearance": "Utseende",
|
||||
"showWebInAppView": "Visa källans hemsida i appvyn",
|
||||
"pinUpdates": "Fäst uppdateringar högst upp i appvyn",
|
||||
"updates": "Uppdateringar",
|
||||
"sourceSpecific": "Källspecifik",
|
||||
"appSource": "Appkälla",
|
||||
"noLogs": "Inga Loggar",
|
||||
"appLogs": "Apploggar",
|
||||
"close": "Stäng",
|
||||
"share": "Dela",
|
||||
"appNotFound": "App ej funnen",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Välj en APK",
|
||||
"appHasMoreThanOnePackage": "{} har fler än ett paket:",
|
||||
"deviceSupportsXArch": "Din enhet stödjer {} CPU-arkiktektur.",
|
||||
"deviceSupportsFollowingArchs": "YDin enhet stödjer följande CPU-arkitekturer:",
|
||||
"warning": "Varning",
|
||||
"sourceIsXButPackageFromYPrompt": "Appens källa är '{}' men releasepaketet kommer från '{}'. Vill du fortsätta?",
|
||||
"updatesAvailable": "Uppdateringar Tillgängliga",
|
||||
"updatesAvailableNotifDescription": "Aviserar användaren att det finns uppdateringar tillgängaliga för en eller fler Appar som följs av Obtainium",
|
||||
"noNewUpdates": "Inga nya uppdateringar.",
|
||||
"xHasAnUpdate": "{} har en uppdatering.",
|
||||
"appsUpdated": "Appar Uppdaterade",
|
||||
"appsNotUpdated": "Misslyckades med att uppdatera applikationer",
|
||||
"appsUpdatedNotifDescription": "Meddelar användaren att uppdateringar av en eller flera appar har tillämpats i bakgrunden",
|
||||
"xWasUpdatedToY": "{} uppdaterades till {}.",
|
||||
"xWasNotUpdatedToY": "Det gick inte att uppdatera {} till {}.",
|
||||
"errorCheckingUpdates": "Fel vid uppdateringskoll",
|
||||
"errorCheckingUpdatesNotifDescription": "En aviserings som visar när bakgrundsuppdateringarkollar misslyckas",
|
||||
"appsRemoved": "Appar borttagna",
|
||||
"appsRemovedNotifDescription": "Aviserar användaren när en eller fler Appar togs bort på grund av fel när de laddades",
|
||||
"xWasRemovedDueToErrorY": "{} togs bort på grund av detta felet: {}",
|
||||
"completeAppInstallation": "Gör klar appinstallation",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium måste vara öppet för att installera Appar",
|
||||
"completeAppInstallationNotifDescription": "Frågar användaren att återvända till Obtaiunium när appinstallation är klar",
|
||||
"checkingForUpdates": "Kollar efter Uppdateringar",
|
||||
"checkingForUpdatesNotifDescription": "Övergående meddelande som visas när du söker efter uppdateringar",
|
||||
"pleaseAllowInstallPerm": "Tillåt Obtanium att installera Appar",
|
||||
"trackOnly": "Följ-Endast",
|
||||
"errorWithHttpStatusCode": "Fel {}",
|
||||
"versionCorrectionDisabled": "Versionskorrigering inaktiverat (plugin verkar inte fungera)",
|
||||
"unknown": "Okänd",
|
||||
"none": "Ingen",
|
||||
"never": "Aldrig",
|
||||
"latestVersionX": "Senaste Version: {}",
|
||||
"installedVersionX": "Installerad Version: {}",
|
||||
"lastUpdateCheckX": "Senaste uppdateringskoll: {}",
|
||||
"remove": "Ta bort",
|
||||
"yesMarkUpdated": "Ja, Märk som Uppdaterad",
|
||||
"fdroid": "F-Droid Officiell",
|
||||
"appIdOrName": "App-ID eller Namn",
|
||||
"appId": "App-ID",
|
||||
"appWithIdOrNameNotFound": "Ingen App funnen med det namnet eller ID",
|
||||
"reposHaveMultipleApps": "Förråd kan innehålla flera ApparR",
|
||||
"fdroidThirdPartyRepo": "F-Droid Tredjeparts Förråd",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"install": "Installera",
|
||||
"markInstalled": "Märk Installerad",
|
||||
"update": "Uppdatera",
|
||||
"markUpdated": "Märk Uppdaterad",
|
||||
"additionalOptions": "Ytterligare Alternativ",
|
||||
"disableVersionDetection": "Inaktivera versionsdetektering",
|
||||
"noVersionDetectionExplanation": "Det här alternativet bör endast användas för appar där versionsidentifiering inte fungerar korrekt.",
|
||||
"downloadingX": "Laddar ner {}",
|
||||
"downloadX": "Ladda ner {}",
|
||||
"downloadedX": "Nedladdad {}",
|
||||
"releaseAsset": "Frigör tillgång",
|
||||
"downloadNotifDescription": "Meddelar användaren om framstegen med att ladda ner en app",
|
||||
"noAPKFound": "Ingen APK funnen",
|
||||
"noVersionDetection": "Ingen versiondetektering",
|
||||
"categorize": "Kategorisera",
|
||||
"categories": "Kategorier",
|
||||
"category": "Kategori",
|
||||
"noCategory": "Ingen Kategori",
|
||||
"noCategories": "Inga Kategorier",
|
||||
"deleteCategoriesQuestion": "Ta Bort Kategorier?",
|
||||
"categoryDeleteWarning": "Alla Appar i de borttagna kategorierna kommer att märkas som okategoriserade.",
|
||||
"addCategory": "Lägg till Kategori",
|
||||
"label": "Märka",
|
||||
"language": "Språk",
|
||||
"copiedToClipboard": "Kopierat till Urklipp",
|
||||
"storagePermissionDenied": "Lagringsbehörighet nekad",
|
||||
"selectedCategorizeWarning": "Detta kommer att ersätta alla befintliga kategoriinställningar för de valda apparna.",
|
||||
"filterAPKsByRegEx": "Filtrera APK-filer efter reguljärt uttryck",
|
||||
"removeFromObtainium": "Ta bort från Obtainium",
|
||||
"uninstallFromDevice": "Avinstallera från Enheten",
|
||||
"onlyWorksWithNonVersionDetectApps": "Fungerar bara för Appar med versionsdetektering inaktiverat..",
|
||||
"releaseDateAsVersion": "Använd releasedatum som version",
|
||||
"releaseDateAsVersionExplanation": "Det här alternativet bör endast användas för appar där versionsidentifiering inte fungerar korrekt, men ett releasedatum är tillgängligt.",
|
||||
"changes": "Ändringar",
|
||||
"releaseDate": "Releasedatum",
|
||||
"importFromURLsInFile": "Importera från URL:er i fil (som OPML)",
|
||||
"versionDetectionExplanation": "Sammanställ versionssträng med version upptäckt från OS",
|
||||
"versionDetection": "Versionsdetektering",
|
||||
"standardVersionDetection": "Standardversionsdetektering",
|
||||
"groupByCategory": "Gruppera via Kategori",
|
||||
"autoApkFilterByArch": "Försök att filtrera APK-filer efter CPU-arkitektur om möjligt",
|
||||
"overrideSource": "Överskrid Källa",
|
||||
"dontShowAgain": "Visa inte detta igen",
|
||||
"dontShowTrackOnlyWarnings": "Visa inte 'Följ-Endast' varningar",
|
||||
"dontShowAPKOriginWarnings": "Visa inte APK-ursprung varningar",
|
||||
"moveNonInstalledAppsToBottom": "Flytta icke-installerade appar till botten av appvyn",
|
||||
"gitlabPATLabel": "Personligt åtkomsttoken för GitLab",
|
||||
"about": "Om",
|
||||
"requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Kolla efter uppdateringar vid start",
|
||||
"tryInferAppIdFromCode": "Försök härleda app-id från källkoden",
|
||||
"removeOnExternalUninstall": "Ta bort externt avinstallerade appar automatiskt",
|
||||
"pickHighestVersionCode": "Välj automatiskt högsta versionskod APK",
|
||||
"checkUpdateOnDetailPage": "Sök efter uppdateringar om att öppna en app-detaljsida",
|
||||
"disablePageTransitions": "Inaktivera sidövergångsanimationer",
|
||||
"reversePageTransitions": "Animeringar för omvänd sidövergång",
|
||||
"minStarCount": "Minsta antal stjärnmarkeringar",
|
||||
"addInfoBelow": "Lägg till denna information nedanför.",
|
||||
"addInfoInSettings": "Lägg till denna information i Inställningar.",
|
||||
"githubSourceNote": "GitHub-hastighetsbegränsning kan undvikas med en API-nyckel.",
|
||||
"sortByLastLinkSegment": "Sortera endast efter det sista segmentet av länken",
|
||||
"filterReleaseNotesByRegEx": "Filtrera versionskommentarer efter reguljärt uttryck",
|
||||
"customLinkFilterRegex": "Anpassad APK-länkfiltrera efter reguljärt uttryck (standard '.apk$')",
|
||||
"appsPossiblyUpdated": "Appuppdateringar försökte",
|
||||
"appsPossiblyUpdatedNotifDescription": "Meddelar användaren om att uppdateringar av en eller flera appar potentiellt har tillämpats i bakgrunden",
|
||||
"xWasPossiblyUpdatedToY": "{} kan ha uppdaterats till {}.",
|
||||
"enableBackgroundUpdates": "Aktivera Bakgrundsuppdateringar",
|
||||
"backgroundUpdateReqsExplanation": "Bakgrundsuppdateringar är inte möjligt för alla appar.",
|
||||
"backgroundUpdateLimitsExplanation": "Framgången för en bakgrundsinstallation kan bara avgöras när Obtainium öppnas.",
|
||||
"verifyLatestTag": "Verifiera 'senaste'-taggen",
|
||||
"intermediateLinkRegex": "Filtrera för en \"mellanliggande\" länk att besöka",
|
||||
"filterByLinkText": "Filtrera länkar efter länktext",
|
||||
"intermediateLinkNotFound": "Mellanlänk hittades inte",
|
||||
"intermediateLink": "Mellanlänk",
|
||||
"exemptFromBackgroundUpdates": "Undta från bakgrundsuppdateringar (om aktiverad)",
|
||||
"bgUpdatesOnWiFiOnly": "Inaktivera Bakgrundsuppdateringar utan WiFi",
|
||||
"autoSelectHighestVersionCode": "Välj automatiskt högsta versionskod APK",
|
||||
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||
"matchGroupToUse": "Match Group to Use",
|
||||
"highlightTouchTargets": "Markera mindre uppenbara beröringsobjekt",
|
||||
"pickExportDir": "Välj Exportsökväg",
|
||||
"autoExportOnChanges": "Automatisk export vid ändringar",
|
||||
"includeSettings": "Inkludera inställningar",
|
||||
"filterVersionsByRegEx": "Filtrera versioner efter reguljärt uttryck",
|
||||
"trySelectingSuggestedVersionCode": "Försök att välja föreslagen versionCode APK",
|
||||
"dontSortReleasesList": "Behåll släpporder från API",
|
||||
"reverseSort": "Omvänd sortering",
|
||||
"takeFirstLink": "Ta första länken",
|
||||
"skipSort": "Hoppa över sortering",
|
||||
"debugMenu": "Felsökningsmeny",
|
||||
"bgTaskStarted": "Bakgrundsuppgift startat - kontrollera loggar.",
|
||||
"runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu",
|
||||
"versionExtractWholePage": "Tillämpa Regex för versionssträngextraktion på hela sidan",
|
||||
"installing": "Installerar",
|
||||
"skipUpdateNotifications": "Hoppa över uppdateringsmeddelanden",
|
||||
"updatesAvailableNotifChannel": "Uppdateringar tillgängliga",
|
||||
"appsUpdatedNotifChannel": "Appar uppdaterade",
|
||||
"appsPossiblyUpdatedNotifChannel": "Appuppdateringar försökte",
|
||||
"errorCheckingUpdatesNotifChannel": "Fel vid sökning efter uppdateringar",
|
||||
"appsRemovedNotifChannel": "Appar borttagna",
|
||||
"downloadingXNotifChannel": "Laddar ner {}",
|
||||
"completeAppInstallationNotifChannel": "Fullständig appinstallation",
|
||||
"checkingForUpdatesNotifChannel": "Kollar efter uppdateringar",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Kontrollera bara installerade appar och endast spårbara appar för uppdateringar",
|
||||
"supportFixedAPKURL": "Stöd fasta APK-webbadresser",
|
||||
"selectX": "Välj {}",
|
||||
"parallelDownloads": "Tillåt parallella nedladdningar",
|
||||
"useShizuku": "Använd Shizuku eller Sui för att installera",
|
||||
"shizukuBinderNotFound": "Shizuku is not running",
|
||||
"shizukuOld": "Gammal Shizuku-version (<11) - uppdatera den",
|
||||
"shizukuOldAndroidWithADB": "Shizuku körs på Android < 8.1 med ADB - uppdatera Android eller använd Sui istället",
|
||||
"shizukuPretendToBeGooglePlay": "Ange Google Play som installationskälla (om Shizuku används)",
|
||||
"useSystemFont": "Använd systemteckensnittet",
|
||||
"useVersionCodeAsOSVersion": "Använd appversionskoden som OS-upptäckt version",
|
||||
"requestHeader": "Rubrik för begäran",
|
||||
"useLatestAssetDateAsReleaseDate": "Använd senaste tillgångsuppladdning som releasedatum",
|
||||
"defaultPseudoVersioningMethod": "Standard pseudoversionsmetod",
|
||||
"partialAPKHash": "Delvis APK-hash",
|
||||
"APKLinkHash": "APK-länk Hash",
|
||||
"directAPKLink": "Direkt APK-länk",
|
||||
"pseudoVersionInUse": "En pseudoversion används",
|
||||
"installed": "Installerad",
|
||||
"latest": "Senast",
|
||||
"invertRegEx": "Invertera reguljärt uttryck",
|
||||
"note": "Anmärkning",
|
||||
"selfHostedNote": "Rullgardinsmenyn \"{}\" kan användas för att nå självhostade/anpassade instanser av valfri källa.",
|
||||
"badDownload": "APK kunde inte analyseras (inkompatibel eller partiell nedladdning)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Dela nya appar med AppVerifier (om tillgängligt)",
|
||||
"appVerifierInstructionToast": "Dela till AppVerifier och återvänd sedan hit när du är klar.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Ta Bort App?",
|
||||
"other": "Ta Bort Appar?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "För många förfrågningar (begränsat pris) - försök igen om {} minut",
|
||||
"other": "För många förfrågningar (begränsat pris) - försök igen om {} minuter"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "BG-uppdateringskontroll påträffade en {}, kommer att schemalägga ett nytt försök om {} minut",
|
||||
"other": "BG-uppdateringskontroll påträffade en {}, kommer att schemalägga ett nytt försök om {} minuter"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "BG-uppdateringskontroll hittades {} uppdatering - kommer att meddela användaren om det behövs",
|
||||
"other": "BG-uppdateringskontroll hittades {} uppdateringar - kommer att meddela användaren om det behövs"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} App",
|
||||
"other": "{} Appar"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URL:er"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Minut",
|
||||
"other": "{} Minuter"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Timme",
|
||||
"other": "{} Timmar"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} Dag",
|
||||
"other": "{} Dagar"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Rensade {n} logg (före = {before}, efter = {after})",
|
||||
"other": "Rensade {n} loggar (före = {before}, efter = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} och 1 app till har tillgängliga uppdateringar.",
|
||||
"other": "{} och {} appar till har tillgängliga uppdateringar."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} och 1 till app uppdaterades.",
|
||||
"other": "{} och {} appar till uppdaterades."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Misslyckades med att uppdatera {} och ytterligare 1 app.",
|
||||
"other": "Det gick inte att uppdatera {} och {} fler appar."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} och 1 till app kan ha uppdaterats.",
|
||||
"other": "{} och {} appar till kan ha uppdaterats."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK:er"
|
||||
}
|
||||
}
|
||||
372
assets/translations/tr.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "Geçerli bir {} Uygulama URL'si değil",
|
||||
"noReleaseFound": "Uygun bir sürüm bulunamadı",
|
||||
"noVersionFound": "Sürüm bulunamadı",
|
||||
"urlMatchesNoSource": "URL, bilinen bir kaynağa uymuyor",
|
||||
"cantInstallOlderVersion": "Eski bir sürümü yükleyemem",
|
||||
"appIdMismatch": "İndirilen paket kimliği mevcut Uygulama kimliği ile eşleşmiyor",
|
||||
"functionNotImplemented": "Bu sınıf bu işlevi uygulamamıştır",
|
||||
"placeholder": "Yer Tutucu",
|
||||
"someErrors": "Bazı Hatalar Oluştu",
|
||||
"unexpectedError": "Beklenmeyen Hata",
|
||||
"ok": "Tamam",
|
||||
"and": "ve",
|
||||
"githubPATLabel": "GitHub Kişisel Erişim Anahtarı (Sınırlamayı Artırır)",
|
||||
"includePrereleases": "Ön sürümleri dahil et",
|
||||
"fallbackToOlderReleases": "Daha eski sürümlere geri dön",
|
||||
"filterReleaseTitlesByRegEx": "Düzenli İfadelerle Sürüm Başlıklarını Filtrele",
|
||||
"invalidRegEx": "Geçersiz düzenli ifade",
|
||||
"noDescription": "Açıklama yok",
|
||||
"cancel": "İptal",
|
||||
"continue": "Devam Et",
|
||||
"requiredInBrackets": "(Gerekli)",
|
||||
"dropdownNoOptsError": "HATA: DİPLOMADA EN AZ BİR SEÇENEK OLMALI",
|
||||
"colour": "Renk",
|
||||
"standard": "Standart",
|
||||
"custom": "Özel",
|
||||
"useMaterialYou": "Sizin Malzemenizi Kullanın",
|
||||
"githubStarredRepos": "GitHub'a Yıldızlı Depolar",
|
||||
"uname": "Kullanıcı Adı",
|
||||
"wrongArgNum": "Hatalı argüman sayısı sağlandı",
|
||||
"xIsTrackOnly": "{} yalnızca Takip Edilen",
|
||||
"source": "Kaynak",
|
||||
"app": "Uygulama",
|
||||
"appsFromSourceAreTrackOnly": "Bu kaynaktan gelen uygulamalar 'Yalnızca Takip Edilen'dir.",
|
||||
"youPickedTrackOnly": "'Yalnızca Takip Edilen' seçeneğini seçtiniz.",
|
||||
"trackOnlyAppDescription": "Uygulama güncellemeleri için takip edilecek, ancak Obtainium onu indiremeyecek veya kuramayacaktır.",
|
||||
"cancelled": "İptal Edildi",
|
||||
"appAlreadyAdded": "Uygulama zaten eklenmiş",
|
||||
"alreadyUpToDateQuestion": "Uygulama Zaten Güncel mi?",
|
||||
"addApp": "Uygulama Ekle",
|
||||
"appSourceURL": "Uygulama Kaynak URL'si",
|
||||
"error": "Hata",
|
||||
"add": "Ekle",
|
||||
"searchSomeSourcesLabel": "Ara (Bazı Kaynaklar Yalnızca)",
|
||||
"search": "Ara",
|
||||
"additionalOptsFor": "{} İçin Ek Seçenekler",
|
||||
"supportedSources": "Desteklenen Kaynaklar",
|
||||
"trackOnlyInBrackets": "(Yalnızca Takip)",
|
||||
"searchableInBrackets": "(Aranabilir)",
|
||||
"appsString": "Uygulamalar",
|
||||
"noApps": "Uygulama Yok",
|
||||
"noAppsForFilter": "Filtre İçin Uygulama Yok",
|
||||
"byX": "{} Tarafından",
|
||||
"percentProgress": "İlerleme: {}%",
|
||||
"pleaseWait": "Lütfen Bekleyin",
|
||||
"updateAvailable": "Güncelleme Var",
|
||||
"notInstalled": "Yüklenmedi",
|
||||
"pseudoVersion": "sözde versiyon",
|
||||
"selectAll": "Hepsini Seç",
|
||||
"deselectX": "{}'yi Seçimden Kaldır",
|
||||
"xWillBeRemovedButRemainInstalled": "{} Obtainium'dan kaldırılacak ancak cihazınızda yüklü kalacaktır.",
|
||||
"removeSelectedAppsQuestion": "Seçilen Uygulamaları Kaldırmak İstiyor musunuz?",
|
||||
"removeSelectedApps": "Seçilen Uygulamaları Kaldır",
|
||||
"updateX": "{}'yi Güncelle",
|
||||
"installX": "{}'yi Yükle",
|
||||
"markXTrackOnlyAsUpdated": "{}(Takip Edilen) olarak Güncellendi olarak İşaretle",
|
||||
"changeX": "{}'yi Değiştir",
|
||||
"installUpdateApps": "Uygulamaları Yükle/Güncelle",
|
||||
"installUpdateSelectedApps": "Seçilen Uygulamaları Yükle/Güncelle",
|
||||
"markXSelectedAppsAsUpdated": "Seçilen Uygulamaları {} olarak Güncellendi olarak İşaretle?",
|
||||
"no": "Hayır",
|
||||
"yes": "Evet",
|
||||
"markSelectedAppsUpdated": "Seçilen Uygulamaları Güncellendi olarak İşaretle",
|
||||
"pinToTop": "Üstte Tut",
|
||||
"unpinFromTop": "Üstten Kaldır",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Seçilen Uygulamaların Yükleme Durumunu Sıfırlamak İstiyor musunuz?",
|
||||
"installStatusOfXWillBeResetExplanation": "Seçilen Uygulamaların yükleme durumu sıfırlanacak.\n\nBu, Obtainium'da gösterilen uygulama sürümünün başarısız güncellemeler veya diğer sorunlar nedeniyle yanlış olması durumunda yardımcı olabilir.",
|
||||
"customLinkMessage": "Bu bağlantılar Obtainium'un kurulu olduğu cihazlarda çalışır",
|
||||
"shareAppConfigLinks": "Uygulama yapılandırmasını HTML bağlantısı olarak paylaş",
|
||||
"shareSelectedAppURLs": "Seçilen Uygulama URL'larını Paylaş",
|
||||
"resetInstallStatus": "Yükleme Durumunu Sıfırla",
|
||||
"more": "Daha Fazla",
|
||||
"removeOutdatedFilter": "Güncel Olmayan Uygulama Filtresini Kaldır",
|
||||
"showOutdatedOnly": "Yalnızca Güncel Olmayan Uygulamaları Göster",
|
||||
"filter": "Filtre",
|
||||
"filterApps": "Uygulamaları Filtrele",
|
||||
"appName": "Uygulama Adı",
|
||||
"author": "Yazar",
|
||||
"upToDateApps": "Güncel Uygulamalar",
|
||||
"nonInstalledApps": "Yüklenmemiş Uygulamalar",
|
||||
"importExport": "İçe/Dışa Aktar",
|
||||
"settings": "Ayarlar",
|
||||
"exportedTo": "{}'e Dışa Aktarıldı",
|
||||
"obtainiumExport": "Obtainium Dışa Aktar",
|
||||
"invalidInput": "Geçersiz Giriş",
|
||||
"importedX": "{} İçe Aktarıldı",
|
||||
"obtainiumImport": "Obtainium İçe Aktar",
|
||||
"importFromURLList": "URL Listesinden İçe Aktar (Örneğin OPML)",
|
||||
"searchQuery": "Arama Sorgusu",
|
||||
"appURLList": "Uygulama URL Listesi",
|
||||
"line": "Satır",
|
||||
"searchX": "{}'yi Ara",
|
||||
"noResults": "Sonuç Bulunamadı",
|
||||
"importX": "{} İçe Aktar",
|
||||
"importedAppsIdDisclaimer": "İçe Aktarılan Uygulamalar yanlışlıkla \"Yüklenmedi\" olarak gösterilebilir.\nBunu düzeltmek için bunları Obtainium üzerinden yeniden yükleyin.\nBu, yalnızca URL ve üçüncü taraf içe aktarma yöntemlerini etkiler.\n\nYalnızca URL ve üçüncü taraf içe aktarma yöntemlerini etkiler.",
|
||||
"importErrors": "İçe Aktarma Hataları",
|
||||
"importedXOfYApps": "{}'den {} Uygulama İçe Aktarıldı.",
|
||||
"followingURLsHadErrors": "Aşağıdaki URL'lerde hatalar oluştu:",
|
||||
"selectURL": "URL Seç",
|
||||
"selectURLs": "URL'leri Seç",
|
||||
"pick": "Seç",
|
||||
"theme": "Tema",
|
||||
"dark": "Koyu",
|
||||
"light": "Aydınlık",
|
||||
"followSystem": "Sistemi Takip Et",
|
||||
"useBlackTheme": "Saf siyah koyu temasını kullan",
|
||||
"appSortBy": "Uygulama Sıralama Ölçütü",
|
||||
"authorName": "Yazar/Ad",
|
||||
"nameAuthor": "Ad/Yazar",
|
||||
"asAdded": "Eklendiği Gibi",
|
||||
"appSortOrder": "Uygulama Sıralama Sırası",
|
||||
"ascending": "Artan",
|
||||
"descending": "Azalan",
|
||||
"bgUpdateCheckInterval": "Arka Planda Güncelleme Kontrol Aralığı",
|
||||
"neverManualOnly": "Asla - Yalnızca El ile",
|
||||
"appearance": "Görünüm",
|
||||
"showWebInAppView": "Kaynağı Uygulama Görünümünde Göster",
|
||||
"pinUpdates": "Güncellemeleri Uygulamalar Görünümünün Üstüne Sabitle",
|
||||
"updates": "Güncellemeler",
|
||||
"sourceSpecific": "Kaynak Özel",
|
||||
"appSource": "Uygulama Kaynağı",
|
||||
"noLogs": "Günlük Yok",
|
||||
"appLogs": "Uygulama Günlükleri",
|
||||
"close": "Kapat",
|
||||
"share": "Paylaş",
|
||||
"appNotFound": "Uygulama Bulunamadı",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-ihracat",
|
||||
"pickAnAPK": "APK Seç",
|
||||
"appHasMoreThanOnePackage": "{}'nin birden fazla paketi var:",
|
||||
"deviceSupportsXArch": "Cihazınız {} CPU mimarisini destekliyor.",
|
||||
"deviceSupportsFollowingArchs": "Cihazınız aşağıdaki CPU mimarilerini destekliyor:",
|
||||
"warning": "Uyarı",
|
||||
"sourceIsXButPackageFromYPrompt": "Uygulama kaynağı '{}', ancak dağıtım paketi '{}'. Devam etmek istiyor musunuz?",
|
||||
"updatesAvailable": "Güncellemeler Var",
|
||||
"updatesAvailableNotifDescription": "Kullanıcıya Obtainium tarafından takip edilen bir veya daha fazla uygulama için güncelleme bulunduğuna dair bilgi verir",
|
||||
"noNewUpdates": "Yeni güncelleme yok.",
|
||||
"xHasAnUpdate": "{} güncelleme alıyor.",
|
||||
"appsUpdated": "Uygulamalar Güncellendi",
|
||||
"appsNotUpdated": "Uygulamalar güncellenemedi",
|
||||
"appsUpdatedNotifDescription": "Kullanıcıya bir veya daha fazla uygulamanın arka planda güncellendiğine dair bilgi verir",
|
||||
"xWasUpdatedToY": "{} şu sürüme güncellendi: {}.",
|
||||
"xWasNotUpdatedToY": "{} öğesi {} olarak güncellenemedi.",
|
||||
"errorCheckingUpdates": "Güncellemeler Kontrol Edilirken Hata Oluştu",
|
||||
"errorCheckingUpdatesNotifDescription": "Arka planda güncelleme kontrolü sırasında hata oluştuğunda görünen bir bildirim",
|
||||
"appsRemoved": "Uygulamalar Kaldırıldı",
|
||||
"appsRemovedNotifDescription": "Bir veya daha fazla uygulamanın yüklenirken hata nedeniyle kaldırıldığını bildiren bir bildirim",
|
||||
"xWasRemovedDueToErrorY": "{} şu hatadan dolayı kaldırıldı: {}",
|
||||
"completeAppInstallation": "Uygulama Yüklemeyi Tamamla",
|
||||
"obtainiumMustBeOpenToInstallApps": "Uygulamaları yüklemek için Obtainium'un açık olması gerekiyor",
|
||||
"completeAppInstallationNotifDescription": "Kullanıcıdan Obtainium'a geri dönüp bir uygulama yüklemeyi tamamlamasını isteyen bir bildirim",
|
||||
"checkingForUpdates": "Güncellemeler Kontrol Ediliyor",
|
||||
"checkingForUpdatesNotifDescription": "Güncellemeler kontrol edildiğinde görünen geçici bir bildirim",
|
||||
"pleaseAllowInstallPerm": "Lütfen Obtainium'un uygulama yüklemesine izin verin",
|
||||
"trackOnly": "Sadece Takip Et",
|
||||
"errorWithHttpStatusCode": "Hata {}",
|
||||
"versionCorrectionDisabled": "Sürüm düzeltme devre dışı bırakıldı (eklenti çalışmıyor gibi görünüyor)",
|
||||
"unknown": "Bilinmiyor",
|
||||
"none": "Hiçbiri",
|
||||
"never": "Asla",
|
||||
"latestVersionX": "En Son Sürüm: {}",
|
||||
"installedVersionX": "Yüklenen Sürüm: {}",
|
||||
"lastUpdateCheckX": "Son Güncelleme Kontrolü: {}",
|
||||
"remove": "Kaldır",
|
||||
"yesMarkUpdated": "Evet, Güncellendi olarak İşaretle",
|
||||
"fdroid": "F-Droid Resmi",
|
||||
"appIdOrName": "Uygulama Kimliği veya Adı",
|
||||
"appId": "Uygulama Kimliği",
|
||||
"appWithIdOrNameNotFound": "Bu kimlik veya ada sahip bir uygulama bulunamadı",
|
||||
"reposHaveMultipleApps": "Depolar birden fazla uygulama içerebilir",
|
||||
"fdroidThirdPartyRepo": "F-Droid Üçüncü Taraf Depo",
|
||||
"steamMobile": "Buhar Mobil",
|
||||
"steamChat": "Steam Sohbet",
|
||||
"install": "Yükle",
|
||||
"markInstalled": "Yüklendi olarak İşaretle",
|
||||
"update": "Güncelle",
|
||||
"markUpdated": "Güncellendi olarak İşaretle",
|
||||
"additionalOptions": "Ek Seçenekler",
|
||||
"disableVersionDetection": "Sürüm Algılama Devre Dışı",
|
||||
"noVersionDetectionExplanation": "Bu seçenek, sürüm algılamanın doğru çalışmadığı uygulamalar için kullanılmalıdır.",
|
||||
"downloadingX": "{} İndiriliyor",
|
||||
"downloadX": "İndir {}",
|
||||
"downloadedX": "İndirildi {}",
|
||||
"releaseAsset": "Varlık Serbest Bırakma",
|
||||
"downloadNotifDescription": "Bir uygulamanın indirme sürecinde ilerlemeyi bildiren bir bildirim",
|
||||
"noAPKFound": "APK bulunamadı",
|
||||
"noVersionDetection": "Sürüm Algılanamıyor",
|
||||
"categorize": "Kategorize Et",
|
||||
"categories": "Kategoriler",
|
||||
"category": "Kategori",
|
||||
"noCategory": "Kategori Yok",
|
||||
"noCategories": "Kategoriler Yok",
|
||||
"deleteCategoriesQuestion": "Kategorileri Silmek İstiyor musunuz?",
|
||||
"categoryDeleteWarning": "Silinen kategorilerdeki tüm uygulamalar kategorisiz olarak ayarlanacaktır.",
|
||||
"addCategory": "Kategori Ekle",
|
||||
"label": "Etiket",
|
||||
"language": "Dil",
|
||||
"copiedToClipboard": "Panoya Kopyalandı",
|
||||
"storagePermissionDenied": "Depolama izni reddedildi",
|
||||
"selectedCategorizeWarning": "Bu, seçilen uygulamalar için mevcut kategori ayarlarını değiştirecektir.",
|
||||
"filterAPKsByRegEx": "APK'leri Düzenli İfade ile Filtrele",
|
||||
"removeFromObtainium": "Obtainium'dan Kaldır",
|
||||
"uninstallFromDevice": "Cihazdan Kaldır",
|
||||
"onlyWorksWithNonVersionDetectApps": "Yalnızca Sürüm Algılaması Devre Dışı Uygulamalar İçin Çalışır.",
|
||||
"releaseDateAsVersion": "Sürüm Olarak Yayın Tarihi Kullan",
|
||||
"releaseDateAsVersionExplanation": "Bu seçenek, sürüm algılamanın doğru çalışmadığı ancak bir sürüm tarihinin mevcut olduğu uygulamalar için kullanılmalıdır.",
|
||||
"changes": "Değişiklikler",
|
||||
"releaseDate": "Yayın Tarihi",
|
||||
"importFromURLsInFile": "Dosyadaki URL'lerden İçe Aktar",
|
||||
"versionDetectionExplanation": "Sürüm dizesini işletim sistemi tarafından algılanan sürümle uzlaştır",
|
||||
"versionDetection": "Sürüm Tespiti",
|
||||
"standardVersionDetection": "Standart sürüm tespiti",
|
||||
"groupByCategory": "Kategoriye Göre Grupla",
|
||||
"autoApkFilterByArch": "Mümkünse APK'leri CPU mimarisi ile filtreleme girişimi",
|
||||
"overrideSource": "Kaynağı Geçersiz Kıl",
|
||||
"dontShowAgain": "Bunu tekrar gösterme",
|
||||
"dontShowTrackOnlyWarnings": "'Yalnızca Takip Edilen' uyarılarını gösterme",
|
||||
"dontShowAPKOriginWarnings": "APK kaynağı uyarılarını gösterme",
|
||||
"moveNonInstalledAppsToBottom": "Yüklenmemiş Uygulamaları Uygulamalar Görünümünün Altına Taşı",
|
||||
"gitlabPATLabel": "GitLab Kişisel Erişim Belirteci",
|
||||
"about": "Hakkında",
|
||||
"requiresCredentialsInSettings": "{}: Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)",
|
||||
"checkOnStart": "Başlangıçta güncellemeleri kontrol et",
|
||||
"tryInferAppIdFromCode": "Uygulama kimliğini kaynak kodundan çıkarma girişimi",
|
||||
"removeOnExternalUninstall": "Harici kaldırmada otomatik olarak kaldırılan uygulamalar",
|
||||
"pickHighestVersionCode": "Otomatik olarak en yüksek sürüm kodlu APK'yi seç",
|
||||
"checkUpdateOnDetailPage": "Uygulama detay sayfasını açarken güncellemeleri kontrol et",
|
||||
"disablePageTransitions": "Sayfa geçiş animasyonlarını devre dışı bırak",
|
||||
"reversePageTransitions": "Sayfa geçiş animasyonlarını tersine çevir",
|
||||
"minStarCount": "Minimum Yıldız Sayısı",
|
||||
"addInfoBelow": "Bu bilgiyi aşağıya ekle.",
|
||||
"addInfoInSettings": "Bu bilgiyi Ayarlar'da ekleyin.",
|
||||
"githubSourceNote": "GitHub hız sınırlaması bir API anahtarı kullanılarak atlanabilir.",
|
||||
"sortByLastLinkSegment": "Bağlantının yalnızca son bölümüne göre sırala",
|
||||
"filterReleaseNotesByRegEx": "Sürüm Notlarını Düzenli İfade ile Filtrele",
|
||||
"customLinkFilterRegex": "Özel APK Bağlantı Filtresi Düzenli İfade ile (Varsayılan '.apk$')",
|
||||
"appsPossiblyUpdated": "Uygulama Güncellemeleri Denendi",
|
||||
"appsPossiblyUpdatedNotifDescription": "Kullanıcıya bir veya daha fazla uygulamanın arka planda potansiyel olarak güncellendiğini bildiren bildirim",
|
||||
"xWasPossiblyUpdatedToY": "{} muhtemelen {} sürümüne güncellendi.",
|
||||
"enableBackgroundUpdates": "Arka plan güncellemelerini etkinleştir",
|
||||
"backgroundUpdateReqsExplanation": "Arka plan güncellemeleri tüm uygulamalar için mümkün olmayabilir.",
|
||||
"backgroundUpdateLimitsExplanation": "Arka plan kurulumunun başarısı, Obtainium'un açıldığında ancak belirlenebilir.",
|
||||
"verifyLatestTag": "'latest' etiketini doğrula",
|
||||
"intermediateLinkRegex": "Ziyaret Edilecek 'Orta Düzey' Bağlantıyı Filtrele",
|
||||
"filterByLinkText": "Bağlantıları bağlantı metnine göre filtrele",
|
||||
"intermediateLinkNotFound": "Ara bağlantı bulunamadı",
|
||||
"intermediateLink": "Ara bağlantı",
|
||||
"exemptFromBackgroundUpdates": "Arka plan güncellemelerinden muaf tut (etkinse)",
|
||||
"bgUpdatesOnWiFiOnly": "WiFi olmadığında arka plan güncellemelerini devre dışı bırak",
|
||||
"autoSelectHighestVersionCode": "Otomatik olarak en yüksek sürüm kodunu seç",
|
||||
"versionExtractionRegEx": "Sürüm Çıkarma Düzenli İfade",
|
||||
"matchGroupToUse": "Sürüm Çıkarma Regex için Kullanılacak Eşleşme Grubu",
|
||||
"highlightTouchTargets": "Daha az belirgin dokunma hedeflerini vurgula",
|
||||
"pickExportDir": "Dışa Aktarılacak Klasörü Seç",
|
||||
"autoExportOnChanges": "Değişikliklerde otomatik olarak dışa aktar",
|
||||
"includeSettings": "Ayarları dahil et",
|
||||
"filterVersionsByRegEx": "Sürümleri Düzenli İfade ile Filtrele",
|
||||
"trySelectingSuggestedVersionCode": "Önerilen sürüm kodunu seçmeyi dene",
|
||||
"dontSortReleasesList": "API'den sıralama düzenini koru",
|
||||
"reverseSort": "Ters sıralama",
|
||||
"takeFirstLink": "İlk bağlantıyı al",
|
||||
"skipSort": "Sıralamayı atla",
|
||||
"debugMenu": "Hata Ayıklama Menüsü",
|
||||
"bgTaskStarted": "Arka plan görevi başladı - günlükleri kontrol et.",
|
||||
"runBgCheckNow": "Arka Plan Güncelleme Kontrolünü Şimdi Çalıştır",
|
||||
"versionExtractWholePage": "Tüm Sayfaya Sürüm Çıkarma Regex'i Uygula",
|
||||
"installing": "Yükleniyor",
|
||||
"skipUpdateNotifications": "Güncelleme bildirimlerini atla",
|
||||
"updatesAvailableNotifChannel": "Güncellemeler Mevcut",
|
||||
"appsUpdatedNotifChannel": "Güncellenen Uygulamalar",
|
||||
"appsPossiblyUpdatedNotifChannel": "Uygulama Güncellemeleri Denendi",
|
||||
"errorCheckingUpdatesNotifChannel": "Güncellemeler Kontrol Edilirken Hata",
|
||||
"appsRemovedNotifChannel": "Kaldırılan Uygulamalar",
|
||||
"downloadingXNotifChannel": "{} İndiriliyor",
|
||||
"completeAppInstallationNotifChannel": "Uygulama Kurulumu Tamamlandı",
|
||||
"checkingForUpdatesNotifChannel": "Güncellemeler Kontrol Ediliyor",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme",
|
||||
"supportFixedAPKURL": "Sabit APK URL'lerini destekleyin",
|
||||
"selectX": "Seçme {}",
|
||||
"parallelDownloads": "Paralel indirmelere izin ver",
|
||||
"useShizuku": "Yüklemek için Shizuku veya Sui'yi kullanın",
|
||||
"shizukuBinderNotFound": "Shizuku is not running",
|
||||
"shizukuOld": "Eski Shizuku sürümü (<11) - güncelleyin",
|
||||
"shizukuOldAndroidWithADB": "Shizuku ADB ile Android < 8.1 üzerinde çalışıyor - Android'i güncelleyin veya bunun yerine Sui kullanın",
|
||||
"shizukuPretendToBeGooglePlay": "Google Play'i yükleme kaynağı olarak ayarlayın (Shizuku kullanılıyorsa)",
|
||||
"useSystemFont": "Sistem yazı tipini kullan",
|
||||
"useVersionCodeAsOSVersion": "Uygulama versionCode'unu işletim sistemi tarafından algılanan sürüm olarak kullan",
|
||||
"requestHeader": "Başlık talep et",
|
||||
"useLatestAssetDateAsReleaseDate": "Yayın tarihi olarak en son öğe yüklemesini kullan",
|
||||
"defaultPseudoVersioningMethod": "Varsayılan Sözde Sürüm Oluşturma Yöntemi",
|
||||
"partialAPKHash": "Kısmi APK Karması",
|
||||
"APKLinkHash": "APK Bağlantı Karması",
|
||||
"directAPKLink": "Doğrudan APK Bağlantısı",
|
||||
"pseudoVersionInUse": "Sözde Versiyon Kullanımda",
|
||||
"installed": "Kurulmuş",
|
||||
"latest": "En sonuncu",
|
||||
"invertRegEx": "Normal ifadeyi ters çevir",
|
||||
"note": "Not",
|
||||
"selfHostedNote": "\"{}\" açılır menüsü, herhangi bir kaynağın kendi kendine barındırılan/özel örneklerine ulaşmak için kullanılabilir.",
|
||||
"badDownload": "APK ayrıştırılamadı (uyumsuz veya kısmi indirme)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Yeni Uygulamaları AppVerifier ile paylaşın (varsa)",
|
||||
"appVerifierInstructionToast": "AppVerifier ile paylaşın, hazır olduğunuzda buraya dönün.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Uygulamayı Kaldır?",
|
||||
"other": "Uygulamaları Kaldır?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Çok fazla istek (hız sınırlı) - {} dakika sonra tekrar deneyin",
|
||||
"other": "Çok fazla istek (hız sınırlı) - {} dakika sonra tekrar deneyin"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Arka plan güncelleme kontrolü bir hatayla karşılaştı, {} dakika sonra tekrar kontrol edilecek",
|
||||
"other": "Arka plan güncelleme kontrolü bir hatayla karşılaştı, {} dakika sonra tekrar kontrol edilecek"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Arka plan güncelleme kontrolü {} güncelleme buldu - gerektiğinde kullanıcıyı bilgilendirecek",
|
||||
"other": "Arka plan güncelleme kontrolü {} güncelleme buldu - gerektiğinde kullanıcıyı bilgilendirecek"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} Uygulama",
|
||||
"other": "{} Uygulamalar"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URL'ler"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Dakika",
|
||||
"other": "{} Dakika"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Saat",
|
||||
"other": "{} Saat"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} Gün",
|
||||
"other": "{} Gün"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "{n} log temizlendi (önce = {before}, sonra = {after})",
|
||||
"other": "{n} log temizlendi (önce = {before}, sonra = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} ve 1 diğer uygulama güncellemeye sahip.",
|
||||
"other": "{} ve {} daha fazla uygulama güncellemeye sahip."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} ve 1 diğer uygulama güncellendi.",
|
||||
"other": "{} ve {} daha fazla uygulama güncellendi."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "{} ve 1 uygulama daha güncellenemedi.",
|
||||
"other": "{} ve {} daha fazla uygulama güncellenemedi."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} ve 1 diğer uygulama muhtemelen güncellendi.",
|
||||
"other": "{} ve {} daha fazla uygulama muhtemelen güncellendi."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK'lar"
|
||||
}
|
||||
}
|
||||
372
assets/translations/uk.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "Неправильна URL-адреса для джерела застосунку {}",
|
||||
"noReleaseFound": "Не вдалося знайти відповідне видання",
|
||||
"noVersionFound": "Не вдалося визначити версію видання",
|
||||
"urlMatchesNoSource": "URL не відповідає відомому джерелу",
|
||||
"cantInstallOlderVersion": "Не можна встановити старішу версію застосунку",
|
||||
"appIdMismatch": "Ідентифікатор пакета, завантажений, не відповідає ідентифікатору існуючого застосунку",
|
||||
"functionNotImplemented": "Цей клас не реалізував цю функцію",
|
||||
"placeholder": "Заповнювач",
|
||||
"someErrors": "Виникла деяка помилка",
|
||||
"unexpectedError": "Неочікувана помилка",
|
||||
"ok": "Добре",
|
||||
"and": "та",
|
||||
"githubPATLabel": "Персональний ключ доступу GitHub (збільшує обмеження на швидкість)",
|
||||
"includePrereleases": "Включити попередні видання",
|
||||
"fallbackToOlderReleases": "Повернутися до старіших видань",
|
||||
"filterReleaseTitlesByRegEx": "Фільтрувати заголовки видань за допомогою регулярного виразу",
|
||||
"invalidRegEx": "Неприпустимий регулярний вираз",
|
||||
"noDescription": "Немає опису",
|
||||
"cancel": "Скасувати",
|
||||
"continue": "Продовжити",
|
||||
"requiredInBrackets": "(Обов'язково)",
|
||||
"dropdownNoOptsError": "ПОМИЛКА: В ВИПАДАЮЧОМУ СПИСКУ МАЄ БУТИ ХОЧА Б ОДИН ЕЛЕМЕНТ",
|
||||
"colour": "Колір",
|
||||
"standard": "Стандартний",
|
||||
"custom": "Нестандартний",
|
||||
"useMaterialYou": "Використовуйте матеріал, який ви",
|
||||
"githubStarredRepos": "Відзначені репозиторії GitHub",
|
||||
"uname": "Ім'я користувача",
|
||||
"wrongArgNum": "Надано неправильну кількість аргументів",
|
||||
"xIsTrackOnly": "{} - тільки відстежування",
|
||||
"source": "Джерело",
|
||||
"app": "застосунок",
|
||||
"appsFromSourceAreTrackOnly": "Застосунки з цього джерела є лише для відстежування.",
|
||||
"youPickedTrackOnly": "Ви вибрали опцію лише для відстежування.",
|
||||
"trackOnlyAppDescription": "Застосунок буде відстежуватися для оновлень, але Obtainium не зможе його завантажити або встановити.",
|
||||
"cancelled": "Скасовано",
|
||||
"appAlreadyAdded": "Застосунок вже додано",
|
||||
"alreadyUpToDateQuestion": "Застосунок вже оновлено?",
|
||||
"addApp": "Додати",
|
||||
"appSourceURL": "URL-адреса джерела застосунку",
|
||||
"error": "Помилка",
|
||||
"add": "Додати",
|
||||
"searchSomeSourcesLabel": "Пошук (Лише деякі джерела)",
|
||||
"search": "Пошук",
|
||||
"additionalOptsFor": "Додаткові опції для {}",
|
||||
"supportedSources": "Підтримувані джерела",
|
||||
"trackOnlyInBrackets": "(Тільки для відстеження)",
|
||||
"searchableInBrackets": "(Можливий пошук)",
|
||||
"appsString": "Застосунки",
|
||||
"noApps": "Застосунків немає",
|
||||
"noAppsForFilter": "Застосунків для фільтрації немає",
|
||||
"byX": "Від {}",
|
||||
"percentProgress": "Прогрес: {}%",
|
||||
"pleaseWait": "Будь ласка, зачекайте",
|
||||
"updateAvailable": "Доступно оновлення",
|
||||
"notInstalled": "Не встановлено",
|
||||
"pseudoVersion": "псевдо-версія",
|
||||
"selectAll": "Вибрати все",
|
||||
"deselectX": "Скасувати вибір {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} буде видалено з Obtainium, але залишиться встановленим на пристрої.",
|
||||
"removeSelectedAppsQuestion": "Видалити вибрані застосунки?",
|
||||
"removeSelectedApps": "Видалити вибрані застосунки",
|
||||
"updateX": "Оновити {}",
|
||||
"installX": "Встановити {}",
|
||||
"markXTrackOnlyAsUpdated": "Позначити {}\n(Тільки відстежування)\nяк оновлено",
|
||||
"changeX": "Змінити {}",
|
||||
"installUpdateApps": "Встановити/Оновити застосунки",
|
||||
"installUpdateSelectedApps": "Встановити/Оновити вибрані застосунки",
|
||||
"markXSelectedAppsAsUpdated": "Позначити {} вибрані застосунки як оновлені?",
|
||||
"no": "Ні",
|
||||
"yes": "Так",
|
||||
"markSelectedAppsUpdated": "Позначити вибрані застосунки як оновлені",
|
||||
"pinToTop": "Закріпити угорі",
|
||||
"unpinFromTop": "Відкріпити зверху",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Скинути статус встановлення для вибраних застосунків?",
|
||||
"installStatusOfXWillBeResetExplanation": "Статус встановлення будь-яких вибраних застосунків буде скинутий.\n\nЦе може допомогти, коли версія застосунку, відображена в Obtainium, є неправильною через невдалі оновлення або інші проблеми.",
|
||||
"customLinkMessage": "Ці посилання працюють на пристроях з встановленим Obtainium",
|
||||
"shareAppConfigLinks": "Поділитися посиланнями на конфігурацію Застосунку як HTML",
|
||||
"shareSelectedAppURLs": "Поділитися вибраними URL-адресами застосунків",
|
||||
"resetInstallStatus": "Скинути статус встановлення",
|
||||
"more": "Більше",
|
||||
"removeOutdatedFilter": "Видалити фільтр застарілих застосунків",
|
||||
"showOutdatedOnly": "Показати лише застарілі застосунки",
|
||||
"filter": "Фільтр",
|
||||
"filterApps": "Фільтрувати застосунки",
|
||||
"appName": "Назва застосунку",
|
||||
"author": "Автор",
|
||||
"upToDateApps": "Актуальні застосунки",
|
||||
"nonInstalledApps": "Невстановлені застосунки",
|
||||
"importExport": "Імпорт/Експорт",
|
||||
"settings": "Налаштування",
|
||||
"exportedTo": "Експортовано в {}",
|
||||
"obtainiumExport": "Експорт з Obtainium",
|
||||
"invalidInput": "Недійсний ввід",
|
||||
"importedX": "Імпортовано {}",
|
||||
"obtainiumImport": "Імпорт в Obtainium",
|
||||
"importFromURLList": "Імпорт зі списку URL-адрес",
|
||||
"searchQuery": "Пошуковий запит",
|
||||
"appURLList": "Список URL-адрес застосунків",
|
||||
"line": "Лінія",
|
||||
"searchX": "Пошук {}",
|
||||
"noResults": "Результати відсутні",
|
||||
"importX": "Імпорт {}",
|
||||
"importedAppsIdDisclaimer": "Імпортовані застосунки можуть неправильно відображатися як \"Не встановлені\".\nДля виправлення цього перевстановіть їх через Obtainium.\nЦе не повинно вплинути на дані застосунків.\n\nПов'язано лише з URL-адресами та імпортом від третіх сторін.",
|
||||
"importErrors": "Помилки імпорту",
|
||||
"importedXOfYApps": "Імпортовано {} з {} застосунків.",
|
||||
"followingURLsHadErrors": "Помилки в наступних URL-адресах:",
|
||||
"selectURL": "Вибрати URL",
|
||||
"selectURLs": "Вибрати URL-адреси",
|
||||
"pick": "Вибрати",
|
||||
"theme": "Тема",
|
||||
"dark": "Темна",
|
||||
"light": "Світла",
|
||||
"followSystem": "Дотримуватися системи",
|
||||
"useBlackTheme": "Використовувати чорну тему (Amoled)",
|
||||
"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": "Застосунки оновлено",
|
||||
"appsNotUpdated": "Не вдалося оновити програми",
|
||||
"appsUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного чи декількох застосунків було застосовано в фоновому режимі",
|
||||
"xWasUpdatedToY": "{} було оновлено до {}.",
|
||||
"xWasNotUpdatedToY": "Не вдалося оновити {} на {}.",
|
||||
"errorCheckingUpdates": "Помилка перевірки оновлень",
|
||||
"errorCheckingUpdatesNotifDescription": "Повідомлення, яке з'являється, коли перевірка оновлень в фоновому режимі завершується невдачею",
|
||||
"appsRemoved": "Застосунки видалено",
|
||||
"appsRemovedNotifDescription": "Повідомляє користувача, що один чи декілька застосунків були видалені через помилки при завантаженні",
|
||||
"xWasRemovedDueToErrorY": "{} було видалено через цю помилку: {}",
|
||||
"completeAppInstallation": "Завершення установки застосунку",
|
||||
"obtainiumMustBeOpenToInstallApps": "Для встановлення застосунків Obtainium має бути відкритий",
|
||||
"completeAppInstallationNotifDescription": "Прохання користувача повернутися до Obtainium для завершення установки застосунку",
|
||||
"checkingForUpdates": "Перевірка оновлень",
|
||||
"checkingForUpdatesNotifDescription": "Тимчасове повідомлення, яке з'являється при перевірці оновлень",
|
||||
"pleaseAllowInstallPerm": "Будь ласка, дозвольте Obtainium встановлювати застосунки",
|
||||
"trackOnly": "Тільки відстеження",
|
||||
"errorWithHttpStatusCode": "Помилка {} HTTP-коду",
|
||||
"versionCorrectionDisabled": "Виправлення версії вимкнено (здається, плагін не працює)",
|
||||
"unknown": "Невідомо",
|
||||
"none": "Нічого",
|
||||
"never": "Ніколи",
|
||||
"latestVersionX": "Остання версія: {}",
|
||||
"installedVersionX": "Встановлено: {}",
|
||||
"lastUpdateCheckX": "Остання перевірка оновлень: {}",
|
||||
"remove": "Видалити",
|
||||
"yesMarkUpdated": "Так, позначити як оновлене",
|
||||
"fdroid": "F-Droid Офіційний",
|
||||
"appIdOrName": "Ідентифікатор або назва застосунку",
|
||||
"appId": "Ідентифікатор застосунку",
|
||||
"appWithIdOrNameNotFound": "Застосунок з таким ідентифікатором або назвою не знайдено",
|
||||
"reposHaveMultipleApps": "Сховища можуть містити кілька застосунків",
|
||||
"fdroidThirdPartyRepo": "F-Droid Стороннє сховище",
|
||||
"steamMobile": "Мобільний Steam",
|
||||
"steamChat": "Чат Steam",
|
||||
"install": "Встановити",
|
||||
"markInstalled": "Позначити як встановлене",
|
||||
"update": "Оновити",
|
||||
"markUpdated": "Позначити як оновлене",
|
||||
"additionalOptions": "Додаткові опції",
|
||||
"disableVersionDetection": "Вимкнути визначення версії",
|
||||
"noVersionDetectionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно.",
|
||||
"downloadingX": "Завантаження {}",
|
||||
"downloadX": "Завантажити {}",
|
||||
"downloadedX": "Завантажено {}",
|
||||
"releaseAsset": "Звільнити актив",
|
||||
"downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку",
|
||||
"noAPKFound": "APK не знайдено",
|
||||
"noVersionDetection": "Визначення версії відключено",
|
||||
"categorize": "Категоризувати",
|
||||
"categories": "Категорії",
|
||||
"category": "Категорія",
|
||||
"noCategory": "Без категорії",
|
||||
"noCategories": "Немає категорій",
|
||||
"deleteCategoriesQuestion": "Видалити категорії?",
|
||||
"categoryDeleteWarning": "Усі застосунки у видалених категоріях будуть переведені у некатегоризовані.",
|
||||
"addCategory": "Додати категорію",
|
||||
"label": "Мітка",
|
||||
"language": "Мова",
|
||||
"copiedToClipboard": "Скопійовано в буфер обміну",
|
||||
"storagePermissionDenied": "Відмовлено у дозволі на доступ до сховища",
|
||||
"selectedCategorizeWarning": "Це замінить будь-які існуючі налаштування категорій для вибраних застосунків.",
|
||||
"filterAPKsByRegEx": "Фільтрувати APK за регулярним виразом",
|
||||
"removeFromObtainium": "Видалити з Obtainium",
|
||||
"uninstallFromDevice": "Видалити з пристрою",
|
||||
"onlyWorksWithNonVersionDetectApps": "Працює лише з застосунками з вимкненим визначенням версії.",
|
||||
"releaseDateAsVersion": "Використовувати дату випуску як рядок версії",
|
||||
"releaseDateAsVersionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно, але є дата випуску.",
|
||||
"changes": "Зміни",
|
||||
"releaseDate": "Дата випуску",
|
||||
"importFromURLsInFile": "Імпорт з URL-адрес у файлі (наприклад, OPML)",
|
||||
"versionDetectionExplanation": "Порівняти рядок версії з версією, визначеною операційною системою",
|
||||
"versionDetection": "Визначення версії",
|
||||
"standardVersionDetection": "Стандартне визначення версії",
|
||||
"groupByCategory": "Групувати за категоріями",
|
||||
"autoApkFilterByArch": "Спробувати фільтрувати APK за архітектурою ЦП, якщо можливо",
|
||||
"overrideSource": "Перевизначити джерело",
|
||||
"dontShowAgain": "Не показувати це знову",
|
||||
"dontShowTrackOnlyWarnings": "Не показувати попередження про 'Тільки відстеження'",
|
||||
"dontShowAPKOriginWarnings": "Не показувати попередження про походження APK",
|
||||
"moveNonInstalledAppsToBottom": "Перемістити невстановлені застосунки вниз у перегляді застосунків",
|
||||
"gitlabPATLabel": "Особистий токен GitLab (Увімкнення пошуку та краще виявлення APK)",
|
||||
"about": "Про програму",
|
||||
"requiresCredentialsInSettings": "{} потребує додаткових облікових даних (у налаштуваннях)",
|
||||
"checkOnStart": "Перевірити наявність оновлень при запуску",
|
||||
"tryInferAppIdFromCode": "Спробувати вивести ідентифікатор застосунку з вихідного коду",
|
||||
"removeOnExternalUninstall": "Автоматично видаляти застосунки, які було видалено зовнішнім чином",
|
||||
"pickHighestVersionCode": "Автоматично вибрати APK з найвищим кодом версії",
|
||||
"checkUpdateOnDetailPage": "Перевіряти наявність оновлень при відкритті сторінки деталей застосунку",
|
||||
"disablePageTransitions": "Вимкнути анімації переходів між сторінками",
|
||||
"reversePageTransitions": "Зворотні анімації переходів між сторінками",
|
||||
"minStarCount": "Мінімальна кількість зірок",
|
||||
"addInfoBelow": "Додати цю інформацію нижче.",
|
||||
"addInfoInSettings": "Додати цю інформацію у налаштуваннях.",
|
||||
"githubSourceNote": "Лімітування швидкості GitHub можна уникнути, використовуючи ключ API.",
|
||||
"sortByLastLinkSegment": "Сортувати лише за останнім сегментом посилання",
|
||||
"filterReleaseNotesByRegEx": "Фільтрувати примітки до релізу за регулярним виразом",
|
||||
"customLinkFilterRegex": "Фільтр кастомного посилання на APK за регулярним виразом (за замовчуванням '.apk$')",
|
||||
"appsPossiblyUpdated": "Спроб оновлення застосунків",
|
||||
"appsPossiblyUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного або декількох застосунків можливо були застосовані в фоновому режимі",
|
||||
"xWasPossiblyUpdatedToY": "{} можливо було оновлено до {}.",
|
||||
"enableBackgroundUpdates": "Увімкнути оновлення в фоновому режимі",
|
||||
"backgroundUpdateReqsExplanation": "Оновлення в фоновому режимі може бути неможливим для всіх застосунків.",
|
||||
"backgroundUpdateLimitsExplanation": "Успіх фонової установки може бути визначений лише після відкриття Obtainium.",
|
||||
"verifyLatestTag": "Перевірити тег 'latest'",
|
||||
"intermediateLinkRegex": "Фільтр для 'Проміжного' Посилання для Відвідування",
|
||||
"filterByLinkText": "Фільтрувати посилання за текстом посилання",
|
||||
"intermediateLinkNotFound": "Проміжне посилання не знайдено",
|
||||
"intermediateLink": "Проміжне посилання",
|
||||
"exemptFromBackgroundUpdates": "Виключено з фонових оновлень (якщо ввімкнено)",
|
||||
"bgUpdatesOnWiFiOnly": "Вимкнути фонові оновлення поза Wi-Fi",
|
||||
"autoSelectHighestVersionCode": "Автоматичний вибір APK з найвищим кодом версії",
|
||||
"versionExtractionRegEx": "Регулярний вираз для вилучення рядка версії",
|
||||
"matchGroupToUse": "Група співпадінь для використання в регулярному виразі вилучення версії",
|
||||
"highlightTouchTargets": "Підсвічувати менш очевидні області дотику",
|
||||
"pickExportDir": "Вибрати каталог експорту",
|
||||
"autoExportOnChanges": "Автоматичний експорт при змінах",
|
||||
"includeSettings": "Включити налаштування",
|
||||
"filterVersionsByRegEx": "Фільтрувати версії за регулярним виразом",
|
||||
"trySelectingSuggestedVersionCode": "Спробуйте вибрати запропонований код версії APK",
|
||||
"dontSortReleasesList": "Зберігати порядок випуску з API",
|
||||
"reverseSort": "Зворотне сортування",
|
||||
"takeFirstLink": "Вибрати перше посилання",
|
||||
"skipSort": "Пропустити сортування",
|
||||
"debugMenu": "Меню налагодження",
|
||||
"bgTaskStarted": "Запущено фонове завдання - перевірте журнали.",
|
||||
"runBgCheckNow": "Запустити перевірку оновлень в фоновому режимі зараз",
|
||||
"versionExtractWholePage": "Застосувати регулярний вираз вилучення версії до всієї сторінки",
|
||||
"installing": "Встановлення",
|
||||
"skipUpdateNotifications": "Пропустити сповіщення про оновлення",
|
||||
"updatesAvailableNotifChannel": "Доступні оновлення",
|
||||
"appsUpdatedNotifChannel": "Застосунки оновлені",
|
||||
"appsPossiblyUpdatedNotifChannel": "Спроба оновлення застосунків",
|
||||
"errorCheckingUpdatesNotifChannel": "Помилка перевірки оновлень",
|
||||
"appsRemovedNotifChannel": "Застосунки видалені",
|
||||
"downloadingXNotifChannel": "Завантаження {}",
|
||||
"completeAppInstallationNotifChannel": "Завершення встановлення застосунку",
|
||||
"checkingForUpdatesNotifChannel": "Перевірка оновлень",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Перевіряти лише встановлені та застосунки, які відстежуються для оновлень",
|
||||
"supportFixedAPKURL": "Підтримка фіксованих посилань на APK",
|
||||
"selectX": "Вибрати {}",
|
||||
"parallelDownloads": "Дозволити паралельні завантаження",
|
||||
"useShizuku": "Використовуйте Shizuku або Sui для встановлення",
|
||||
"shizukuBinderNotFound": "Сумісний сервіс Shizuku не було знайдено",
|
||||
"shizukuOld": "Стара версія Shizuku (<11) - оновіть її",
|
||||
"shizukuOldAndroidWithADB": "Shizuku працює на Android < 8.1 з ADB - оновіть Android або використовуйте Sui замість нього",
|
||||
"shizukuPretendToBeGooglePlay": "Виберіть Google Play як джерело встановлення (якщо використовується Shizuku)",
|
||||
"useSystemFont": "Використовувати системний шрифт",
|
||||
"useVersionCodeAsOSVersion": "Використовувати код версії застосунку як версію, визначену операційною системою",
|
||||
"requestHeader": "Заголовок запиту",
|
||||
"useLatestAssetDateAsReleaseDate": "Використовувати останню дату завантаження ресурсу як дату випуску",
|
||||
"defaultPseudoVersioningMethod": "Метод за замовчуванням псевдо-версіонування",
|
||||
"partialAPKHash": "Хеш часткового APK",
|
||||
"APKLinkHash": "Хеш посилання на APK",
|
||||
"directAPKLink": "Пряме посилання на APK",
|
||||
"pseudoVersionInUse": "Використовується псевдо-версія",
|
||||
"installed": "Встановлено",
|
||||
"latest": "Остання",
|
||||
"invertRegEx": "Інвертувати регулярний вираз",
|
||||
"note": "Примітка",
|
||||
"selfHostedNote": "Випадаючий список \"{}\" може використовуватися для доступу до власних/призначених для самостійного використання екземплярів будь-якого джерела.",
|
||||
"badDownload": "APK не вдалося розпарсити (несумісний або часткове завантаження)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Діліться новими додатками з AppVerifier (якщо доступно)",
|
||||
"appVerifierInstructionToast": "Надішліть на AppVerifier, а потім поверніться сюди, коли будете готові.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Видалити застосунок?",
|
||||
"other": "Видалити застосунки?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Забагато запитів (обмеження швидкості) - повторіть спробу через {} хвилину",
|
||||
"other": "Забагато запитів (обмеження швидкості) - повторіть спробу через {} хвилин"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Помилка перевірки оновлень у фоновому режимі - спробую знову через {} хвилину",
|
||||
"other": "Помилка перевірки оновлень у фоновому режимі - спробую знову через {} хвилин"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Фонова перевірка оновлень знайшла {} оновлення - сповістити користувача, якщо це необхідно",
|
||||
"other": "Фонова перевірка оновлень знайшла {} оновлень - сповістити користувача, якщо це необхідно"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} застосунок",
|
||||
"other": "{} застосунки"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL-адреса",
|
||||
"other": "{} URL-адреси"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} хвилина",
|
||||
"other": "{} хвилин"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} година",
|
||||
"other": "{} годин"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} день",
|
||||
"other": "{} днів"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Очищено {n} журнал (до = {before}, після = {after})",
|
||||
"other": "Очищено {n} журналів (до = {before}, після = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} та ще 1 застосунок мають оновлення.",
|
||||
"other": "{} та ще {} застосунки мають оновлення."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} та ще 1 застосунок було оновлено.",
|
||||
"other": "{} та ще {} застосунків було оновлено."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Не вдалося оновити {} та ще 1 програму.",
|
||||
"other": "Не вдалося оновити {} і {} та інші програми."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} та ще 1 застосунок можливо було оновлено.",
|
||||
"other": "{} та ще {} застосунків можливо було оновлено."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APK-файли"
|
||||
}
|
||||
}
|
||||
372
assets/translations/vi.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"invalidURLForSource": "URL ứng dụng {} không hợp lệ",
|
||||
"noReleaseFound": "Không thể tìm thấy bản phát hành phù hợp",
|
||||
"noVersionFound": "Không thể xác định phiên bản phát hành",
|
||||
"urlMatchesNoSource": "URL không khớp với nguồn đã biết",
|
||||
"cantInstallOlderVersion": "Không thể cài đặt phiên bản cũ hơn của Ứng dụng",
|
||||
"appIdMismatch": "ID gói đã tải xuống không khớp với ID ứng dụng hiện tại",
|
||||
"functionNotImplemented": "Lớp này chưa triển khai chức năng này",
|
||||
"placeholder": "Giữ chỗ",
|
||||
"someErrors": "Đã xảy ra một số lỗi",
|
||||
"unexpectedError": "Lỗi không mong đợi",
|
||||
"ok": "OK",
|
||||
"and": "và",
|
||||
"githubPATLabel": "Token truy cập cá nhân GitHub (Cải thiện tốc độ giới hạn)",
|
||||
"includePrereleases": "Bao gồm các bản phát hành trước",
|
||||
"fallbackToOlderReleases": "Dự phòng về bản phát hành cũ hơn",
|
||||
"filterReleaseTitlesByRegEx": "Lọc tiêu đề bản phát hành theo biểu thức chính quy",
|
||||
"invalidRegEx": "Biểu thức chính quy không hợp lệ",
|
||||
"noDescription": "Không có mô tả",
|
||||
"cancel": "Hủy",
|
||||
"continue": "Tiếp tục",
|
||||
"requiredInBrackets": "(Yêu cầu)",
|
||||
"dropdownNoOptsError": "LỖI: TẢI XUỐNG PHẢI CÓ ÍT NHẤT MỘT LỰA CHỌN",
|
||||
"colour": "Màu sắc",
|
||||
"standard": "Standard",
|
||||
"custom": "Custom",
|
||||
"useMaterialYou": "Use Material You",
|
||||
"githubStarredRepos": "Kho lưu trữ có gắn dấu sao GitHub",
|
||||
"uname": "Tên người dùng",
|
||||
"wrongArgNum": "Số lượng đối số được cung cấp sai",
|
||||
"xIsTrackOnly": "{} là Chỉ theo dõi",
|
||||
"source": "Nguồn",
|
||||
"app": "Ứng dụng",
|
||||
"appsFromSourceAreTrackOnly": "Các ứng dụng từ nguồn này là 'Chỉ theo dõi'.",
|
||||
"youPickedTrackOnly": "Bạn đã chọn tùy chọn 'Chỉ theo dõi'.",
|
||||
"trackOnlyAppDescription": "Ứng dụng sẽ được theo dõi để cập nhật, nhưng Obtainium sẽ không thể tải xuống hoặc cài đặt nó.",
|
||||
"cancelled": "Đã hủy",
|
||||
"appAlreadyAdded": "Ứng dụng được thêm rồi",
|
||||
"alreadyUpToDateQuestion": "Ứng dụng đã được cập nhật?",
|
||||
"addApp": "Thêm",
|
||||
"appSourceURL": "URL nguồn ứng dụng",
|
||||
"error": "Lỗi",
|
||||
"add": "Thêm",
|
||||
"searchSomeSourcesLabel": "Tìm kiếm (Chỉ một số nguồn)",
|
||||
"search": "Tìm kiếm",
|
||||
"additionalOptsFor": "Tùy chọn bổ sung cho {}",
|
||||
"supportedSources": "Nguồn được hỗ trợ",
|
||||
"trackOnlyInBrackets": "(Chỉ theo dõi)",
|
||||
"searchableInBrackets": "(Có thể tìm kiếm)",
|
||||
"appsString": "Ứng dụng",
|
||||
"noApps": "Không có ứng dụng",
|
||||
"noAppsForFilter": "Không có ứng dụng cho bộ lọc",
|
||||
"byX": "Bởi {}",
|
||||
"percentProgress": "Đang tải {}%",
|
||||
"pleaseWait": "Vui lòng chờ",
|
||||
"updateAvailable": "Có sẵn bản cập nhật",
|
||||
"notInstalled": "Chưa cài đặt",
|
||||
"pseudoVersion": "phiên bản giả",
|
||||
"selectAll": "Chọn tất cả",
|
||||
"deselectX": "Bỏ chọn {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} sẽ bị xóa khỏi Obtainium nhưng vẫn còn cài đặt trên thiết bị.",
|
||||
"removeSelectedAppsQuestion": "Xóa ứng dụng đã chọn?",
|
||||
"removeSelectedApps": "Xóa ứng dụng đã chọn",
|
||||
"updateX": "Cập nhật {}",
|
||||
"installX": "Cài đặt {}",
|
||||
"markXTrackOnlyAsUpdated": "Đánh dấu {}\n(Chỉ theo dõi)\nnhư là đã cập nhật",
|
||||
"changeX": "Thay đổi {}",
|
||||
"installUpdateApps": "Cài đặt/Cập nhật ứng dụng",
|
||||
"installUpdateSelectedApps": "Cài đặt/Cập nhật ứng dụng đã chọn",
|
||||
"markXSelectedAppsAsUpdated": "Đánh dấu {} ứng dụng đã chọn là đã cập nhật?",
|
||||
"no": "Không",
|
||||
"yes": "Đúng",
|
||||
"markSelectedAppsUpdated": "Đánh dấu các ứng dụng đã chọn là đã cập nhật",
|
||||
"pinToTop": "Ghim đầu trang",
|
||||
"unpinFromTop": "Bỏ ghim khỏi đầu trang",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Đặt lại trạng thái cài đặt cho ứng dụng đã chọn?",
|
||||
"installStatusOfXWillBeResetExplanation": "Trạng thái cài đặt của mọi Ứng dụng đã chọn sẽ được đặt lại.\n\nĐiều này có thể hữu ích khi phiên bản Ứng dụng hiển thị trong Obtainium không chính xác do cập nhật không thành công hoặc các sự cố khác.",
|
||||
"customLinkMessage": "Các liên kết này hoạt động trên các thiết bị có cài đặt Obtainium",
|
||||
"shareAppConfigLinks": "Chia sẻ cấu hình ứng dụng dưới dạng liên kết HTML",
|
||||
"shareSelectedAppURLs": "Chia sẻ URL ứng dụng đã chọn",
|
||||
"resetInstallStatus": "Đặt lại trạng thái cài đặt",
|
||||
"more": "Nhiều hơn",
|
||||
"removeOutdatedFilter": "Xóa bộ lọc ứng dụng lỗi thời",
|
||||
"showOutdatedOnly": "Chỉ hiển thị các ứng dụng lỗi thời",
|
||||
"filter": "Lọc",
|
||||
"filterApps": "Lọc ứng dụng",
|
||||
"appName": "Tên ứng dụng",
|
||||
"author": "Tác giả",
|
||||
"upToDateApps": "Ứng dụng cập nhật",
|
||||
"nonInstalledApps": "Ứng dụng chưa được cài đặt",
|
||||
"importExport": "Nhập/Xuất",
|
||||
"settings": "Thiết đặt",
|
||||
"exportedTo": "Đã xuất sang {}",
|
||||
"obtainiumExport": "Xuất",
|
||||
"invalidInput": "Đầu vào không hợp lệ",
|
||||
"importedX": "Đã nhập {}",
|
||||
"obtainiumImport": "Nhập",
|
||||
"importFromURLList": "Nhập từ danh sách URL",
|
||||
"searchQuery": "Truy vấn tìm kiếm",
|
||||
"appURLList": "Danh sách URL ứng dụng",
|
||||
"line": "Hàng",
|
||||
"searchX": "Tìm kiếm {}",
|
||||
"noResults": "Không có kết quả nào được tìm thấy",
|
||||
"importX": "Nhập {}",
|
||||
"importedAppsIdDisclaimer": "Ứng dụng đã nhập có thể hiển thị không chính xác là \"Chưa được cài đặt\".\nĐể khắc phục sự cố này, hãy cài đặt lại chúng thông qua Obtainium.\nĐiều này sẽ không ảnh hưởng đến dữ liệu Ứng dụng.\n\nChỉ ảnh hưởng đến URL và phương thức nhập của bên thứ ba.",
|
||||
"importErrors": "Lỗi nhập",
|
||||
"importedXOfYApps": "{} trong số {} Ứng dụng đã được nhập.",
|
||||
"followingURLsHadErrors": "Các URL sau có lỗi:",
|
||||
"selectURL": "Chọn URL",
|
||||
"selectURLs": "Chọn URL",
|
||||
"pick": "Chọn",
|
||||
"theme": "Chủ đề",
|
||||
"dark": "Tối",
|
||||
"light": "Sáng",
|
||||
"followSystem": "Theo hệ thống",
|
||||
"useBlackTheme": "Nền đen",
|
||||
"appSortBy": "Sắp xếp ứng dụng",
|
||||
"authorName": "Tác giả",
|
||||
"nameAuthor": "Tên",
|
||||
"asAdded": "Như đã thêm",
|
||||
"appSortOrder": "Thứ tự sắp xếp",
|
||||
"ascending": "Tăng dần",
|
||||
"descending": "Giảm dần",
|
||||
"bgUpdateCheckInterval": "Thời gian tự động kiểm tra cập nhật",
|
||||
"neverManualOnly": "Không bao giờ",
|
||||
"appearance": "Hiển thị",
|
||||
"showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem chi tiết Ứng dụng",
|
||||
"pinUpdates": "Chuyển ứng dụng có phiên bản mới lên đầu danh sách",
|
||||
"updates": "Cập nhật",
|
||||
"sourceSpecific": "Cài đặt Nguồn",
|
||||
"appSource": "Nguồn ứng dụng",
|
||||
"noLogs": "Không có nhật ký",
|
||||
"appLogs": "Nhật ký ứng dụng",
|
||||
"close": "Đóng",
|
||||
"share": "Chia sẻ",
|
||||
"appNotFound": "Không tìm thấy ứng dụng",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Chọn một APK",
|
||||
"appHasMoreThanOnePackage": "{} có nhiều gói:",
|
||||
"deviceSupportsXArch": "Thiết bị của bạn hỗ trợ kiến trúc CPU {}.",
|
||||
"deviceSupportsFollowingArchs": "Thiết bị của bạn hỗ trợ các kiến trúc CPU sau:",
|
||||
"warning": "Cảnh báo",
|
||||
"sourceIsXButPackageFromYPrompt": "Nguồn ứng dụng là '{}' nhưng gói phát hành đến từ '{}'. Tiếp tục?",
|
||||
"updatesAvailable": "Cập nhật có sẵn",
|
||||
"updatesAvailableNotifDescription": "Thông báo cho người dùng rằng có bản cập nhật cho một hoặc nhiều Ứng dụng được theo dõi bởi Obtainium",
|
||||
"noNewUpdates": "Không có bản cập nhật mới.",
|
||||
"xHasAnUpdate": "{} có bản cập nhật.",
|
||||
"appsUpdated": "Ứng dụng đã cập nhật ",
|
||||
"appsNotUpdated": "Failed to update applications",
|
||||
"appsUpdatedNotifDescription": "Thông báo cho người dùng rằng các bản cập nhật cho một hoặc nhiều Ứng dụng đã được áp dụng trong nền",
|
||||
"xWasUpdatedToY": "{} đã được cập nhật thành {}.",
|
||||
"xWasNotUpdatedToY": "Failed to update {} to {}.",
|
||||
"errorCheckingUpdates": "Lỗi kiểm tra bản cập nhật",
|
||||
"errorCheckingUpdatesNotifDescription": "Thông báo hiển thị khi kiểm tra cập nhật nền không thành công",
|
||||
"appsRemoved": "Ứng dụng đã loại bỏ",
|
||||
"appsRemovedNotifDescription": "Thông báo cho người dùng rằng một hoặc nhiều Ứng dụng đã bị loại bỏ do lỗi khi tải chúng",
|
||||
"xWasRemovedDueToErrorY": "{} đã bị loại bỏ do lỗi này: {}",
|
||||
"completeAppInstallation": "Hoàn tất cài đặt ứng dụng",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium phải được mở để cài đặt Ứng dụng",
|
||||
"completeAppInstallationNotifDescription": "Yêu cầu người dùng quay lại Obtainium để hoàn tất cài đặt Ứng dụng",
|
||||
"checkingForUpdates": "Đang kiểm tra cập nhật",
|
||||
"checkingForUpdatesNotifDescription": "Thông báo tạm thời xuất hiện khi kiểm tra bản cập nhật",
|
||||
"pleaseAllowInstallPerm": "Vui lòng cho phép Obtainium cài đặt Ứng dụng",
|
||||
"trackOnly": "Chỉ theo dõi",
|
||||
"errorWithHttpStatusCode": "Lỗi {}",
|
||||
"versionCorrectionDisabled": "Tính năng sửa phiên bản bị vô hiệu hóa (plugin dường như không hoạt động)",
|
||||
"unknown": "Không xác định",
|
||||
"none": "Không",
|
||||
"never": "Không bao giờ",
|
||||
"latestVersionX": "Phiên bản mới nhất: {}",
|
||||
"installedVersionX": "Phiên bản đã cài đặt: {}",
|
||||
"lastUpdateCheckX": "Kiểm tra cập nhật lần cuối: {}",
|
||||
"remove": "Loại bỏ",
|
||||
"yesMarkUpdated": "Có, Đánh dấu là đã cập nhật",
|
||||
"fdroid": "F-Droid Chính thức",
|
||||
"appIdOrName": "ID hoặc tên ứng dụng",
|
||||
"appId": "ID ứng dụng",
|
||||
"appWithIdOrNameNotFound": "Không tìm thấy ứng dụng nào có ID hoặc tên đó",
|
||||
"reposHaveMultipleApps": "Kho có thể chứa nhiều Ứng dụng",
|
||||
"fdroidThirdPartyRepo": "Kho lưu trữ bên thứ ba F-Droid",
|
||||
"steamMobile": "Điện thoại di động hơi nước",
|
||||
"steamChat": "Trò chuyện hơi nước",
|
||||
"install": "Cài đặt",
|
||||
"markInstalled": "Đánh dấu là đã cài đặt",
|
||||
"update": "Cập nhật",
|
||||
"markUpdated": "Đánh dấu đã cập nhật",
|
||||
"additionalOptions": "Tùy chọn bổ sung",
|
||||
"disableVersionDetection": "Tắt tính năng phát hiện phiên bản",
|
||||
"noVersionDetectionExplanation": "Chỉ nên sử dụng tùy chọn này cho Ứng dụng mà tính năng phát hiện phiên bản không hoạt động chính xác.",
|
||||
"downloadingX": "Đang tải xuống {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Thông báo cho người dùng về tiến trình tải xuống Ứng dụng",
|
||||
"noAPKFound": "Không tìm thấy APK",
|
||||
"noVersionDetection": "Không phát hiện phiên bản",
|
||||
"categorize": "Phân loại",
|
||||
"categories": "Danh mục",
|
||||
"category": "Danh mục",
|
||||
"noCategory": "Không danh mục",
|
||||
"noCategories": "Không danh mục",
|
||||
"deleteCategoriesQuestion": "Xóa danh mục?",
|
||||
"categoryDeleteWarning": "Tất cả ứng dụng trong danh mục đã xóa sẽ được đặt thành chưa được phân loại.",
|
||||
"addCategory": "Thêm thể loại",
|
||||
"label": "Nhãn",
|
||||
"language": "Ngôn ngữ",
|
||||
"copiedToClipboard": "Sao chép vào clipboard",
|
||||
"storagePermissionDenied": "Quyền lưu trữ bị từ chối",
|
||||
"selectedCategorizeWarning": "Điều này sẽ thay thế mọi thiết đặt danh mục hiện có cho Ứng dụng đã chọn.",
|
||||
"filterAPKsByRegEx": "Lọc APK theo biểu thức chính quy",
|
||||
"removeFromObtainium": "Loại khỏi Obtainium",
|
||||
"uninstallFromDevice": "Gỡ cài đặt khỏi thiết bị",
|
||||
"onlyWorksWithNonVersionDetectApps": "Chỉ hoạt động với Ứng dụng đã tắt tính năng phát hiện phiên bản.",
|
||||
"releaseDateAsVersion": "Sử dụng ngày phát hành làm phiên bản",
|
||||
"releaseDateAsVersionExplanation": "Chỉ nên sử dụng tùy chọn này cho Ứng dụng trong đó tính năng phát hiện phiên bản không hoạt động chính xác nhưng đã có ngày phát hành.",
|
||||
"changes": "Thay đổi",
|
||||
"releaseDate": "Ngày phát hành",
|
||||
"importFromURLsInFile": "Nhập từ URL trong Tệp (như OPML)",
|
||||
"versionDetectionExplanation": "Đối chiếu chuỗi phiên bản với phiên bản được phát hiện từ hệ điều hành",
|
||||
"versionDetection": "Phát hiện phiên bản",
|
||||
"standardVersionDetection": "Phát hiện phiên bản tiêu chuẩn",
|
||||
"groupByCategory": "Nhóm theo danh mục",
|
||||
"autoApkFilterByArch": "Cố gắng lọc APK theo kiến trúc CPU nếu có thể",
|
||||
"overrideSource": "Ghi đè nguồn",
|
||||
"dontShowAgain": "Đừng hiển thị thông tin này nữa",
|
||||
"dontShowTrackOnlyWarnings": "Không hiển thị cảnh báo 'Chỉ theo dõi'",
|
||||
"dontShowAPKOriginWarnings": "Không hiển thị cảnh báo nguồn gốc APK",
|
||||
"moveNonInstalledAppsToBottom": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách",
|
||||
"gitlabPATLabel": "Token truy cập cá nhân GitLab",
|
||||
"about": "Giới thiệu",
|
||||
"requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Thiết đặt)",
|
||||
"checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
|
||||
"tryInferAppIdFromCode": "Thử suy ra ID ứng dụng từ mã nguồn",
|
||||
"removeOnExternalUninstall": "Tự động xóa ứng dụng đã gỡ cài đặt bên ngoài",
|
||||
"pickHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
|
||||
"checkUpdateOnDetailPage": "Kiểm tra các bản cập nhật khi mở trang chi tiết Ứng dụng",
|
||||
"disablePageTransitions": "Tắt hiệu ứng chuyển trang",
|
||||
"reversePageTransitions": "Hoạt ảnh chuyển đổi trang đảo ngược",
|
||||
"minStarCount": "Số lượng sao tối thiểu",
|
||||
"addInfoBelow": "Thêm thông tin này vào bên dưới.",
|
||||
"addInfoInSettings": "Thêm thông tin này vào Thiết đặt.",
|
||||
"githubSourceNote": "Có thể tránh được việc giới hạn tốc độ GitHub bằng cách sử dụng khóa API.",
|
||||
"sortByLastLinkSegment": "Chỉ sắp xếp theo đoạn cuối của liên kết",
|
||||
"filterReleaseNotesByRegEx": "Lọc ghi chú phát hành theo biểu thức chính quy",
|
||||
"customLinkFilterRegex": "Bộ lọc liên kết APK tùy chỉnh theo biểu thức chính quy (Mặc định '.apk$')",
|
||||
"appsPossiblyUpdated": "Đã cố gắng cập nhật ứng dụng",
|
||||
"appsPossiblyUpdatedNotifDescription": "Thông báo cho người dùng rằng các bản cập nhật cho một hoặc nhiều Ứng dụng có khả năng được áp dụng trong nền",
|
||||
"xWasPossiblyUpdatedToY": "{} có thể đã được cập nhật thành {}.",
|
||||
"enableBackgroundUpdates": "Tự động cập nhật trong nền",
|
||||
"backgroundUpdateReqsExplanation": "Có thể không thực hiện được cập nhật trong nền cho tất cả ứng dụng.",
|
||||
"backgroundUpdateLimitsExplanation": "Sự thành công của cài đặt trong nền chỉ có thể được xác định khi mở Obtainium.",
|
||||
"verifyLatestTag": "Xác minh thẻ 'mới nhất'",
|
||||
"intermediateLinkRegex": "Lọc tìm liên kết 'Trung cấp' để truy cập",
|
||||
"filterByLinkText": "Lọc liên kết theo văn bản liên kết",
|
||||
"intermediateLinkNotFound": "Không tìm thấy liên kết trung gian",
|
||||
"intermediateLink": "Liên kết trung gian",
|
||||
"exemptFromBackgroundUpdates": "Miễn cập nhật nền (nếu được bật)",
|
||||
"bgUpdatesOnWiFiOnly": "Tắt cập nhật nền khi không có WiFi",
|
||||
"autoSelectHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
|
||||
"versionExtractionRegEx": "Trích xuất phiên bản RegEx",
|
||||
"matchGroupToUse": "Nhóm đối sánh để sử dụng cho Regex trích xuất phiên bản",
|
||||
"highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
|
||||
"pickExportDir": "Chọn thư mục xuất",
|
||||
"autoExportOnChanges": "Tự động xuất",
|
||||
"includeSettings": "Bao gồm thiết đặt",
|
||||
"filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy",
|
||||
"trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
|
||||
"dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",
|
||||
"reverseSort": "Sắp xếp ngược",
|
||||
"takeFirstLink": "Lấy liên kết đầu tiên",
|
||||
"skipSort": "Bỏ qua việc sắp xếp",
|
||||
"debugMenu": "Danh sách gỡ lỗi",
|
||||
"bgTaskStarted": "Tác vụ nền đã bắt đầu - kiểm tra nhật ký.",
|
||||
"runBgCheckNow": "Chạy kiểm tra cập nhật nền ngay bây giờ",
|
||||
"versionExtractWholePage": "Áp dụng Regex trích xuất phiên bản cho toàn bộ trang",
|
||||
"installing": "Đang cài đặt",
|
||||
"skipUpdateNotifications": "Bỏ qua thông báo cập nhật",
|
||||
"updatesAvailableNotifChannel": "Cập nhật có sẵn",
|
||||
"appsUpdatedNotifChannel": "Đã cập nhật ứng dụng",
|
||||
"appsPossiblyUpdatedNotifChannel": "Đã cố gắng cập nhật ứng dụng",
|
||||
"errorCheckingUpdatesNotifChannel": "Lỗi kiểm tra bản cập nhật",
|
||||
"appsRemovedNotifChannel": "Ứng dụng đã bị loại bỏ",
|
||||
"downloadingXNotifChannel": "Đang tải xuống {}",
|
||||
"completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng",
|
||||
"checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra cập nhật các ứng dụng đã cài đặt và Chỉ theo dõi",
|
||||
"supportFixedAPKURL": "Hỗ trợ URL APK cố định",
|
||||
"selectX": "Lựa chọn {}",
|
||||
"parallelDownloads": "Cho phép tải đa luồng",
|
||||
"useShizuku": "Use Shizuku or Sui to install",
|
||||
"shizukuBinderNotFound": "Shizuku chưa khởi động",
|
||||
"shizukuOld": "Old Shizuku version (<11) - update it",
|
||||
"shizukuOldAndroidWithADB": "Shizuku running on Android < 8.1 with ADB - update Android or use Sui instead",
|
||||
"shizukuPretendToBeGooglePlay": "Set Google Play as the installation source (if Shizuku is used)",
|
||||
"useSystemFont": "Sử dụng phông chữ hệ thống",
|
||||
"useVersionCodeAsOSVersion": "Sử dụng Mã phiên bản ứng dụng làm phiên bản do hệ điều hành phát hiện",
|
||||
"requestHeader": "Tiêu đề yêu cầu",
|
||||
"useLatestAssetDateAsReleaseDate": "Sử dụng nội dung tải lên mới nhất làm ngày phát hành",
|
||||
"defaultPseudoVersioningMethod": "Phương pháp tạo phiên bản giả mặc định",
|
||||
"partialAPKHash": "Băm APK một phần",
|
||||
"APKLinkHash": "Băm liên kết APK",
|
||||
"directAPKLink": "Liên kết APK trực tiếp",
|
||||
"pseudoVersionInUse": "Phiên bản giả đang được sử dụng",
|
||||
"installed": "Đã cài đặt",
|
||||
"latest": "Mới nhất",
|
||||
"invertRegEx": "Đảo ngược biểu thức chính quy",
|
||||
"note": "Ghi chú",
|
||||
"selfHostedNote": "Trình đơn thả xuống \"{}\" có thể được dùng để tiếp cận các phiên bản tự lưu trữ/tùy chỉnh của bất kỳ nguồn nào.",
|
||||
"badDownload": "Không thể phân tích cú pháp APK (tải xuống một phần hoặc không tương thích)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Chia sẻ ứng dụng mới với AppVerifier (nếu có)",
|
||||
"appVerifierInstructionToast": "Chia sẻ lên AppVerifier, sau đó quay lại đây khi sẵn sàng.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Gỡ ứng dụng?",
|
||||
"other": "Gỡ ứng dụng?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Quá nhiều yêu cầu (tốc độ giới hạn) - hãy thử lại sau {} phút",
|
||||
"other": "Quá nhiều yêu cầu (tốc độ giới hạn) - hãy thử lại sau {} phút"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Việc kiểm tra bản cập nhật BG gặp phải {}, sẽ lên lịch kiểm tra lại sau {} phút",
|
||||
"other": "Việc kiểm tra bản cập nhật BG gặp phải {}, sẽ lên lịch kiểm tra lại sau {} phút"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Đang kiểm tra bản cập nhật BG tìm thấy {} bản cập nhật - sẽ thông báo cho người dùng nếu cần",
|
||||
"other": "Đang kiểm tra bản cập nhật BG tìm thấy {} bản cập nhật - sẽ thông báo cho người dùng nếu cần"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} Ứng dụng",
|
||||
"other": "{} Ứng dụng"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URL"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Phút",
|
||||
"other": "{} Phút"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Giờ",
|
||||
"other": "{} Giờ"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} Ngày",
|
||||
"other": "{} ngày"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Đã xóa {n} nhật ký (trước = {trước}, sau = {sau})",
|
||||
"other": "Đã xóa {n} nhật ký (trước = {trước}, sau = {sau})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} và 1 ứng dụng khác có bản cập nhật.",
|
||||
"other": "{} và {} ứng dụng khác có bản cập nhật."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} và 1 ứng dụng khác đã được cập nhật.",
|
||||
"other": "{} và {} ứng dụng khác đã được cập nhật."
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "Failed to update {} and 1 more app.",
|
||||
"other": "Failed to update {} and {} more apps."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} và 1 ứng dụng khác có thể đã được cập nhật.",
|
||||
"other": "{} và {} ứng dụng khác có thể đã được cập nhật."
|
||||
},
|
||||
"apk": {
|
||||
"one": "{} APK",
|
||||
"other": "{} APKs"
|
||||
}
|
||||
}
|
||||
@@ -1,244 +1,329 @@
|
||||
{
|
||||
"invalidURLForSource": "不是一个有效的 {} URL",
|
||||
"noReleaseFound": "找不到合适的更新",
|
||||
"noVersionFound": "无法确定更新版本",
|
||||
"urlMatchesNoSource": "URL 与已知来源不符",
|
||||
"cantInstallOlderVersion": "无法安装旧版应用程序",
|
||||
"appIdMismatch": "下载的软件包名与现有的应用程序包名不一致",
|
||||
"functionNotImplemented": "该类没有实现此功能",
|
||||
"invalidURLForSource": "无效的 {} URL",
|
||||
"noReleaseFound": "找不到合适的发行版",
|
||||
"noVersionFound": "无法确定发行版本号",
|
||||
"urlMatchesNoSource": "URL 与已知的来源不符",
|
||||
"cantInstallOlderVersion": "无法安装旧版本的应用",
|
||||
"appIdMismatch": "所下载 APK 的应用 ID 与现有应用不一致",
|
||||
"functionNotImplemented": "该类未实现此功能",
|
||||
"placeholder": "占位符",
|
||||
"someErrors": "出现了一些错误",
|
||||
"unexpectedError": "意外错误",
|
||||
"ok": "好的",
|
||||
"and": "和",
|
||||
"startedBgUpdateTask": "开始后台检查更新任务",
|
||||
"bgUpdateIgnoreAfterIs": "下次后台更新检查 {}",
|
||||
"startedActualBGUpdateCheck": "后台检查更新已开始",
|
||||
"bgUpdateTaskFinished": "后台检查更新已完成",
|
||||
"firstRun": "这是你第一次运行 Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "设置检查更新间隔为 {}",
|
||||
"githubPATLabel": "GitHub 个人访问令牌 (提高 API 限制)",
|
||||
"githubPATHint": "个人访问令牌必须为: username:token 形式",
|
||||
"githubPATFormat": "username:token",
|
||||
"githubPATLinkText": "关于 GitHub 个人访问令牌",
|
||||
"includePrereleases": "包含预发布版",
|
||||
"fallbackToOlderReleases": "回退到旧版",
|
||||
"filterReleaseTitlesByRegEx": "使用正则以过滤发布标题",
|
||||
"invalidRegEx": "表达式无效",
|
||||
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
||||
"includePrereleases": "包含预发行版",
|
||||
"fallbackToOlderReleases": "将旧发行版作为备选",
|
||||
"filterReleaseTitlesByRegEx": "筛选发行标题(正则表达式)",
|
||||
"invalidRegEx": "无效的正则表达式",
|
||||
"noDescription": "无描述",
|
||||
"cancel": "取消",
|
||||
"continue": "继续",
|
||||
"requiredInBrackets": "(必须)",
|
||||
"dropdownNoOptsError": "错误:下拉菜单必须至少有一个选项",
|
||||
"colour": "颜色",
|
||||
"githubStarredRepos": "GitHub 已星标仓库",
|
||||
"requiredInBrackets": "(必填)",
|
||||
"dropdownNoOptsError": "错误:下拉菜单必须包含至少一个选项",
|
||||
"colour": "配色",
|
||||
"standard": "标准",
|
||||
"custom": "定制",
|
||||
"useMaterialYou": "使用 Material You",
|
||||
"githubStarredRepos": "已星标的 GitHub 仓库",
|
||||
"uname": "用户名",
|
||||
"wrongArgNum": "提供了错误的参数数量",
|
||||
"xIsTrackOnly": "{} 仅追踪",
|
||||
"source": "源码",
|
||||
"app": "应用程序",
|
||||
"appsFromSourceAreTrackOnly": "来自此来源的应用为仅追踪",
|
||||
"youPickedTrackOnly": "你已选择仅追踪选项",
|
||||
"trackOnlyAppDescription": "该应用程序将被跟踪更新,但 Obtainium 无法下载或安装它",
|
||||
"wrongArgNum": "参数数量错误",
|
||||
"xIsTrackOnly": "“{}”为“仅追踪”模式",
|
||||
"source": "来源",
|
||||
"app": "应用",
|
||||
"appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
|
||||
"youPickedTrackOnly": "您选择了“仅追踪”。",
|
||||
"trackOnlyAppDescription": "该应用的更新会被追踪,但 Obtainium 无法下载或安装它。",
|
||||
"cancelled": "已取消",
|
||||
"appAlreadyAdded": "此应用程序已被添加",
|
||||
"alreadyUpToDateQuestion": "应用已是最新?",
|
||||
"appAlreadyAdded": "此应用已经添加",
|
||||
"alreadyUpToDateQuestion": "应用是否已经为最新版本?",
|
||||
"addApp": "添加应用",
|
||||
"appSourceURL": "应用来源 URL",
|
||||
"appSourceURL": "来源 URL",
|
||||
"error": "错误",
|
||||
"add": "添加",
|
||||
"searchSomeSourcesLabel": "搜索 (仅部分来源)",
|
||||
"searchSomeSourcesLabel": "搜索(仅支持部分来源)",
|
||||
"search": "搜索",
|
||||
"additionalOptsFor": "{} 的更多选项",
|
||||
"supportedSourcesBelow": "受支持的来源:",
|
||||
"supportedSources": "支持的来源",
|
||||
"trackOnlyInBrackets": "(仅追踪)",
|
||||
"searchableInBrackets": "(可被搜索)",
|
||||
"appsString": "应用程序",
|
||||
"noApps": "无应用程序",
|
||||
"noAppsForFilter": "没有应用可被过滤",
|
||||
"byX": "来自 {}",
|
||||
"percentProgress": "进度: {}%",
|
||||
"pleaseWait": "请等待...",
|
||||
"searchableInBrackets": "(可搜索)",
|
||||
"appsString": "应用列表",
|
||||
"noApps": "无应用",
|
||||
"noAppsForFilter": "没有符合条件的应用",
|
||||
"byX": "作者:{}",
|
||||
"percentProgress": "进度:{}%",
|
||||
"pleaseWait": "请稍候",
|
||||
"updateAvailable": "更新可用",
|
||||
"estimateInBracketsShort": "(预计.)",
|
||||
"notInstalled": "未安装",
|
||||
"estimateInBrackets": "(预计)",
|
||||
"pseudoVersion": "虚拟版本号",
|
||||
"selectAll": "全选",
|
||||
"deselectN": "取消选择 {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} 将被从 Obtainium 中删除,但仍安装在设备上。",
|
||||
"removeSelectedAppsQuestion": "删除已选择的应用程序吗?",
|
||||
"removeSelectedApps": "删除已选择的应用程序",
|
||||
"deselectX": "取消选择 {}",
|
||||
"xWillBeRemovedButRemainInstalled": "“{}”将从 Obtainium 中删除,但仍安装在您的设备中。",
|
||||
"removeSelectedAppsQuestion": "是否删除选中的应用?",
|
||||
"removeSelectedApps": "删除选中的应用",
|
||||
"updateX": "更新 {}",
|
||||
"installX": "安装 {}",
|
||||
"markXTrackOnlyAsUpdated": "将仅追踪编辑为已更新",
|
||||
"markXTrackOnlyAsUpdated": "将“{}”\n(仅追踪)\n标记为已更新",
|
||||
"changeX": "更改 {}",
|
||||
"installUpdateApps": "安装/更新应用程序",
|
||||
"installUpdateSelectedApps": "安装/更新已选择的应用程序",
|
||||
"onlyAppliesToInstalledAndOutdatedApps": "'只适用于已安装但已过时的应用程序",
|
||||
"markXSelectedAppsAsUpdated": "将已选择的 {} 个应用程序标记为已更新?",
|
||||
"no": "不要",
|
||||
"yes": "好的",
|
||||
"markSelectedAppsUpdated": "标记已选择的应用程序为已更新",
|
||||
"installUpdateApps": "安装/更新应用",
|
||||
"installUpdateSelectedApps": "安装/更新选中的应用",
|
||||
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
|
||||
"no": "否",
|
||||
"yes": "是",
|
||||
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
|
||||
"pinToTop": "置顶",
|
||||
"unpinFromTop": "取消置顶",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "为已选择的应用程序重置安装状态吗?",
|
||||
"installStatusOfXWillBeResetExplanation": "当 Obtainium 中显示的应用程序版本由于更新失败或其他问题而不正确时,这将有助于重置任何选定应用程序的安装状态。",
|
||||
"shareSelectedAppURLs": "分享已选择的应用程序 URL",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "是否重置选中应用的安装状态?",
|
||||
"installStatusOfXWillBeResetExplanation": "选中应用的安装状态将会被重置。\n\n当更新安装失败或其他问题导致 Obtainium 中的应用版本显示错误时,可以尝试通过此方法解决。",
|
||||
"customLinkMessage": "这些链接适用于安装了 Gettingium 的设备",
|
||||
"shareAppConfigLinks": "将应用程序配置共享为 HTML 链接",
|
||||
"shareSelectedAppURLs": "分享选中应用的 URL",
|
||||
"resetInstallStatus": "重置安装状态",
|
||||
"more": "更多",
|
||||
"removeOutdatedFilter": "删除过时的应用程序过滤器",
|
||||
"showOutdatedOnly": "只显示过时的应用程序",
|
||||
"filter": "过滤器",
|
||||
"filterActive": "过滤器 *",
|
||||
"filterApps": "过滤应用",
|
||||
"removeOutdatedFilter": "删除失效的应用筛选",
|
||||
"showOutdatedOnly": "只显示待更新应用",
|
||||
"filter": "筛选",
|
||||
"filterApps": "筛选应用",
|
||||
"appName": "应用名称",
|
||||
"author": "作者",
|
||||
"upToDateApps": "已更新的应用程序",
|
||||
"nonInstalledApps": "未安装的应用程序",
|
||||
"upToDateApps": "无需更新的应用",
|
||||
"nonInstalledApps": "未安装的应用",
|
||||
"importExport": "导入/导出",
|
||||
"settings": "设置",
|
||||
"exportedTo": "导出到 {}",
|
||||
"obtainiumExport": "Obtainium 导出",
|
||||
"invalidInput": "无效输入",
|
||||
"importedX": "已导出到 {}",
|
||||
"obtainiumImport": "Obtainium 导入",
|
||||
"exportedTo": "已导出至 {}",
|
||||
"obtainiumExport": "导出 Obtainium",
|
||||
"invalidInput": "无效的输入",
|
||||
"importedX": "已导入 {}",
|
||||
"obtainiumImport": "导入 Obtainium",
|
||||
"importFromURLList": "从 URL 列表导入",
|
||||
"searchQuery": "搜索查询",
|
||||
"appURLList": "应用 URL 列表",
|
||||
"line": "行",
|
||||
"searchX": "搜索 {}",
|
||||
"searchX": "搜索{}",
|
||||
"noResults": "无结果",
|
||||
"importX": "导入 {}",
|
||||
"importedAppsIdDisclaimer": "导入的应用程序可能显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。",
|
||||
"importX": "导入{}",
|
||||
"importedAppsIdDisclaimer": "导入的应用可能会错误地显示为“未安装”状态。\n请通过 Obtainium 重新安装这些应用来解决此问题。",
|
||||
"importErrors": "导入错误",
|
||||
"importedXOfYApps": "{} 中的 {} 个应用已导入",
|
||||
"followingURLsHadErrors": "以下 URL 有错误:",
|
||||
"okay": "好的",
|
||||
"selectURL": "已选择的 URL",
|
||||
"selectURLs": "已选择的 URL",
|
||||
"importedXOfYApps": "已导入 {} 中的 {} 个应用。",
|
||||
"followingURLsHadErrors": "下列 URL 存在错误:",
|
||||
"selectURL": "选择 URL",
|
||||
"selectURLs": "选择 URL",
|
||||
"pick": "选择",
|
||||
"theme": "主题",
|
||||
"dark": "深色",
|
||||
"light": "浅色",
|
||||
"followSystem": "跟随系统",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"appSortBy": "排列方式",
|
||||
"authorName": "作者 / 名字",
|
||||
"nameAuthor": "名字 / 作者",
|
||||
"asAdded": "添加顺序",
|
||||
"appSortOrder": "排列顺序",
|
||||
"useBlackTheme": "使用纯黑深色主题",
|
||||
"appSortBy": "排序依据",
|
||||
"authorName": "作者 / 应用名称",
|
||||
"nameAuthor": "应用名称 / 作者",
|
||||
"asAdded": "添加次序",
|
||||
"appSortOrder": "顺序",
|
||||
"ascending": "升序",
|
||||
"descending": "降序",
|
||||
"bgUpdateCheckInterval": "后台更新检查间隔",
|
||||
"neverManualOnly": "手动",
|
||||
"appearance": "外观",
|
||||
"showWebInAppView": "在应用来源页显示网页",
|
||||
"pinUpdates": "需更新的应用置顶",
|
||||
"updates": "检查间隔",
|
||||
"sourceSpecific": "Github 访问令牌",
|
||||
"showWebInAppView": "应用详情页显示来源网页",
|
||||
"pinUpdates": "将待更新应用置顶",
|
||||
"updates": "更新",
|
||||
"sourceSpecific": "来源",
|
||||
"appSource": "源代码",
|
||||
"noLogs": "无日志",
|
||||
"appLogs": "应用日志",
|
||||
"appLogs": "日志",
|
||||
"close": "关闭",
|
||||
"share": "分享",
|
||||
"appNotFound": "未找到应用",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-导出",
|
||||
"pickAnAPK": "选择一个安装包",
|
||||
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
||||
"deviceSupportsXArch": "你的设备支持 {} 架构",
|
||||
"deviceSupportsFollowingArchs": "你的设备支持以下架构:",
|
||||
"obtainiumExportHyphenatedLowercase": "获取出口",
|
||||
"pickAnAPK": "选择一个 APK 文件",
|
||||
"appHasMoreThanOnePackage": "“{}”有多个架构可用:",
|
||||
"deviceSupportsXArch": "您的设备支持 {} 架构。",
|
||||
"deviceSupportsFollowingArchs": "您的设备支持下列架构:",
|
||||
"warning": "警告",
|
||||
"sourceIsXButPackageFromYPrompt": "此应用来源是 '{}' 但更新包来自 '{}'。 继续吗?",
|
||||
"sourceIsXButPackageFromYPrompt": "此应用的来源是“{}”,但 APK 文件来自“{}”。是否继续?",
|
||||
"updatesAvailable": "更新可用",
|
||||
"updatesAvailableNotifDescription": "通知 Obtainium 所跟踪应用程序的更新",
|
||||
"noNewUpdates": "你的应用已是最新。",
|
||||
"xHasAnUpdate": "{} 有更新啦",
|
||||
"updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发送通知",
|
||||
"noNewUpdates": "全部应用已是最新。",
|
||||
"xHasAnUpdate": "“{}”可以更新了。",
|
||||
"appsUpdated": "应用已更新",
|
||||
"appsUpdatedNotifDescription": "通知在后台安装应用程序的更新",
|
||||
"xWasUpdatedToY": "{} 已更新到 {}.",
|
||||
"appsNotUpdated": "更新应用程序失败",
|
||||
"appsUpdatedNotifDescription": "当应用在后台安装更新时发送通知",
|
||||
"xWasUpdatedToY": "“{}”已更新至 {}。",
|
||||
"xWasNotUpdatedToY": "未能将 {} 更新为 {}。",
|
||||
"errorCheckingUpdates": "检查更新出错",
|
||||
"errorCheckingUpdatesNotifDescription": "当后台更新检查失败时显示的通知",
|
||||
"errorCheckingUpdatesNotifDescription": "当后台检查更新失败时显示的通知",
|
||||
"appsRemoved": "应用已删除",
|
||||
"appsRemovedNotifDescription": "通知由于加载应用程序时出错而被删除",
|
||||
"xWasRemovedDueToErrorY": "{} 已因以下错误被删除: {}",
|
||||
"appsRemovedNotifDescription": "当应用因加载出错而被删除时发送通知",
|
||||
"xWasRemovedDueToErrorY": "“{}”由于以下错误被删除:{}",
|
||||
"completeAppInstallation": "完成应用安装",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium 需要被启动以安装更新",
|
||||
"completeAppInstallationNotifDescription": "需要返回 Obtainium,以完成应用程序的安装。",
|
||||
"checkingForUpdates": "检查更新中",
|
||||
"checkingForUpdatesNotifDescription": "检查更新时出现的瞬时通知",
|
||||
"pleaseAllowInstallPerm": "请允许 Obtainium 安装应用程序",
|
||||
"obtainiumMustBeOpenToInstallApps": "必须启动 Obtainium 才能安装应用",
|
||||
"completeAppInstallationNotifDescription": "提示返回 Obtainium 以完成应用的安装",
|
||||
"checkingForUpdates": "正在检查更新",
|
||||
"checkingForUpdatesNotifDescription": "检查更新时短暂显示的通知",
|
||||
"pleaseAllowInstallPerm": "请授予 Obtainium 安装应用的权限",
|
||||
"trackOnly": "仅追踪",
|
||||
"errorWithHttpStatusCode": "错误 {}",
|
||||
"versionCorrectionDisabled": "禁用版本更正(插件似乎未起作用)",
|
||||
"errorWithHttpStatusCode": "{} 错误",
|
||||
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
|
||||
"unknown": "未知",
|
||||
"none": "无",
|
||||
"never": "从不",
|
||||
"latestVersionX": "最新: {}",
|
||||
"installedVersionX": "已安装: {}",
|
||||
"lastUpdateCheckX": "最后检查: {}",
|
||||
"never": "从未",
|
||||
"latestVersionX": "最新版本:{}",
|
||||
"installedVersionX": "当前版本:{}",
|
||||
"lastUpdateCheckX": "上次更新检查:{}",
|
||||
"remove": "删除",
|
||||
"yesMarkUpdated": "'是的,标为已更新",
|
||||
"fdroid": "F-Droid",
|
||||
"yesMarkUpdated": "是,标记为已更新",
|
||||
"fdroid": "F-Droid 官方存储库",
|
||||
"appIdOrName": "应用 ID 或名称",
|
||||
"appWithIdOrNameNotFound": "没有发现具有此 ID 或名称的应用",
|
||||
"reposHaveMultipleApps": "来源可能包含多个应用",
|
||||
"fdroidThirdPartyRepo": "F-Droid 第三方源",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"appId": "应用 ID",
|
||||
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
||||
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
||||
"fdroidThirdPartyRepo": "F-Droid 第三方存储库",
|
||||
"steamMobile": "蒸汽手机",
|
||||
"steamChat": "蒸汽聊天",
|
||||
"install": "安装",
|
||||
"markInstalled": "标记为已安装",
|
||||
"update": "更新",
|
||||
"markUpdated": "标记为已更新",
|
||||
"additionalOptions": "附加选项",
|
||||
"disableVersionDetection": "关闭版本检测",
|
||||
"noVersionDetectionExplanation": "此选项应只用于版本检测不能工作的应用程序",
|
||||
"downloadingX": "下载中 {}",
|
||||
"downloadNotifDescription": "通知用户下载进度",
|
||||
"noAPKFound": "未找到安装包",
|
||||
"noVersionDetection": "无版本检测",
|
||||
"categorize": "归档",
|
||||
"categories": "归档",
|
||||
"disableVersionDetection": "禁用版本检测",
|
||||
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
||||
"downloadingX": "正在下载“{}”",
|
||||
"downloadX": "下载 {}",
|
||||
"downloadedX": "下载 {}",
|
||||
"releaseAsset": "发行版",
|
||||
"downloadNotifDescription": "提示应用的下载进度",
|
||||
"noAPKFound": "未找到 APK 文件",
|
||||
"noVersionDetection": "禁用版本检测",
|
||||
"categorize": "归类",
|
||||
"categories": "类别",
|
||||
"category": "类别",
|
||||
"noCategory": "无类别",
|
||||
"noCategories": "无类别",
|
||||
"deleteCategoriesQuestion": "删除所有类别?",
|
||||
"categoryDeleteWarning": "所有被删除类别的应用程序将被设置为无类别",
|
||||
"deleteCategoriesQuestion": "是否删除选中的类别?",
|
||||
"categoryDeleteWarning": "被删除类别下的应用将恢复为未分类状态。",
|
||||
"addCategory": "添加类别",
|
||||
"label": "标签",
|
||||
"language": "语言",
|
||||
"copiedToClipboard": "Copied to Clipboard",
|
||||
"storagePermissionDenied": "存储权限已被拒绝",
|
||||
"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",
|
||||
"groupByCategory": "Group by Category",
|
||||
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
|
||||
"copiedToClipboard": "已复制至剪贴板",
|
||||
"storagePermissionDenied": "已拒绝授予存储权限",
|
||||
"selectedCategorizeWarning": "这将覆盖选中应用当前的类别设置。",
|
||||
"filterAPKsByRegEx": "筛选 APK 文件(正则表达式)",
|
||||
"removeFromObtainium": "从 Obtainium 中删除",
|
||||
"uninstallFromDevice": "从设备中卸载",
|
||||
"onlyWorksWithNonVersionDetectApps": "仅适用于禁用版本检测的应用。",
|
||||
"releaseDateAsVersion": "将发行日期作为版本号",
|
||||
"releaseDateAsVersionExplanation": "此选项应该仅用于无法进行版本检测但能够获取发行日期的应用。",
|
||||
"changes": "更新日志",
|
||||
"releaseDate": "发行日期",
|
||||
"importFromURLsInFile": "从文件中的 URL 导入(如 OPML)",
|
||||
"versionDetectionExplanation": "使发行版本号与应用定义的版本号一致",
|
||||
"versionDetection": "版本检测",
|
||||
"standardVersionDetection": "常规版本检测",
|
||||
"groupByCategory": "按类别分组显示",
|
||||
"autoApkFilterByArch": "如果可能,尝试按设备支持的 CPU 架构筛选 APK 文件",
|
||||
"overrideSource": "覆盖来源",
|
||||
"dontShowAgain": "不再显示",
|
||||
"dontShowTrackOnlyWarnings": "忽略“仅追踪”模式警告",
|
||||
"dontShowAPKOriginWarnings": "忽略 APK 文件来源警告",
|
||||
"moveNonInstalledAppsToBottom": "将未安装应用置底",
|
||||
"gitlabPATLabel": "GitLab 个人访问令牌",
|
||||
"about": "相关文档",
|
||||
"requiresCredentialsInSettings": "{}:此功能需要额外的凭据(在“设置”中添加)",
|
||||
"checkOnStart": "启动时进行一次检查",
|
||||
"tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
|
||||
"removeOnExternalUninstall": "自动删除列表中已卸载的应用",
|
||||
"pickHighestVersionCode": "自动选取内部版本号最高的 APK 文件",
|
||||
"checkUpdateOnDetailPage": "打开应用详情页时进行检查",
|
||||
"disablePageTransitions": "禁用页面过渡动画效果",
|
||||
"reversePageTransitions": "反转页面过渡动画效果",
|
||||
"minStarCount": "最小星标数",
|
||||
"addInfoBelow": "在下方添加此凭据。",
|
||||
"addInfoInSettings": "在“设置”中添加此凭据。",
|
||||
"githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。",
|
||||
"sortByLastLinkSegment": "仅根据链接的末尾部分进行筛选",
|
||||
"filterReleaseNotesByRegEx": "筛选发行说明(正则表达式)",
|
||||
"customLinkFilterRegex": "筛选自定义来源的 APK 文件链接\n(正则表达式,默认匹配模式为“.apk$”)",
|
||||
"appsPossiblyUpdated": "已尝试更新应用",
|
||||
"appsPossiblyUpdatedNotifDescription": "当应用已尝试在后台更新时发送通知",
|
||||
"xWasPossiblyUpdatedToY": "已尝试将“{}”更新至 {}。",
|
||||
"enableBackgroundUpdates": "启用后台更新",
|
||||
"backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。",
|
||||
"backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。",
|
||||
"verifyLatestTag": "验证“Latest”标签",
|
||||
"intermediateLinkRegex": "筛选中转链接(正则表达式)",
|
||||
"filterByLinkText": "根据链接文本进行筛选",
|
||||
"intermediateLinkNotFound": "未找到中转链接",
|
||||
"intermediateLink": "中转链接",
|
||||
"exemptFromBackgroundUpdates": "禁用后台更新(如果已经全局启用)",
|
||||
"bgUpdatesOnWiFiOnly": "未连接 Wi-Fi 时禁用后台更新",
|
||||
"autoSelectHighestVersionCode": "自动选择内部版本号最高的 APK 文件",
|
||||
"versionExtractionRegEx": "版本号匹配组规则(正则表达式)",
|
||||
"matchGroupToUse": "引用匹配组规则,将用于版本字符串提取",
|
||||
"highlightTouchTargets": "突出展示不明显的触摸区域",
|
||||
"pickExportDir": "选择导出文件夹",
|
||||
"autoExportOnChanges": "数据变更时自动导出",
|
||||
"includeSettings": "同时导出应用设置",
|
||||
"filterVersionsByRegEx": "筛选版本号(正则表达式)",
|
||||
"trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
|
||||
"dontSortReleasesList": "保持来自 API 的发行顺序",
|
||||
"reverseSort": "反转排序",
|
||||
"takeFirstLink": "选取第一个链接",
|
||||
"skipSort": "不进行排序",
|
||||
"debugMenu": "调试选项",
|
||||
"bgTaskStarted": "后台任务已启动 - 详见日志",
|
||||
"runBgCheckNow": "立即进行后台更新检查",
|
||||
"versionExtractWholePage": "将版本号提取规则应用于完整页面",
|
||||
"installing": "正在安装",
|
||||
"skipUpdateNotifications": "忽略更新通知",
|
||||
"updatesAvailableNotifChannel": "更新可用",
|
||||
"appsUpdatedNotifChannel": "应用已更新",
|
||||
"appsPossiblyUpdatedNotifChannel": "已尝试更新应用",
|
||||
"errorCheckingUpdatesNotifChannel": "检查更新出错",
|
||||
"appsRemovedNotifChannel": "应用已删除",
|
||||
"downloadingXNotifChannel": "正在下载“{}”",
|
||||
"completeAppInstallationNotifChannel": "完成应用安装",
|
||||
"checkingForUpdatesNotifChannel": "正在检查更新",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "只检查已安装和“仅追踪”的应用",
|
||||
"supportFixedAPKURL": "支持固定的 APK 文件链接",
|
||||
"selectX": "选择{}",
|
||||
"parallelDownloads": "启用并行下载",
|
||||
"useShizuku": "使用 Shizuku 或 Sui 安装",
|
||||
"shizukuBinderNotFound": "未发现兼容的 Shizuku 服务",
|
||||
"shizukuOld": "旧的 Shizuku 版本 (<11) - 更新它",
|
||||
"shizukuOldAndroidWithADB": "使用 ADB 在 Android < 8.1 上运行 Shizuku - 更新 Android 或使用 Sui 代替",
|
||||
"shizukuPretendToBeGooglePlay": "将 Google Play 设置为安装源(如果使用 Shizuku)",
|
||||
"useSystemFont": "使用系统字体",
|
||||
"useVersionCodeAsOSVersion": "使用内部版本号代替应用定义的版本号",
|
||||
"requestHeader": "请求标头",
|
||||
"useLatestAssetDateAsReleaseDate": "使用最近文件上传时间作为发行日期",
|
||||
"defaultPseudoVersioningMethod": "默认虚拟版本方案",
|
||||
"partialAPKHash": "APK 文件散列值片段",
|
||||
"APKLinkHash": "APK 文件链接散列值",
|
||||
"directAPKLink": "APK 文件直链",
|
||||
"pseudoVersionInUse": "正在使用虚拟版本号",
|
||||
"installed": "当前版本",
|
||||
"latest": "最新版本",
|
||||
"invertRegEx": "反转匹配",
|
||||
"note": "备注",
|
||||
"selfHostedNote": "可以通过“{}”下拉菜单来指向任意来源的自托管/自定义实例。",
|
||||
"badDownload": "无法解析 APK 文件(不兼容或文件不完整)",
|
||||
"beforeNewInstallsShareToAppVerifier": "与 AppVerifier 共享新应用程序(如有)",
|
||||
"appVerifierInstructionToast": "分享到 AppVerifier,准备就绪后返回此处。",
|
||||
"removeAppQuestion": {
|
||||
"one": "删除应用?",
|
||||
"other": "删除应用?"
|
||||
"one": "是否删除应用?",
|
||||
"other": "是否删除应用?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
||||
"one": "API 请求过于频繁(速率限制)- 在 {} 分钟后重试",
|
||||
"other": "API 请求过于频繁(速率限制)- 在 {} 分钟后重试"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "后台更新检查遇到了 {} 问题, 将在 {} 分钟后重试",
|
||||
"other": "后台更新检查遇到了 {} 问题, 将在 {} 分钟后重试"
|
||||
"one": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试",
|
||||
"other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "后台更新检查找到了 {} 个更新 - 将通知用户",
|
||||
"other": "后台更新检查找到了 {} 个更新 - 将通知用户"
|
||||
"one": "后台检查发现 {} 个应用更新 - 如有需要将发送通知",
|
||||
"other": "后台检查发现 {} 个应用更新 - 如有需要将发送通知"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} 个应用",
|
||||
@@ -261,15 +346,27 @@
|
||||
"other": "{} 天"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "清除了 {n} 个日志 (清除前 = {before}, 清除后 = {after})",
|
||||
"other": "清除了 {n} 个日志 (清除前 = {before}, 清除后 = {after})"
|
||||
"one": "清除了 {n} 个日志({before} 之前,{after} 之后)",
|
||||
"other": "清除了 {n} 个日志({before} 之前,{after} 之后)"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} 和 {} 更多应用已被更新",
|
||||
"other": "{} 和 {} 更多应用已被更新"
|
||||
"one": "“{}”和另外 1 个应用可以更新了。",
|
||||
"other": "“{}”和另外 {} 个应用可以更新了。"
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} 和 {} 更多应用已被安装",
|
||||
"other": "{} 和 {} 更多应用已被安装"
|
||||
"one": "{} 和另外 1 个应用已更新。",
|
||||
"other": "“{}”和另外 {} 个应用已更新。"
|
||||
},
|
||||
"xAndNMoreUpdatesFailed": {
|
||||
"one": "更新 {} 和另外 1 个应用程序失败。",
|
||||
"other": "未能更新 {} 和 {} 更多应用程序。"
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} 和另外 1 个应用已尝试更新。",
|
||||
"other": "“{}”和另外 {} 个应用已尝试更新。"
|
||||
},
|
||||
"apk": {
|
||||
"one": "{}APK",
|
||||
"other": "{}APK"
|
||||
}
|
||||
}
|
||||
|
||||
27
build.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Convenience script
|
||||
|
||||
CURR_DIR="$(pwd)"
|
||||
trap "cd "$CURR_DIR"" EXIT
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
|
||||
fi
|
||||
cd .flutter
|
||||
git fetch
|
||||
git checkout "$(flutter --version | head -2 | tail -1 | awk '{print $4}')" # Ensure included Flutter submodule version equals my environment
|
||||
cd ..
|
||||
rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any
|
||||
flutter build apk --flavor normal && flutter build apk --split-per-abi --flavor normal # Build (both split and combined APKs)
|
||||
for file in ./build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done
|
||||
flutter build apk --flavor fdroid -t lib/main_fdroid.dart && # Do the same for the F-Droid flavour
|
||||
flutter build apk --split-per-abi --flavor fdroid -t lib/main_fdroid.dart
|
||||
for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures
|
||||
rsync -r ./build/app/outputs/flutter-apk/ ~/Downloads/Obtainium-build/ # Dropoff in Downloads to allow for drag-drop into Flatpak Firefox
|
||||
cd ~/Downloads/Obtainium-build/ # Make zips just in case (for in-comment uploads)
|
||||
for apk in *.apk; do
|
||||
PREFIX="$(echo "$apk" | head -c -5)"
|
||||
zip "$PREFIX" "$PREFIX"*
|
||||
done
|
||||
mkdir -p zips
|
||||
mv *.zip zips/
|
||||
54
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
<p>Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available.</p>
|
||||
<p>Read the <a href="https://github.com/ImranR98/Obtainium/wiki">Wiki</a></p>
|
||||
<p>
|
||||
<b>Currently supported App sources:</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Open Source - General:</p>
|
||||
<ul>
|
||||
<li>GitHub</li>
|
||||
<li>GitLab</li>
|
||||
<li>Codeberg</li>
|
||||
<li>F-Droid</li>
|
||||
<li>Third Party F-Droid Repos</li>
|
||||
<li>IzzyOnDroid</li>
|
||||
<li>SourceForge</li>
|
||||
<li>SourceHut</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Other - General:</p>
|
||||
<ul>
|
||||
<li>APKPure</li>
|
||||
<li>Aptoide</li>
|
||||
<li>Uptodowng</li>
|
||||
<li>APKMirror (Track-Only)</li>
|
||||
<li>Huawei AppGallery</li>
|
||||
<li>Jenkins Jobs</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open Source - App-Specific:</p>
|
||||
<ul>
|
||||
<li>Mullvad</li>
|
||||
<li>Signal</li>
|
||||
<li>VLC</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Other - App-Specific:</p>
|
||||
<ul>
|
||||
<li>WhatsApp</li>
|
||||
<li>Telegram App</li>
|
||||
<li>Neutron Code</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><p>"HTML" (Fallback): Any other URL that returns an HTML page with links to APK files</p></li>
|
||||
</ul>
|
||||
<p>
|
||||
<b>Limitations:</b>
|
||||
</p>
|
||||
<p>
|
||||
For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
||||
</p>
|
||||
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Executable file
|
After Width: | Height: | Size: 66 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Get android app updates directly from the source
|
||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Obtainium
|
||||
54
fastlane/metadata/android/ru/full_description.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
<p>Obtainium позволяет вам устанавливать и обновлять приложения прямо с их объявлений о выпусках и получать уведомления о новых выпусках.</p>
|
||||
<p>Для деталей читайте <a href="https://github.com/ImranR98/Obtainium/wiki">Вики</a></p>
|
||||
<p>
|
||||
<b>Поддерживаемые источники приложений:</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Свободное ПО - Общие:</p>
|
||||
<ul>
|
||||
<li>GitHub</li>
|
||||
<li>GitLab</li>
|
||||
<li>Codeberg</li>
|
||||
<li>F-Droid</li>
|
||||
<li>Third Party F-Droid Repos</li>
|
||||
<li>IzzyOnDroid</li>
|
||||
<li>SourceForge</li>
|
||||
<li>SourceHut</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Другие - Общие:</p>
|
||||
<ul>
|
||||
<li>APKPure</li>
|
||||
<li>Aptoide</li>
|
||||
<li>Uptodowng</li>
|
||||
<li>APKMirror (Track-Only)</li>
|
||||
<li>Huawei AppGallery</li>
|
||||
<li>Jenkins Jobs</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Свободное ПО - Для отдельных приложений:</p>
|
||||
<ul>
|
||||
<li>Mullvad</li>
|
||||
<li>Signal</li>
|
||||
<li>VLC</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Другие - Для отдельных приложений:</p>
|
||||
<ul>
|
||||
<li>WhatsApp</li>
|
||||
<li>Telegram App</li>
|
||||
<li>Neutron Code</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><p>"HTML" (Подстраховка): Любой другой URL-адрес, который возвращает HTML-страницу со ссылками на APK-файлы.</p></li>
|
||||
</ul>
|
||||
<p>
|
||||
<b>Ограничения:</b>
|
||||
</p>
|
||||
<p>
|
||||
Для некоторых источников данные собираются с помощью веб-скрапинга и могут легко сломаться из-за изменений в дизайне веб-сайта. В таких случаях более надежные методы могут быть недоступны.
|
||||
</p>
|
||||
1
fastlane/metadata/android/ru/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Получайте обновления приложений прямо из источника
|
||||
124
lib/app_sources/apkcombo.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class APKCombo extends AppSource {
|
||||
APKCombo() {
|
||||
hosts = ['apkcombo.com'];
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+',
|
||||
caseSensitive: false);
|
||||
var match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return Uri.parse(standardUrl).pathSegments.last;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
{bool forAPKDownload = false}) async {
|
||||
return {
|
||||
"User-Agent": "curl/8.0.1",
|
||||
"Accept": "*/*",
|
||||
"Connection": "keep-alive",
|
||||
"Host": hosts[0]
|
||||
};
|
||||
}
|
||||
|
||||
Future<List<MapEntry<String, String>>> getApkUrls(
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||
var res = await sourceRequest('$standardUrl/download/apk', {});
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
return html
|
||||
.querySelectorAll('#variants-tab > div > ul > li')
|
||||
.map((e) {
|
||||
String? arch = e
|
||||
.querySelector('code')
|
||||
?.text
|
||||
.trim()
|
||||
.replaceAll(',', '')
|
||||
.replaceAll(':', '-')
|
||||
.replaceAll(' ', '-');
|
||||
return e.querySelectorAll('a').map((e) {
|
||||
String? url = e.attributes['href'];
|
||||
if (url != null &&
|
||||
!Uri.parse(url).path.toLowerCase().endsWith('.apk')) {
|
||||
url = null;
|
||||
}
|
||||
String verCode =
|
||||
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
|
||||
return MapEntry<String, String>(
|
||||
arch != null ? '$arch-$verCode.apk' : '', url ?? '');
|
||||
}).toList();
|
||||
})
|
||||
.reduce((value, element) => [...value, ...element])
|
||||
.where((element) => element.value.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||
Map<String, dynamic> additionalSettings) async {
|
||||
var freshURLs = await getApkUrls(standardUrl, additionalSettings);
|
||||
var path2Match = Uri.parse(apkUrl).path;
|
||||
for (var url in freshURLs) {
|
||||
if (Uri.parse(url.value).path == path2Match) {
|
||||
return url.value;
|
||||
}
|
||||
}
|
||||
throw NoAPKError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String appId = (await tryInferringAppId(standardUrl))!;
|
||||
var preres = await sourceRequest(standardUrl, additionalSettings);
|
||||
if (preres.statusCode != 200) {
|
||||
throw getObtainiumHttpError(preres);
|
||||
}
|
||||
var res = parse(preres.body);
|
||||
String? version = res.querySelector('div.version')?.text.trim();
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
String appName = res.querySelector('div.app_name')?.text.trim() ?? appId;
|
||||
String author = res.querySelector('div.author')?.text.trim() ?? appName;
|
||||
List<String> infoArray = res
|
||||
.querySelectorAll('div.information-table > .item > div.value')
|
||||
.map((e) => e.text.trim())
|
||||
.toList();
|
||||
DateTime? releaseDate;
|
||||
if (infoArray.length >= 2) {
|
||||
try {
|
||||
releaseDate = DateFormat('MMMM d, yyyy').parse(infoArray[1]);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return APKDetails(
|
||||
version,
|
||||
await getApkUrls(standardUrl, additionalSettings),
|
||||
AppNames(author, appName),
|
||||
releaseDate: releaseDate);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class APKMirror extends AppSource {
|
||||
APKMirror() {
|
||||
host = 'apkmirror.com';
|
||||
hosts = ['apkmirror.com'];
|
||||
enforceTrackOnly = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
@@ -31,13 +32,15 @@ class APKMirror extends AppSource {
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -57,7 +60,7 @@ class APKMirror extends AppSource {
|
||||
true
|
||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||
: null;
|
||||
Response res = await get(Uri.parse('$standardUrl/feed'));
|
||||
Response res = await sourceRequest('$standardUrl/feed', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var items = parse(res.body).querySelectorAll('item');
|
||||
dynamic targetRelease;
|
||||
@@ -83,7 +86,7 @@ class APKMirror extends AppSource {
|
||||
dateString != null ? HttpDate.parse('$dateString GMT') : null;
|
||||
String? version = titleString
|
||||
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
||||
RegExp(' by ').firstMatch(titleString)?.start ?? 0)
|
||||
RegExp(' by ').allMatches(titleString).last.start)
|
||||
.trim();
|
||||
if (version == null || version.isEmpty) {
|
||||
version = titleString;
|
||||
|
||||
97
lib/app_sources/apkpure.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
parseDateTimeMMMddCommayyyy(String? dateString) {
|
||||
DateTime? releaseDate;
|
||||
try {
|
||||
releaseDate = dateString != null
|
||||
? DateFormat('MMM dd, yyyy').parse(dateString)
|
||||
: null;
|
||||
releaseDate = dateString != null && releaseDate == null
|
||||
? DateFormat('MMMM dd, yyyy').parse(dateString)
|
||||
: releaseDate;
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
return releaseDate;
|
||||
}
|
||||
|
||||
class APKPure extends AppSource {
|
||||
APKPure() {
|
||||
hosts = ['apkpure.net', 'apkpure.com'];
|
||||
allowSubDomains = true;
|
||||
naiveStandardVersionDetection = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegExB = RegExp(
|
||||
'^https?://m.${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||
if (match != null) {
|
||||
url = 'https://${getSourceRegex(hosts)}${Uri.parse(url).path}';
|
||||
}
|
||||
RegExp standardUrlRegExA = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+(/+[^/]+)?',
|
||||
caseSensitive: false);
|
||||
match = standardUrlRegExA.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return Uri.parse(standardUrl).pathSegments.last;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String appId = (await tryInferringAppId(standardUrl))!;
|
||||
String host = Uri.parse(standardUrl).host;
|
||||
var res = await sourceRequest('$standardUrl/download', additionalSettings);
|
||||
var resChangelog = await sourceRequest(standardUrl, additionalSettings);
|
||||
if (res.statusCode == 200 && resChangelog.statusCode == 200) {
|
||||
var html = parse(res.body);
|
||||
var htmlChangelog = parse(resChangelog.body);
|
||||
String? version = html.querySelector('span.info-sdk span')?.text.trim();
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
String? dateString =
|
||||
html.querySelector('span.info-other span.date')?.text.trim();
|
||||
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
|
||||
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||
List<MapEntry<String, String>> apkUrls = [
|
||||
MapEntry('$appId.apk',
|
||||
'https://d.${hosts.contains(host) ? 'cdnpure.com' : host}/b/$type/$appId?version=latest')
|
||||
];
|
||||
String author = html
|
||||
.querySelector('span.info-sdk')
|
||||
?.text
|
||||
.trim()
|
||||
.substring(version.length + 4) ??
|
||||
Uri.parse(standardUrl).pathSegments.reversed.last;
|
||||
String appName =
|
||||
html.querySelector('h1.info-title')?.text.trim() ?? appId;
|
||||
String? changeLog = htmlChangelog
|
||||
.querySelector("div.whats-new-info p:not(.date)")
|
||||
?.innerHtml
|
||||
.trim()
|
||||
.replaceAll("<br>", " \n");
|
||||
return APKDetails(version, apkUrls, AppNames(author, appName),
|
||||
releaseDate: releaseDate, changeLog: changeLog);
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
lib/app_sources/aptoide.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Aptoide extends AppSource {
|
||||
Aptoide() {
|
||||
hosts = ['aptoide.com'];
|
||||
name = 'Aptoide';
|
||||
allowSubDomains = true;
|
||||
naiveStandardVersionDetection = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return (await getAppDetailsJSON(
|
||||
standardUrl, additionalSettings))['package'];
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getAppDetailsJSON(
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body);
|
||||
String? id;
|
||||
if (idMatch != null) {
|
||||
id = res.body.substring(idMatch.start + 12, idMatch.end);
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
var res2 = await sourceRequest(
|
||||
'https://ws2.aptoide.com/api/7/getApp/app_id/$id', additionalSettings);
|
||||
if (res2.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
return jsonDecode(res2.body)?['nodes']?['meta']?['data'];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
var appDetails = await getAppDetailsJSON(standardUrl, additionalSettings);
|
||||
String appName = appDetails['name'] ?? tr('app');
|
||||
String author = appDetails['developer']?['name'] ?? name;
|
||||
String? dateStr = appDetails['updated'];
|
||||
String? version = appDetails['file']?['vername'];
|
||||
String? apkUrl = appDetails['file']?['path'];
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
if (apkUrl == null) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
DateTime? relDate;
|
||||
if (dateStr != null) {
|
||||
relDate = DateTime.parse(dateStr);
|
||||
}
|
||||
|
||||
return APKDetails(
|
||||
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
||||
releaseDate: relDate);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,29 @@
|
||||
import 'dart:convert';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Codeberg extends AppSource {
|
||||
GitHub gh = GitHub();
|
||||
Codeberg() {
|
||||
host = 'codeberg.org';
|
||||
hosts = ['codeberg.org'];
|
||||
|
||||
additionalSourceSpecificSettingFormItems = [];
|
||||
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
GeneratedFormSwitch('includePrereleases',
|
||||
label: tr('includePrereleases'), defaultValue: false)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
||||
label: tr('filterReleaseTitlesByRegEx'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
]
|
||||
];
|
||||
additionalSourceAppSpecificSettingFormItems =
|
||||
gh.additionalSourceAppSpecificSettingFormItems;
|
||||
|
||||
canSearch = true;
|
||||
searchQuerySettingFormItems = gh.searchQuerySettingFormItems;
|
||||
}
|
||||
|
||||
var gh = GitHub();
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -57,10 +35,10 @@ class Codeberg extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
return gh.getLatestAPKDetailsCommon(
|
||||
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100',
|
||||
standardUrl,
|
||||
additionalSettings);
|
||||
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||
(bool useTagUrl) async {
|
||||
return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||
}, null);
|
||||
}
|
||||
|
||||
AppNames getAppNames(String standardUrl) {
|
||||
@@ -70,10 +48,12 @@ class Codeberg extends AppSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> search(String query) async {
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
return gh.searchCommon(
|
||||
query,
|
||||
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
||||
'data');
|
||||
'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
||||
'data',
|
||||
querySettings: querySettings);
|
||||
}
|
||||
}
|
||||
|
||||
44
lib/app_sources/directAPKLink.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class DirectAPKLink extends AppSource {
|
||||
HTML html = HTML();
|
||||
|
||||
DirectAPKLink() {
|
||||
neverAutoSelect = true;
|
||||
name = tr('directAPKLink');
|
||||
additionalSourceAppSpecificSettingFormItems = html
|
||||
.additionalSourceAppSpecificSettingFormItems
|
||||
.where((element) => element
|
||||
.where((element) => element.key == 'requestHeader')
|
||||
.isNotEmpty)
|
||||
.toList();
|
||||
excludeCommonSettingKeys = [
|
||||
'versionExtractionRegEx',
|
||||
'matchGroupToUse',
|
||||
'versionDetection',
|
||||
'useVersionCodeAsOSVersion',
|
||||
'apkFilterRegEx',
|
||||
'autoApkFilterByArch'
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
var additionalSettingsNew =
|
||||
getDefaultValuesFromFormItems(html.combinedAppSpecificSettingFormItems);
|
||||
for (var s in additionalSettings.keys) {
|
||||
if (additionalSettingsNew.containsKey(s)) {
|
||||
additionalSettingsNew[s] = additionalSettings[s];
|
||||
}
|
||||
}
|
||||
additionalSettingsNew['defaultPseudoVersioningMethod'] = 'partialAPKHash';
|
||||
additionalSettingsNew['directAPKLink'] = true;
|
||||
additionalSettings['versionDetection'] = false;
|
||||
return html.getLatestAPKDetails(standardUrl, additionalSettingsNew);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,250 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/app_sources/gitlab.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class FDroid extends AppSource {
|
||||
FDroid() {
|
||||
host = 'f-droid.org';
|
||||
hosts = ['f-droid.org'];
|
||||
name = tr('fdroid');
|
||||
naiveStandardVersionDetection = true;
|
||||
canSearch = true;
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
GeneratedFormTextField('filterVersionsByRegEx',
|
||||
label: tr('filterVersionsByRegEx'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('trySelectingSuggestedVersionCode',
|
||||
label: tr('trySelectingSuggestedVersionCode'))
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('autoSelectHighestVersionCode',
|
||||
label: tr('autoSelectHighestVersionCode'))
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegExB =
|
||||
RegExp('^https?://(cloudflare\\.)?$host/+[^/]+/+packages/+[^/]+');
|
||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegExB = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||
if (match != null) {
|
||||
url =
|
||||
'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
|
||||
'https://${Uri.parse(match.group(0)!).host}/packages/${Uri.parse(url).pathSegments.last}';
|
||||
}
|
||||
RegExp standardUrlRegExA =
|
||||
RegExp('^https?://(cloudflare\\.)?$host/+packages/+[^/]+');
|
||||
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||
RegExp standardUrlRegExA = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+',
|
||||
caseSensitive: false);
|
||||
match = standardUrlRegExA.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String? tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return Uri.parse(standardUrl).pathSegments.last;
|
||||
}
|
||||
|
||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
Response res, String apkUrlPrefix, String standardUrl) {
|
||||
if (res.statusCode == 200) {
|
||||
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
|
||||
if (releases.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String? latestVersion = releases[0]['versionName'];
|
||||
if (latestVersion == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
List<String> apkUrls = releases
|
||||
.where((element) => element['versionName'] == latestVersion)
|
||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||
.toList();
|
||||
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
||||
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String? appId = tryInferringAppId(standardUrl);
|
||||
String? appId = await tryInferringAppId(standardUrl);
|
||||
String host = Uri.parse(standardUrl).host;
|
||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await get(Uri.parse('https://$host/api/v1/packages/$appId')),
|
||||
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await sourceRequest(
|
||||
'https://$host/api/v1/packages/$appId', additionalSettings),
|
||||
'https://$host/repo/$appId',
|
||||
standardUrl);
|
||||
standardUrl,
|
||||
name,
|
||||
additionalSettings: additionalSettings);
|
||||
if (!hostChanged) {
|
||||
try {
|
||||
var res = await sourceRequest(
|
||||
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
|
||||
additionalSettings);
|
||||
var lines = res.body.split('\n');
|
||||
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
|
||||
if (authorLines.isNotEmpty) {
|
||||
details.names.author =
|
||||
authorLines.first.split(': ').sublist(1).join(': ');
|
||||
}
|
||||
var changelogUrls = lines
|
||||
.where((l) => l.startsWith('Changelog: '))
|
||||
.map((e) => e.split(' ').sublist(1).join(' '));
|
||||
if (changelogUrls.isNotEmpty) {
|
||||
details.changeLog = changelogUrls.first;
|
||||
bool isGitHub = false;
|
||||
bool isGitLab = false;
|
||||
try {
|
||||
GitHub().sourceSpecificStandardizeURL(details.changeLog!);
|
||||
isGitHub = true;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
try {
|
||||
GitLab().sourceSpecificStandardizeURL(details.changeLog!);
|
||||
isGitLab = true;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
if ((isGitHub || isGitLab) &&
|
||||
(details.changeLog?.indexOf('/blob/') ?? -1) >= 0) {
|
||||
details.changeLog = (await sourceRequest(
|
||||
details.changeLog!.replaceFirst('/blob/', '/raw/'),
|
||||
additionalSettings))
|
||||
.body;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail silently
|
||||
}
|
||||
if ((details.changeLog?.length ?? 0) > 2048) {
|
||||
details.changeLog = '${details.changeLog!.substring(0, 2048)}...';
|
||||
}
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
Response res = await sourceRequest(
|
||||
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
|
||||
if (res.statusCode == 200) {
|
||||
Map<String, List<String>> urlsWithDescriptions = {};
|
||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||
String? url = e.attributes['href'];
|
||||
if (url != null) {
|
||||
try {
|
||||
standardizeUrl(url);
|
||||
} catch (e) {
|
||||
url = null;
|
||||
}
|
||||
}
|
||||
if (url != null) {
|
||||
urlsWithDescriptions[url] = [
|
||||
e.querySelector('.package-name')?.text.trim() ?? '',
|
||||
e.querySelector('.package-summary')?.text.trim() ??
|
||||
tr('noDescription')
|
||||
];
|
||||
}
|
||||
});
|
||||
return urlsWithDescriptions;
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
Response res, String apkUrlPrefix, String standardUrl, String sourceName,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||
var autoSelectHighestVersionCode =
|
||||
additionalSettings['autoSelectHighestVersionCode'] == true;
|
||||
var trySelectingSuggestedVersionCode =
|
||||
additionalSettings['trySelectingSuggestedVersionCode'] == true;
|
||||
var filterVersionsByRegEx =
|
||||
(additionalSettings['filterVersionsByRegEx'] as String?)?.isNotEmpty ==
|
||||
true
|
||||
? additionalSettings['filterVersionsByRegEx']
|
||||
: null;
|
||||
var apkFilterRegEx =
|
||||
(additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true
|
||||
? additionalSettings['apkFilterRegEx']
|
||||
: null;
|
||||
if (res.statusCode == 200) {
|
||||
var response = jsonDecode(res.body);
|
||||
List<dynamic> releases = response['packages'] ?? [];
|
||||
if (apkFilterRegEx != null) {
|
||||
releases = releases.where((rel) {
|
||||
String apk = '${apkUrlPrefix}_${rel['versionCode']}.apk';
|
||||
return filterApks([MapEntry(apk, apk)], apkFilterRegEx, false)
|
||||
.isNotEmpty;
|
||||
}).toList();
|
||||
}
|
||||
if (releases.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String? version;
|
||||
Iterable<dynamic> releaseChoices = [];
|
||||
// Grab the versionCode suggested if the user chose to do that
|
||||
// Only do so at this stage if the user has no release filter
|
||||
if (trySelectingSuggestedVersionCode &&
|
||||
response['suggestedVersionCode'] != null &&
|
||||
filterVersionsByRegEx == null) {
|
||||
var suggestedReleases = releases.where((element) =>
|
||||
element['versionCode'] == response['suggestedVersionCode']);
|
||||
if (suggestedReleases.isNotEmpty) {
|
||||
releaseChoices = suggestedReleases;
|
||||
version = suggestedReleases.first['versionName'];
|
||||
}
|
||||
}
|
||||
// Apply the release filter if any
|
||||
if (filterVersionsByRegEx?.isNotEmpty == true) {
|
||||
version = null;
|
||||
releaseChoices = [];
|
||||
for (var i = 0; i < releases.length; i++) {
|
||||
if (RegExp(filterVersionsByRegEx!)
|
||||
.hasMatch(releases[i]['versionName'])) {
|
||||
version = releases[i]['versionName'];
|
||||
}
|
||||
}
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
}
|
||||
// Default to the highest version
|
||||
version ??= releases[0]['versionName'];
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
// If a suggested release was not already picked, pick all those with the selected version
|
||||
if (releaseChoices.isEmpty) {
|
||||
releaseChoices =
|
||||
releases.where((element) => element['versionName'] == version);
|
||||
}
|
||||
// For the remaining releases, use the toggles to auto-select one if possible
|
||||
if (releaseChoices.length > 1) {
|
||||
if (autoSelectHighestVersionCode) {
|
||||
releaseChoices = [releaseChoices.first];
|
||||
} else if (trySelectingSuggestedVersionCode &&
|
||||
response['suggestedVersionCode'] != null) {
|
||||
var suggestedReleases = releaseChoices.where((element) =>
|
||||
element['versionCode'] == response['suggestedVersionCode']);
|
||||
if (suggestedReleases.isNotEmpty) {
|
||||
releaseChoices = suggestedReleases;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (releaseChoices.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
List<String> apkUrls = releaseChoices
|
||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||
.toList();
|
||||
return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
|
||||
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
class FDroidRepo extends AppSource {
|
||||
FDroidRepo() {
|
||||
name = tr('fdroidThirdPartyRepo');
|
||||
canSearch = true;
|
||||
excludeFromMassSearch = true;
|
||||
neverAutoSelect = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
@@ -15,19 +19,112 @@ class FDroidRepo extends AppSource {
|
||||
label: tr('appIdOrName'),
|
||||
hint: tr('reposHaveMultipleApps'),
|
||||
required: true)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('pickHighestVersionCode',
|
||||
label: tr('pickHighestVersionCode'), defaultValue: false)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegExp =
|
||||
RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)');
|
||||
RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
String removeQueryParamsFromUrl(String url, {List<String> keep = const []}) {
|
||||
var uri = Uri.parse(url);
|
||||
Map<String, dynamic> resultParams = {};
|
||||
uri.queryParameters.forEach((key, value) {
|
||||
if (keep.contains(key)) {
|
||||
resultParams[key] = value;
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
});
|
||||
url = uri.replace(queryParameters: resultParams).toString();
|
||||
if (url.endsWith('?')) {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
var standardUri = Uri.parse(url);
|
||||
var pathSegments = standardUri.pathSegments;
|
||||
if (pathSegments.isNotEmpty && pathSegments.last == 'index.xml') {
|
||||
pathSegments.removeLast();
|
||||
standardUri = standardUri.replace(path: pathSegments.join('/'));
|
||||
}
|
||||
return removeQueryParamsFromUrl(standardUri.toString(), keep: ['appId']);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
String? url = querySettings['url'];
|
||||
if (url == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
url = removeQueryParamsFromUrl(standardizeUrl(url));
|
||||
var res = await sourceRequestWithURLVariants(url, {});
|
||||
if (res.statusCode == 200) {
|
||||
var body = parse(res.body);
|
||||
Map<String, List<String>> results = {};
|
||||
body.querySelectorAll('application').toList().forEach((app) {
|
||||
String appId = app.attributes['id']!;
|
||||
String appName = app.querySelector('name')?.innerHtml ?? appId;
|
||||
String appDesc = app.querySelector('desc')?.innerHtml ?? '';
|
||||
if (query.isEmpty ||
|
||||
appId.contains(query) ||
|
||||
appName.contains(query) ||
|
||||
appDesc.contains(query)) {
|
||||
results[
|
||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
|
||||
appName,
|
||||
appDesc
|
||||
];
|
||||
}
|
||||
});
|
||||
return results;
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
App endOfGetAppChanges(App app) {
|
||||
var uri = Uri.parse(app.url);
|
||||
String? appId;
|
||||
if (!isTempId(app)) {
|
||||
appId = app.id;
|
||||
} else if (uri.queryParameters['appId'] != null) {
|
||||
appId = uri.queryParameters['appId'];
|
||||
}
|
||||
if (appId != null) {
|
||||
app.url = uri
|
||||
.replace(
|
||||
queryParameters: Map.fromEntries(
|
||||
[...uri.queryParameters.entries, MapEntry('appId', appId)]))
|
||||
.toString();
|
||||
app.additionalSettings['appIdOrName'] = appId;
|
||||
app.id = appId;
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
Future<Response> sourceRequestWithURLVariants(
|
||||
String url,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
var res = await sourceRequest(
|
||||
'$url${url.endsWith('/index.xml') ? '' : '/index.xml'}',
|
||||
additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
var base = url.endsWith('/index.xml')
|
||||
? url.split('/').reversed.toList().sublist(1).reversed.join('/')
|
||||
: url;
|
||||
res = await sourceRequest('$base/repo/index.xml', additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
res = await sourceRequest(
|
||||
'$base/fdroid/repo/index.xml', additionalSettings);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -36,10 +133,17 @@ class FDroidRepo extends AppSource {
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String? appIdOrName = additionalSettings['appIdOrName'];
|
||||
var standardUri = Uri.parse(standardUrl);
|
||||
if (standardUri.queryParameters['appId'] != null) {
|
||||
appIdOrName = standardUri.queryParameters['appId'];
|
||||
}
|
||||
standardUrl = removeQueryParamsFromUrl(standardUrl);
|
||||
bool pickHighestVersionCode = additionalSettings['pickHighestVersionCode'];
|
||||
if (appIdOrName == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
var res = await get(Uri.parse('$standardUrl/index.xml'));
|
||||
var res =
|
||||
await sourceRequestWithURLVariants(standardUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var body = parse(res.body);
|
||||
var foundApps = body.querySelectorAll('application').where((element) {
|
||||
@@ -48,7 +152,7 @@ class FDroidRepo extends AppSource {
|
||||
if (foundApps.isEmpty) {
|
||||
foundApps = body.querySelectorAll('application').where((element) {
|
||||
return element.querySelector('name')?.innerHtml.toLowerCase() ==
|
||||
appIdOrName.toLowerCase();
|
||||
appIdOrName!.toLowerCase();
|
||||
}).toList();
|
||||
}
|
||||
if (foundApps.isEmpty) {
|
||||
@@ -57,7 +161,7 @@ class FDroidRepo extends AppSource {
|
||||
.querySelector('name')
|
||||
?.innerHtml
|
||||
.toLowerCase()
|
||||
.contains(appIdOrName.toLowerCase()) ??
|
||||
.contains(appIdOrName!.toLowerCase()) ??
|
||||
false;
|
||||
}).toList();
|
||||
}
|
||||
@@ -65,8 +169,9 @@ class FDroidRepo extends AppSource {
|
||||
throw ObtainiumError(tr('appWithIdOrNameNotFound'));
|
||||
}
|
||||
var authorName = body.querySelector('repo')?.attributes['name'] ?? name;
|
||||
var appName =
|
||||
foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName;
|
||||
String appId = foundApps[0].attributes['id']!;
|
||||
foundApps[0].querySelector('name')?.innerHtml ?? appId;
|
||||
var appName = foundApps[0].querySelector('name')?.innerHtml ?? appId;
|
||||
var releases = foundApps[0].querySelectorAll('package');
|
||||
String? latestVersion = releases[0].querySelector('version')?.innerHtml;
|
||||
String? added = releases[0].querySelector('added')?.innerHtml;
|
||||
@@ -74,11 +179,21 @@ class FDroidRepo extends AppSource {
|
||||
if (latestVersion == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
List<String> apkUrls = releases
|
||||
var latestVersionReleases = releases
|
||||
.where((element) =>
|
||||
element.querySelector('version')?.innerHtml == latestVersion &&
|
||||
element.querySelector('apkname') != null)
|
||||
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
|
||||
.toList();
|
||||
if (latestVersionReleases.length > 1 && pickHighestVersionCode) {
|
||||
latestVersionReleases.sort((e1, e2) {
|
||||
return int.parse(e2.querySelector('versioncode')!.innerHtml)
|
||||
.compareTo(int.parse(e1.querySelector('versioncode')!.innerHtml));
|
||||
});
|
||||
latestVersionReleases = [latestVersionReleases[0]];
|
||||
}
|
||||
List<String> apkUrls = latestVersionReleases
|
||||
.map((e) =>
|
||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
|
||||
.toList();
|
||||
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
||||
AppNames(authorName, appName),
|
||||
|
||||
@@ -1,40 +1,31 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/logs_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class GitHub extends AppSource {
|
||||
GitHub() {
|
||||
host = 'github.com';
|
||||
hosts = ['github.com'];
|
||||
appIdInferIsOptional = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
|
||||
additionalSourceSpecificSettingFormItems = [
|
||||
sourceConfigSettingFormItems = [
|
||||
GeneratedFormTextField('github-creds',
|
||||
label: tr('githubPATLabel'),
|
||||
password: true,
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
if (value != null && value.trim().isNotEmpty) {
|
||||
if (value
|
||||
.split(':')
|
||||
.where((element) => element.trim().isNotEmpty)
|
||||
.length !=
|
||||
2) {
|
||||
return tr('githubPATHint');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
hint: tr('githubPATFormat'),
|
||||
belowWidgets: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
height: 4,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
@@ -43,10 +34,13 @@ class GitHub extends AppSource {
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text(
|
||||
tr('githubPATLinkText'),
|
||||
tr('about'),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline, fontSize: 12),
|
||||
))
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
])
|
||||
];
|
||||
|
||||
@@ -68,29 +62,160 @@ class GitHub extends AppSource {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('filterReleaseNotesByRegEx',
|
||||
label: tr('filterReleaseNotesByRegEx'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
],
|
||||
[GeneratedFormSwitch('verifyLatestTag', label: tr('verifyLatestTag'))],
|
||||
[
|
||||
GeneratedFormSwitch('dontSortReleasesList',
|
||||
label: tr('dontSortReleasesList'))
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('useLatestAssetDateAsReleaseDate',
|
||||
label: tr('useLatestAssetDateAsReleaseDate'), defaultValue: false)
|
||||
]
|
||||
];
|
||||
|
||||
canSearch = true;
|
||||
searchQuerySettingFormItems = [
|
||||
GeneratedFormTextField('minStarCount',
|
||||
label: tr('minStarCount'),
|
||||
defaultValue: '0',
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
try {
|
||||
int.parse(value ?? '0');
|
||||
} catch (e) {
|
||||
return tr('invalidInput');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
const possibleBuildGradleLocations = [
|
||||
'/app/build.gradle',
|
||||
'android/app/build.gradle',
|
||||
'src/app/build.gradle'
|
||||
];
|
||||
for (var path in possibleBuildGradleLocations) {
|
||||
try {
|
||||
var res = await sourceRequest(
|
||||
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
|
||||
additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
try {
|
||||
var body = jsonDecode(res.body);
|
||||
var trimmedLines = utf8
|
||||
.decode(base64
|
||||
.decode(body['content'].toString().split('\n').join('')))
|
||||
.split('\n')
|
||||
.map((e) => e.trim());
|
||||
var appIds = trimmedLines.where((l) =>
|
||||
l.startsWith('applicationId "') ||
|
||||
l.startsWith('applicationId \''));
|
||||
appIds = appIds.map((appId) => appId
|
||||
.split(appId.startsWith('applicationId "') ? '"' : '\'')[1]);
|
||||
appIds = appIds.map((appId) {
|
||||
if (appId.startsWith('\${') && appId.endsWith('}')) {
|
||||
appId = trimmedLines
|
||||
.where((l) => l.startsWith(
|
||||
'def ${appId.substring(2, appId.length - 1)}'))
|
||||
.first;
|
||||
appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
|
||||
}
|
||||
return appId;
|
||||
}).where((appId) => appId.isNotEmpty);
|
||||
if (appIds.length == 1) {
|
||||
return appIds.first;
|
||||
}
|
||||
} catch (err) {
|
||||
LogsProvider().add(
|
||||
'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore - ID will be extracted from the APK
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
Future<String> getCredentialPrefixIfAny() async {
|
||||
@override
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
{bool forAPKDownload = false}) async {
|
||||
var token = await getTokenIfAny(additionalSettings);
|
||||
var headers = <String, String>{};
|
||||
if (token != null) {
|
||||
headers[HttpHeaders.authorizationHeader] = 'Token $token';
|
||||
}
|
||||
if (forAPKDownload == true) {
|
||||
headers[HttpHeaders.acceptHeader] = 'application/octet-stream';
|
||||
}
|
||||
if (headers.isNotEmpty) {
|
||||
return headers;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
|
||||
SettingsProvider settingsProvider = SettingsProvider();
|
||||
await settingsProvider.initializeSettings();
|
||||
String? creds = settingsProvider
|
||||
.getSettingString(additionalSourceSpecificSettingFormItems[0].key);
|
||||
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
||||
var sourceConfig =
|
||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
||||
String? creds = sourceConfig['github-creds'];
|
||||
if (creds != null) {
|
||||
var userNameEndIndex = creds.indexOf(':');
|
||||
if (userNameEndIndex > 0) {
|
||||
creds = creds.substring(
|
||||
userNameEndIndex + 1); // For old username-included token inputs
|
||||
}
|
||||
return creds;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getSourceNote() async {
|
||||
if (!hostChanged && (await getTokenIfAny({})) == null) {
|
||||
return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
|
||||
'https://api.${hosts[0]}';
|
||||
|
||||
Future<String> convertStandardUrlToAPIUrl(
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) async =>
|
||||
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}';
|
||||
|
||||
@override
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||
@@ -108,28 +233,84 @@ class GitHub extends AppSource {
|
||||
true
|
||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||
: null;
|
||||
Response res = await get(Uri.parse(requestUrl));
|
||||
String? regexNotesFilter =
|
||||
(additionalSettings['filterReleaseNotesByRegEx'] as String?)
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? additionalSettings['filterReleaseNotesByRegEx']
|
||||
: null;
|
||||
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
|
||||
bool dontSortReleasesList =
|
||||
additionalSettings['dontSortReleasesList'] == true;
|
||||
bool useLatestAssetDateAsReleaseDate =
|
||||
additionalSettings['useLatestAssetDateAsReleaseDate'] == true;
|
||||
dynamic latestRelease;
|
||||
if (verifyLatestTag) {
|
||||
var temp = requestUrl.split('?');
|
||||
Response res = await sourceRequest(
|
||||
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}',
|
||||
additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
if (onHttpErrorCode != null) {
|
||||
onHttpErrorCode(res);
|
||||
}
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
latestRelease = jsonDecode(res.body);
|
||||
}
|
||||
Response res = await sourceRequest(requestUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||
if (latestRelease != null) {
|
||||
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
|
||||
if (releases
|
||||
.where((element) =>
|
||||
(element['tag_name'] ?? element['name']) == latestTag)
|
||||
.isEmpty) {
|
||||
releases = [latestRelease, ...releases];
|
||||
}
|
||||
}
|
||||
|
||||
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)
|
||||
?.map((e) {
|
||||
return e['name'] != null && e['browser_download_url'] != null
|
||||
List<MapEntry<String, String>> getReleaseAssetUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)?.map((e) {
|
||||
return (e['name'] != null) &&
|
||||
((e['url'] ?? e['browser_download_url']) != null)
|
||||
? MapEntry(e['name'] as String,
|
||||
e['browser_download_url'] as String)
|
||||
(e['url'] ?? e['browser_download_url']) as String)
|
||||
: const MapEntry('', '');
|
||||
})
|
||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||
.toList() ??
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
DateTime? getReleaseDateFromRelease(dynamic rel) =>
|
||||
DateTime? getPublishDateFromRelease(dynamic rel) =>
|
||||
rel?['published_at'] != null
|
||||
? DateTime.parse(rel['published_at'])
|
||||
: null;
|
||||
DateTime? getNewestAssetDateFromRelease(dynamic rel) {
|
||||
var t = (rel['assets'] as List<dynamic>?)
|
||||
?.map((e) {
|
||||
return e?['updated_at'] != null
|
||||
? DateTime.parse(e['updated_at'])
|
||||
: null;
|
||||
})
|
||||
.where((e) => e != null)
|
||||
.toList();
|
||||
t?.sort((a, b) => b!.compareTo(a!));
|
||||
if (t?.isNotEmpty == true) {
|
||||
return t!.first;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime? getReleaseDateFromRelease(dynamic rel, bool useAssetDate) =>
|
||||
!useAssetDate
|
||||
? getPublishDateFromRelease(rel)
|
||||
: getNewestAssetDateFromRelease(rel);
|
||||
|
||||
if (dontSortReleasesList) {
|
||||
releases = releases.reversed.toList();
|
||||
} else {
|
||||
releases.sort((a, b) {
|
||||
// See #478
|
||||
// See #478 and #534
|
||||
if (a == b) {
|
||||
return 0;
|
||||
} else if (a == null) {
|
||||
@@ -137,10 +318,41 @@ class GitHub extends AppSource {
|
||||
} else if (b == null) {
|
||||
return 1;
|
||||
} else {
|
||||
return getReleaseDateFromRelease(a)!
|
||||
.compareTo(getReleaseDateFromRelease(b)!);
|
||||
var nameA = a['tag_name'] ?? a['name'];
|
||||
var nameB = b['tag_name'] ?? b['name'];
|
||||
var stdFormats = findStandardFormatsForVersion(nameA, true)
|
||||
.intersection(findStandardFormatsForVersion(nameB, true));
|
||||
if (stdFormats.isNotEmpty) {
|
||||
var reg = RegExp(stdFormats.first);
|
||||
var matchA = reg.firstMatch(nameA);
|
||||
var matchB = reg.firstMatch(nameB);
|
||||
return compareAlphaNumeric(
|
||||
(nameA as String).substring(matchA!.start, matchA.end),
|
||||
(nameB as String).substring(matchB!.start, matchB.end));
|
||||
} else {
|
||||
return (getReleaseDateFromRelease(
|
||||
a, useLatestAssetDateAsReleaseDate) ??
|
||||
DateTime(1))
|
||||
.compareTo(getReleaseDateFromRelease(
|
||||
b, useLatestAssetDateAsReleaseDate) ??
|
||||
DateTime(0));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (latestRelease != null &&
|
||||
(latestRelease['tag_name'] ?? latestRelease['name']) != null &&
|
||||
releases.isNotEmpty &&
|
||||
latestRelease !=
|
||||
(releases[releases.length - 1]['tag_name'] ??
|
||||
releases[0]['name'])) {
|
||||
var ind = releases.indexWhere((element) =>
|
||||
(latestRelease['tag_name'] ?? latestRelease['name']) ==
|
||||
(element['tag_name'] ?? element['name']));
|
||||
if (ind >= 0) {
|
||||
releases.add(releases.removeAt(ind));
|
||||
}
|
||||
}
|
||||
releases = releases.reversed.toList();
|
||||
dynamic targetRelease;
|
||||
var prerrelsSkipped = 0;
|
||||
@@ -163,29 +375,56 @@ class GitHub extends AppSource {
|
||||
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
||||
continue;
|
||||
}
|
||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||
if (regexNotesFilter != null &&
|
||||
!RegExp(regexNotesFilter)
|
||||
.hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
||||
continue;
|
||||
}
|
||||
var allAssetUrls = getReleaseAssetUrls(releases[i]);
|
||||
List<MapEntry<String, String>> apkUrls = allAssetUrls
|
||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
|
||||
apkUrls = filterApks(apkUrls, additionalSettings['apkFilterRegEx'],
|
||||
additionalSettings['invertAPKFilter']);
|
||||
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||
continue;
|
||||
}
|
||||
targetRelease = releases[i];
|
||||
targetRelease['apkUrls'] = apkUrls;
|
||||
targetRelease['version'] =
|
||||
targetRelease['tag_name'] ?? targetRelease['name'];
|
||||
if (targetRelease['tarball_url'] != null) {
|
||||
allAssetUrls.add(MapEntry(
|
||||
(targetRelease['version'] ?? 'source') + '.tar.gz',
|
||||
targetRelease['tarball_url']));
|
||||
}
|
||||
if (targetRelease['zipball_url'] != null) {
|
||||
allAssetUrls.add(MapEntry(
|
||||
(targetRelease['version'] ?? 'source') + '.zip',
|
||||
targetRelease['zipball_url']));
|
||||
}
|
||||
targetRelease['allAssetUrls'] = allAssetUrls;
|
||||
break;
|
||||
}
|
||||
if (targetRelease == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String? version = targetRelease['tag_name'];
|
||||
DateTime? releaseDate = getReleaseDateFromRelease(targetRelease);
|
||||
String? version = targetRelease['version'];
|
||||
DateTime? releaseDate = getReleaseDateFromRelease(
|
||||
targetRelease, useLatestAssetDateAsReleaseDate);
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
var changeLog = targetRelease['body'].toString();
|
||||
var changeLog = (targetRelease['body'] ?? '').toString();
|
||||
return APKDetails(
|
||||
version,
|
||||
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
|
||||
getAppNames(standardUrl),
|
||||
releaseDate: releaseDate,
|
||||
changeLog: changeLog.isEmpty ? null : changeLog);
|
||||
changeLog: changeLog.isEmpty ? null : changeLog,
|
||||
allAssetUrls:
|
||||
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>);
|
||||
} else {
|
||||
if (onHttpErrorCode != null) {
|
||||
onHttpErrorCode(res);
|
||||
@@ -194,15 +433,35 @@ class GitHub extends AppSource {
|
||||
}
|
||||
}
|
||||
|
||||
getLatestAPKDetailsCommon2(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
Future<String> Function(bool) reqUrlGenerator,
|
||||
dynamic Function(Response)? onHttpErrorCode) async {
|
||||
try {
|
||||
return await getLatestAPKDetailsCommon(
|
||||
await reqUrlGenerator(false), standardUrl, additionalSettings,
|
||||
onHttpErrorCode: onHttpErrorCode);
|
||||
} catch (err) {
|
||||
if (err is NoReleasesError && additionalSettings['trackOnly'] == true) {
|
||||
return await getLatestAPKDetailsCommon(
|
||||
await reqUrlGenerator(true), standardUrl, additionalSettings,
|
||||
onHttpErrorCode: onHttpErrorCode);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
return getLatestAPKDetailsCommon(
|
||||
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100',
|
||||
standardUrl,
|
||||
additionalSettings, onHttpErrorCode: (Response res) {
|
||||
return await getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||
(bool useTagUrl) async {
|
||||
return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||
}, (Response res) {
|
||||
rateLimitErrorCheck(res);
|
||||
});
|
||||
}
|
||||
@@ -213,21 +472,29 @@ class GitHub extends AppSource {
|
||||
return AppNames(names[0], names[1]);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> searchCommon(
|
||||
Future<Map<String, List<String>>> searchCommon(
|
||||
String query, String requestUrl, String rootProp,
|
||||
{Function(Response)? onHttpErrorCode}) async {
|
||||
Response res = await get(Uri.parse(requestUrl));
|
||||
{Function(Response)? onHttpErrorCode,
|
||||
Map<String, dynamic> querySettings = const {}}) async {
|
||||
Response res = await sourceRequest(requestUrl, {});
|
||||
if (res.statusCode == 200) {
|
||||
Map<String, String> urlsWithDescriptions = {};
|
||||
int minStarCount = querySettings['minStarCount'] != null
|
||||
? int.parse(querySettings['minStarCount'])
|
||||
: 0;
|
||||
Map<String, List<String>> urlsWithDescriptions = {};
|
||||
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
|
||||
if ((e['stargazers_count'] ?? e['stars_count'] ?? 0) >= minStarCount) {
|
||||
urlsWithDescriptions.addAll({
|
||||
e['html_url'] as String:
|
||||
e['html_url'] as String: [
|
||||
e['full_name'] as String,
|
||||
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
||||
(e['description'] != null
|
||||
? e['description'] as String
|
||||
: tr('noDescription')))
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
return urlsWithDescriptions;
|
||||
} else {
|
||||
if (onHttpErrorCode != null) {
|
||||
@@ -238,13 +505,14 @@ class GitHub extends AppSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> search(String query) async {
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
return searchCommon(
|
||||
query,
|
||||
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||
'${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||
'items', onHttpErrorCode: (Response res) {
|
||||
rateLimitErrorCheck(res);
|
||||
});
|
||||
}, querySettings: querySettings);
|
||||
}
|
||||
|
||||
rateLimitErrorCheck(Response res) {
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
import 'package:html/parser.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class GitLab extends AppSource {
|
||||
GitLab() {
|
||||
host = 'gitlab.com';
|
||||
hosts = ['gitlab.com'];
|
||||
canSearch = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
|
||||
sourceConfigSettingFormItems = [
|
||||
GeneratedFormTextField('gitlab-creds',
|
||||
label: tr('gitlabPATLabel'),
|
||||
password: true,
|
||||
required: false,
|
||||
belowWidgets: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
launchUrlString(
|
||||
'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token',
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text(
|
||||
tr('about'),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline, fontSize: 12),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
)
|
||||
])
|
||||
];
|
||||
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
@@ -19,78 +52,144 @@ class GitLab extends AppSource {
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
||||
SettingsProvider settingsProvider = SettingsProvider();
|
||||
await settingsProvider.initializeSettings();
|
||||
var sourceConfig =
|
||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
||||
String? creds = sourceConfig['gitlab-creds'];
|
||||
return creds != null && creds.isNotEmpty ? creds : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
var url =
|
||||
'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
|
||||
var res = await sourceRequest(url, {});
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var json = jsonDecode(res.body) as List<dynamic>;
|
||||
Map<String, List<String>> results = {};
|
||||
for (var element in json) {
|
||||
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
|
||||
element['name_with_namespace'],
|
||||
element['description'] ?? tr('noDescription')
|
||||
];
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||
'$standardUrl/-/releases';
|
||||
|
||||
@override
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
{bool forAPKDownload = false}) async {
|
||||
// Change headers to pacify, e.g. cloudflare protection
|
||||
// Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697)
|
||||
var headers = <String, String>{};
|
||||
headers[HttpHeaders.refererHeader] = 'https://${hosts[0]}';
|
||||
if (headers.isNotEmpty) {
|
||||
return headers;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
bool fallbackToOlderReleases =
|
||||
additionalSettings['fallbackToOlderReleases'] == true;
|
||||
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
|
||||
if (res.statusCode == 200) {
|
||||
var standardUri = Uri.parse(standardUrl);
|
||||
var parsedHtml = parse(res.body);
|
||||
var apkDetailsList = parsedHtml.querySelectorAll('entry').map((entry) {
|
||||
var entryContent = parse(
|
||||
parseFragment(entry.querySelector('content')!.innerHtml).text);
|
||||
var apkUrls = [
|
||||
...getLinksFromParsedHTML(
|
||||
entryContent,
|
||||
RegExp(
|
||||
'^${standardUri.path.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
||||
return '\\${x[0]}';
|
||||
})}/uploads/[^/]+/[^/]+\\.apk\$',
|
||||
caseSensitive: false),
|
||||
standardUri.origin),
|
||||
// GitLab releases may contain links to externally hosted APKs
|
||||
...getLinksFromParsedHTML(entryContent,
|
||||
RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
|
||||
.where((element) => Uri.parse(element).host != '')
|
||||
.toList()
|
||||
];
|
||||
// Prepare request params
|
||||
var names = GitHub().getAppNames(standardUrl);
|
||||
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
||||
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
|
||||
|
||||
var entryId = entry.querySelector('id')?.innerHtml;
|
||||
var version =
|
||||
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
||||
var releaseDateString = entry.querySelector('updated')?.innerHtml;
|
||||
DateTime? releaseDate = releaseDateString != null
|
||||
? DateTime.parse(releaseDateString)
|
||||
: null;
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
bool trackOnly = additionalSettings['trackOnly'] == true;
|
||||
|
||||
// Request data from REST API
|
||||
Response res = await sourceRequest(
|
||||
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
|
||||
additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
|
||||
|
||||
// Extract .apk details from received data
|
||||
Iterable<APKDetails> apkDetailsList = [];
|
||||
var json = jsonDecode(res.body) as List<dynamic>;
|
||||
apkDetailsList = json.map((e) {
|
||||
var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? [])
|
||||
.map((e) {
|
||||
return (e['direct_asset_url'] ?? e['url'] ?? '') as String;
|
||||
})
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
List<String> uploadedAPKsFromDescription =
|
||||
((e['description'] ?? '') as String)
|
||||
.split('](')
|
||||
.join('\n')
|
||||
.split('.apk)')
|
||||
.join('.apk\n')
|
||||
.split('\n')
|
||||
.where((s) => s.startsWith('/uploads/') && s.endsWith('apk'))
|
||||
.map((s) => '$standardUrl$s')
|
||||
.toList();
|
||||
var apkUrlsSet = apkUrlsFromAssets.toSet();
|
||||
apkUrlsSet.addAll(uploadedAPKsFromDescription);
|
||||
var releaseDateString = e['released_at'] ?? e['created_at'];
|
||||
DateTime? releaseDate =
|
||||
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
|
||||
return APKDetails(
|
||||
e['tag_name'] ?? e['name'],
|
||||
getApkUrlsFromUrls(apkUrlsSet.toList()),
|
||||
GitHub().getAppNames(standardUrl),
|
||||
releaseDate: releaseDate);
|
||||
});
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
if (fallbackToOlderReleases) {
|
||||
if (additionalSettings['trackOnly'] != true) {
|
||||
var finalResult = apkDetailsList.first;
|
||||
|
||||
// Fallback procedure
|
||||
bool fallbackToOlderReleases =
|
||||
additionalSettings['fallbackToOlderReleases'] == true;
|
||||
if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) {
|
||||
apkDetailsList =
|
||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
||||
finalResult = apkDetailsList.first;
|
||||
}
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
|
||||
if (finalResult.apkUrls.isEmpty && !trackOnly) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
}
|
||||
return apkDetailsList.first;
|
||||
|
||||
finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
|
||||
if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+\$')
|
||||
.hasMatch(apkUrl.value)) {
|
||||
return MapEntry(
|
||||
apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/'));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
return apkUrl;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,342 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
||||
try {
|
||||
Uri.parse(ambiguousUrl).origin;
|
||||
return ambiguousUrl;
|
||||
} catch (err) {
|
||||
// is relative
|
||||
}
|
||||
var currPathSegments = referenceAbsoluteUrl.path
|
||||
.split('/')
|
||||
.where((element) => element.trim().isNotEmpty)
|
||||
.toList();
|
||||
String absoluteUrl;
|
||||
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
||||
absoluteUrl = '${referenceAbsoluteUrl.origin}$ambiguousUrl';
|
||||
} else if (currPathSegments.isEmpty) {
|
||||
absoluteUrl = '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
|
||||
} else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) {
|
||||
absoluteUrl =
|
||||
'${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
|
||||
} else {
|
||||
absoluteUrl =
|
||||
'${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - (currPathSegments.last.contains('.') ? 1 : 0)).join('/')}/$ambiguousUrl';
|
||||
}
|
||||
return Uri.parse(absoluteUrl).toString();
|
||||
}
|
||||
|
||||
int compareAlphaNumeric(String a, String b) {
|
||||
List<String> aParts = _splitAlphaNumeric(a);
|
||||
List<String> bParts = _splitAlphaNumeric(b);
|
||||
|
||||
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
|
||||
String aPart = aParts[i];
|
||||
String bPart = bParts[i];
|
||||
|
||||
bool aIsNumber = _isNumeric(aPart);
|
||||
bool bIsNumber = _isNumeric(bPart);
|
||||
|
||||
if (aIsNumber && bIsNumber) {
|
||||
int aNumber = int.parse(aPart);
|
||||
int bNumber = int.parse(bPart);
|
||||
int cmp = aNumber.compareTo(bNumber);
|
||||
if (cmp != 0) {
|
||||
return cmp;
|
||||
}
|
||||
} else if (!aIsNumber && !bIsNumber) {
|
||||
int cmp = aPart.compareTo(bPart);
|
||||
if (cmp != 0) {
|
||||
return cmp;
|
||||
}
|
||||
} else {
|
||||
// Alphanumeric strings come before numeric strings
|
||||
return aIsNumber ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
return aParts.length.compareTo(bParts.length);
|
||||
}
|
||||
|
||||
List<String> _splitAlphaNumeric(String s) {
|
||||
List<String> parts = [];
|
||||
StringBuffer sb = StringBuffer();
|
||||
|
||||
bool isNumeric = _isNumeric(s[0]);
|
||||
sb.write(s[0]);
|
||||
|
||||
for (int i = 1; i < s.length; i++) {
|
||||
bool currentIsNumeric = _isNumeric(s[i]);
|
||||
if (currentIsNumeric == isNumeric) {
|
||||
sb.write(s[i]);
|
||||
} else {
|
||||
parts.add(sb.toString());
|
||||
sb.clear();
|
||||
sb.write(s[i]);
|
||||
isNumeric = currentIsNumeric;
|
||||
}
|
||||
}
|
||||
|
||||
parts.add(sb.toString());
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
bool _isNumeric(String s) {
|
||||
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
|
||||
}
|
||||
|
||||
class HTML extends AppSource {
|
||||
var finalStepFormitems = [
|
||||
[
|
||||
GeneratedFormTextField('customLinkFilterRegex',
|
||||
label: tr('customLinkFilterRegex'),
|
||||
hint: 'download/(.*/)?(android|apk|mobile)',
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('versionExtractWholePage',
|
||||
label: tr('versionExtractWholePage'))
|
||||
]
|
||||
];
|
||||
var commonFormItems = [
|
||||
[GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
|
||||
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
|
||||
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
|
||||
[
|
||||
GeneratedFormSwitch('sortByLastLinkSegment',
|
||||
label: tr('sortByLastLinkSegment'))
|
||||
],
|
||||
];
|
||||
var intermediateFormItems = [
|
||||
[
|
||||
GeneratedFormTextField('customLinkFilterRegex',
|
||||
label: tr('intermediateLinkRegex'),
|
||||
hint: '([0-9]+.)*[0-9]+/\$',
|
||||
required: true,
|
||||
additionalValidators: [(value) => regExValidator(value)])
|
||||
],
|
||||
];
|
||||
HTML() {
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
GeneratedFormSubForm(
|
||||
'intermediateLink', [...intermediateFormItems, ...commonFormItems],
|
||||
label: tr('intermediateLink'))
|
||||
],
|
||||
finalStepFormitems[0],
|
||||
...commonFormItems,
|
||||
...finalStepFormitems.sublist(1),
|
||||
[
|
||||
GeneratedFormSubForm(
|
||||
'requestHeader',
|
||||
[
|
||||
[
|
||||
GeneratedFormTextField('requestHeader',
|
||||
label: tr('requestHeader'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
if ((value ?? 'empty:valid')
|
||||
.split(':')
|
||||
.map((e) => e.trim())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.length <
|
||||
2) {
|
||||
return tr('invalidInput');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
])
|
||||
]
|
||||
],
|
||||
label: tr('requestHeader'),
|
||||
defaultValue: [
|
||||
{
|
||||
'requestHeader':
|
||||
'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormDropdown(
|
||||
'defaultPseudoVersioningMethod',
|
||||
[
|
||||
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
||||
MapEntry('APKLinkHash', tr('APKLinkHash'))
|
||||
],
|
||||
label: tr('defaultPseudoVersioningMethod'),
|
||||
defaultValue: 'partialAPKHash')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
{bool forAPKDownload = false}) async {
|
||||
if (additionalSettings.isNotEmpty) {
|
||||
if (additionalSettings['requestHeader']?.isNotEmpty != true) {
|
||||
additionalSettings['requestHeader'] = [];
|
||||
}
|
||||
additionalSettings['requestHeader'] = additionalSettings['requestHeader']
|
||||
.where((l) => l['requestHeader'].isNotEmpty == true)
|
||||
.toList();
|
||||
Map<String, String> requestHeaders = {};
|
||||
for (int i = 0; i < (additionalSettings['requestHeader'].length); i++) {
|
||||
var temp =
|
||||
(additionalSettings['requestHeader'][i]['requestHeader'] as String)
|
||||
.split(':');
|
||||
requestHeaders[temp[0].trim()] = temp.sublist(1).join(':').trim();
|
||||
}
|
||||
return requestHeaders;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Given an HTTP response, grab some links according to the common additional settings
|
||||
// (those that apply to intermediate and final steps)
|
||||
Future<List<MapEntry<String, String>>> grabLinksCommon(
|
||||
Response res, Map<String, dynamic> additionalSettings) async {
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
List<MapEntry<String, String>> allLinks = html
|
||||
.querySelectorAll('a')
|
||||
.map((element) => MapEntry(
|
||||
element.attributes['href'] ?? '',
|
||||
element.text.isNotEmpty
|
||||
? element.text
|
||||
: (element.attributes['href'] ?? '').split('/').last))
|
||||
.where((element) => element.key.isNotEmpty)
|
||||
.map((e) =>
|
||||
MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
|
||||
.toList();
|
||||
if (allLinks.isEmpty) {
|
||||
allLinks = RegExp(
|
||||
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
|
||||
.allMatches(res.body)
|
||||
.map((match) =>
|
||||
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
|
||||
.toList();
|
||||
}
|
||||
List<MapEntry<String, String>> links = [];
|
||||
bool skipSort = additionalSettings['skipSort'] == true;
|
||||
bool filterLinkByText = additionalSettings['filterByLinkText'] == true;
|
||||
if ((additionalSettings['customLinkFilterRegex'] as String?)?.isNotEmpty ==
|
||||
true) {
|
||||
var reg = RegExp(additionalSettings['customLinkFilterRegex']);
|
||||
links = allLinks.where((element) {
|
||||
var link = element.key;
|
||||
try {
|
||||
link = Uri.decodeFull(element.key);
|
||||
} catch (e) {
|
||||
// Some links may not have valid encoding
|
||||
}
|
||||
return reg.hasMatch(filterLinkByText ? element.value : link);
|
||||
}).toList();
|
||||
} else {
|
||||
links = allLinks.where((element) {
|
||||
var link = element.key;
|
||||
try {
|
||||
link = Uri.decodeFull(element.key);
|
||||
} catch (e) {
|
||||
// Some links may not have valid encoding
|
||||
}
|
||||
return Uri.parse(filterLinkByText ? element.value : link)
|
||||
.path
|
||||
.toLowerCase()
|
||||
.endsWith('.apk');
|
||||
}).toList();
|
||||
}
|
||||
if (!skipSort) {
|
||||
links.sort((a, b) => additionalSettings['sortByLastLinkSegment'] == true
|
||||
? compareAlphaNumeric(
|
||||
a.key.split('/').where((e) => e.isNotEmpty).last,
|
||||
b.key.split('/').where((e) => e.isNotEmpty).last)
|
||||
: compareAlphaNumeric(a.key, b.key));
|
||||
}
|
||||
if (additionalSettings['reverseSort'] == true) {
|
||||
links = links.reversed.toList();
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
var uri = Uri.parse(standardUrl);
|
||||
Response res = await get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
List<String> links = parse(res.body)
|
||||
.querySelectorAll('a')
|
||||
.map((element) => element.attributes['href'] ?? '')
|
||||
.where((element) => element.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
|
||||
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||
links = links.where((element) => reg.hasMatch(element)).toList();
|
||||
var currentUrl = standardUrl;
|
||||
if (additionalSettings['intermediateLink']?.isNotEmpty != true) {
|
||||
additionalSettings['intermediateLink'] = [];
|
||||
}
|
||||
additionalSettings['intermediateLink'] =
|
||||
additionalSettings['intermediateLink']
|
||||
.where((l) => l['customLinkFilterRegex'].isNotEmpty == true)
|
||||
.toList();
|
||||
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
|
||||
var intLinks = await grabLinksCommon(
|
||||
await sourceRequest(currentUrl, additionalSettings),
|
||||
additionalSettings['intermediateLink'][i]);
|
||||
if (intLinks.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
} else {
|
||||
currentUrl = intLinks.last.key;
|
||||
}
|
||||
}
|
||||
var uri = Uri.parse(currentUrl);
|
||||
List<MapEntry<String, String>> links = [];
|
||||
String versionExtractionWholePageString = currentUrl;
|
||||
if (additionalSettings['directAPKLink'] != true) {
|
||||
Response res = await sourceRequest(currentUrl, additionalSettings);
|
||||
versionExtractionWholePageString =
|
||||
res.body.split('\r\n').join('\n').split('\n').join('\\n');
|
||||
links = await grabLinksCommon(res, additionalSettings);
|
||||
links = filterApks(links, additionalSettings['apkFilterRegEx'],
|
||||
additionalSettings['invertAPKFilter']);
|
||||
if (links.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
var rel = links.last;
|
||||
var apkName = rel.split('/').last;
|
||||
var version = apkName.substring(0, apkName.length - 4);
|
||||
List<String> apkUrls = [rel].map((e) {
|
||||
} else {
|
||||
links = [MapEntry(currentUrl, currentUrl)];
|
||||
}
|
||||
var rel = links.last.key;
|
||||
var relDecoded = rel;
|
||||
try {
|
||||
Uri.parse(e).origin;
|
||||
return e;
|
||||
} catch (err) {
|
||||
// is relative
|
||||
}
|
||||
var currPathSegments = uri.path.split('/');
|
||||
if (e.startsWith('/') || currPathSegments.isEmpty) {
|
||||
return '${uri.origin}/$e';
|
||||
} else {
|
||||
return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e';
|
||||
}
|
||||
}).toList();
|
||||
return APKDetails(
|
||||
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
relDecoded = Uri.decodeFull(rel);
|
||||
} catch (e) {
|
||||
// Some links may not have valid encoding
|
||||
}
|
||||
String? version;
|
||||
version = extractVersion(
|
||||
additionalSettings['versionExtractionRegEx'] as String?,
|
||||
additionalSettings['matchGroupToUse'] as String?,
|
||||
additionalSettings['versionExtractWholePage'] == true
|
||||
? versionExtractionWholePageString
|
||||
: relDecoded);
|
||||
version ??=
|
||||
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
|
||||
? rel.hashCode.toString()
|
||||
: (await checkPartialDownloadHashDynamic(rel)).toString();
|
||||
return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
|
||||
AppNames(uri.host, tr('app')));
|
||||
}
|
||||
}
|
||||
|
||||
95
lib/app_sources/huaweiappgallery.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class HuaweiAppGallery extends AppSource {
|
||||
HuaweiAppGallery() {
|
||||
name = 'Huawei AppGallery';
|
||||
hosts = ['appgallery.huawei.com'];
|
||||
versionDetectionDisallowed = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/app/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
getDlUrl(String standardUrl) =>
|
||||
'https://${hosts[0].replaceAll('appgallery.', 'appgallery.cloud.')}/appdl/${standardUrl.split('/').last}';
|
||||
|
||||
requestAppdlRedirect(
|
||||
String dlUrl, Map<String, dynamic> additionalSettings) async {
|
||||
Response res =
|
||||
await sourceRequest(dlUrl, additionalSettings, followRedirects: false);
|
||||
if (res.statusCode == 200 ||
|
||||
res.statusCode == 302 ||
|
||||
res.statusCode == 304) {
|
||||
return res;
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
appIdFromRedirectDlUrl(String redirectDlUrl) {
|
||||
var parts = redirectDlUrl
|
||||
.split('?')[0]
|
||||
.split('/')
|
||||
.last
|
||||
.split('.')
|
||||
.reversed
|
||||
.toList();
|
||||
parts.removeAt(0);
|
||||
parts.removeAt(0);
|
||||
return parts.reversed.join('.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
String dlUrl = getDlUrl(standardUrl);
|
||||
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
||||
return res.headers['location'] != null
|
||||
? appIdFromRedirectDlUrl(res.headers['location']!)
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String dlUrl = getDlUrl(standardUrl);
|
||||
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
||||
if (res.headers['location'] == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String appId = appIdFromRedirectDlUrl(res.headers['location']!);
|
||||
var relDateStr =
|
||||
res.headers['location']?.split('?')[0].split('.').reversed.toList()[1];
|
||||
var relDateStrAdj = relDateStr?.split('');
|
||||
var tempLen = relDateStrAdj?.length ?? 0;
|
||||
var i = 2;
|
||||
while (i < tempLen) {
|
||||
relDateStrAdj?.insert((i + i ~/ 2 - 1), '-');
|
||||
i += 2;
|
||||
}
|
||||
var relDate = relDateStrAdj == null
|
||||
? null
|
||||
: DateFormat('yy-MM-dd-HH-mm', 'en_US').parse(relDateStrAdj.join(''));
|
||||
if (relDateStr == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(
|
||||
relDateStr, [MapEntry('$appId.apk', dlUrl)], AppNames(name, appId),
|
||||
releaseDate: relDate);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,40 @@
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/fdroid.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class IzzyOnDroid extends AppSource {
|
||||
late FDroid fd;
|
||||
|
||||
IzzyOnDroid() {
|
||||
host = 'android.izzysoft.de';
|
||||
hosts = ['izzysoft.de'];
|
||||
fd = FDroid();
|
||||
additionalSourceAppSpecificSettingFormItems =
|
||||
fd.additionalSourceAppSpecificSettingFormItems;
|
||||
allowSubDomains = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegExA = RegExp(
|
||||
'^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegExA.firstMatch(url);
|
||||
if (match == null) {
|
||||
RegExp standardUrlRegExB = RegExp(
|
||||
'^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+',
|
||||
caseSensitive: false);
|
||||
match = standardUrlRegExB.firstMatch(url);
|
||||
}
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String? tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||
return FDroid().tryInferringAppId(standardUrl);
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return fd.tryInferringAppId(standardUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -29,11 +42,14 @@ class IzzyOnDroid extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String? appId = tryInferringAppId(standardUrl);
|
||||
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await get(
|
||||
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
|
||||
String? appId = await tryInferringAppId(standardUrl);
|
||||
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await sourceRequest(
|
||||
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId',
|
||||
additionalSettings),
|
||||
'https://android.izzysoft.de/frepo/$appId',
|
||||
standardUrl);
|
||||
standardUrl,
|
||||
name,
|
||||
additionalSettings: additionalSettings);
|
||||
}
|
||||
}
|
||||
|
||||
68
lib/app_sources/jenkins.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Jenkins extends AppSource {
|
||||
Jenkins() {
|
||||
versionDetectionDisallowed = true;
|
||||
neverAutoSelect = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
}
|
||||
|
||||
String trimJobUrl(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('.*/job/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||
'$standardUrl/-/releases';
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
standardUrl = trimJobUrl(standardUrl);
|
||||
Response res = await sourceRequest(
|
||||
'$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var json = jsonDecode(res.body);
|
||||
var releaseDate = json['timestamp'] == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
||||
var version =
|
||||
json['number'] == null ? null : (json['number'] as int).toString();
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
var apkUrls = (json['artifacts'] as List<dynamic>)
|
||||
.map((e) {
|
||||
var path = (e['relativePath'] as String?);
|
||||
if (path != null && path.isNotEmpty) {
|
||||
path = '$standardUrl/lastSuccessfulBuild/artifact/$path';
|
||||
}
|
||||
return path == null
|
||||
? const MapEntry<String, String>('', '')
|
||||
: MapEntry<String, String>(
|
||||
(e['fileName'] ?? e['relativePath']) as String, path);
|
||||
})
|
||||
.where((url) =>
|
||||
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
return APKDetails(
|
||||
version,
|
||||
apkUrls,
|
||||
releaseDate: releaseDate,
|
||||
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,19 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Mullvad extends AppSource {
|
||||
Mullvad() {
|
||||
host = 'mullvad.net';
|
||||
hosts = ['mullvad.net'];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -28,7 +30,8 @@ class Mullvad extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res = await get(Uri.parse('$standardUrl/en/download/android'));
|
||||
Response res = await sourceRequest(
|
||||
'$standardUrl/en/download/android', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var versions = parse(res.body)
|
||||
.querySelectorAll('p')
|
||||
|
||||
@@ -5,17 +5,20 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class NeutronCode extends AppSource {
|
||||
NeutronCode() {
|
||||
host = 'neutroncode.com';
|
||||
hosts = ['neutroncode.com'];
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -78,7 +81,7 @@ class NeutronCode extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res = await get(Uri.parse(standardUrl));
|
||||
Response res = await sourceRequest(standardUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var http = parse(res.body);
|
||||
var name = http.querySelector('.pd-title')?.innerHtml;
|
||||
@@ -91,7 +94,7 @@ class NeutronCode extends AppSource {
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
String? apkUrl = 'https://$host/download/$filename';
|
||||
String? apkUrl = 'https://${hosts[0]}/download/$filename';
|
||||
var dateStringOriginal =
|
||||
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
|
||||
var dateString = dateStringOriginal != null
|
||||
|
||||
@@ -5,12 +5,12 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Signal extends AppSource {
|
||||
Signal() {
|
||||
host = 'signal.org';
|
||||
hosts = ['signal.org'];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
return 'https://$host';
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return 'https://${hosts[0]}';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -18,8 +18,8 @@ class Signal extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res =
|
||||
await get(Uri.parse('https://updates.$host/android/latest.json'));
|
||||
Response res = await sourceRequest(
|
||||
'https://updates.${hosts[0]}/android/latest.json', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var json = jsonDecode(res.body);
|
||||
String? apkUrl = json['url'];
|
||||
|
||||
@@ -5,17 +5,34 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class SourceForge extends AppSource {
|
||||
SourceForge() {
|
||||
host = 'sourceforge.net';
|
||||
hosts = ['sourceforge.net'];
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
var sourceRegex = getSourceRegex(hosts);
|
||||
RegExp standardUrlRegExC =
|
||||
RegExp('^https?://(www\\.)?$sourceRegex/p/.+', caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegExC.firstMatch(url);
|
||||
if (match != null) {
|
||||
url =
|
||||
'https://${Uri.parse(match.group(0)!).host}/projects/${url.substring(Uri.parse(match.group(0)!).host.length + '/projects/'.length + 1)}';
|
||||
}
|
||||
RegExp standardUrlRegExB = RegExp(
|
||||
'^https?://(www\\.)?$sourceRegex/projects/[^/]+',
|
||||
caseSensitive: false);
|
||||
match = standardUrlRegExB.firstMatch(url);
|
||||
if (match != null && match.group(0) == url) {
|
||||
url = '$url/files';
|
||||
}
|
||||
RegExp standardUrlRegExA = RegExp(
|
||||
'^https?://(www\\.)?$sourceRegex/projects/[^/]+/files(/.+)?',
|
||||
caseSensitive: false);
|
||||
match = standardUrlRegExA.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -23,37 +40,79 @@ class SourceForge extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res = await get(Uri.parse('$standardUrl/rss?path=/'));
|
||||
var standardUri = Uri.parse(standardUrl);
|
||||
if (standardUri.pathSegments.length == 2) {
|
||||
standardUrl = '$standardUrl/files';
|
||||
standardUri = Uri.parse(standardUrl);
|
||||
}
|
||||
Response res = await sourceRequest(
|
||||
'${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
|
||||
additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var parsedHtml = parse(res.body);
|
||||
var allDownloadLinks =
|
||||
parsedHtml.querySelectorAll('guid').map((e) => e.innerHtml).toList();
|
||||
var allDownloadLinks = parsedHtml
|
||||
.querySelectorAll('guid')
|
||||
.map((e) => e.innerHtml)
|
||||
.where((element) => element.startsWith(standardUrl))
|
||||
.toList();
|
||||
getVersion(String url) {
|
||||
try {
|
||||
var tokens = url.split('/');
|
||||
var fi = tokens.indexOf('files');
|
||||
return tokens[tokens[fi + 2] == 'download' ? fi - 1 : fi + 1];
|
||||
var segments = url
|
||||
.substring(standardUrl.length)
|
||||
.split('/')
|
||||
.where((element) => element.isNotEmpty)
|
||||
.toList()
|
||||
.reversed
|
||||
.toList()
|
||||
.sublist(1)
|
||||
.reversed
|
||||
.toList();
|
||||
segments = segments.length > 1
|
||||
? segments.reversed.toList().sublist(1).reversed.toList()
|
||||
: segments;
|
||||
var version = segments.isNotEmpty ? segments.join('/') : null;
|
||||
if (version != null) {
|
||||
try {
|
||||
var extractedVersion = extractVersion(
|
||||
additionalSettings['versionExtractionRegEx'] as String?,
|
||||
additionalSettings['matchGroupToUse'] as String?,
|
||||
version);
|
||||
if (extractedVersion != null) {
|
||||
version = extractedVersion;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is NoVersionError) {
|
||||
version = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
return version;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? version = getVersion(allDownloadLinks[0]);
|
||||
var apkUrlListAllReleases = allDownloadLinks
|
||||
.where((element) => element.toLowerCase().endsWith('.apk/download'))
|
||||
.where((element) => getVersion(element) != null)
|
||||
.toList();
|
||||
if (apkUrlListAllReleases.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String? version = getVersion(apkUrlListAllReleases[0]);
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
var apkUrlListAllReleases = allDownloadLinks
|
||||
.where((element) => element.toLowerCase().endsWith('.apk/download'))
|
||||
.toList();
|
||||
|
||||
var apkUrlList =
|
||||
apkUrlListAllReleases // This can be used skipped for fallback support later
|
||||
.where((element) => getVersion(element) == version)
|
||||
.toList();
|
||||
return APKDetails(
|
||||
version,
|
||||
getApkUrlsFromUrls(apkUrlList),
|
||||
AppNames(
|
||||
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
|
||||
var segments = standardUrl.split('/');
|
||||
return APKDetails(version, getApkUrlsFromUrls(apkUrlList),
|
||||
AppNames(name, segments[segments.indexOf('files') - 1]));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
|
||||
120
lib/app_sources/sourcehut.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
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';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class SourceHut extends AppSource {
|
||||
SourceHut() {
|
||||
hosts = ['git.sr.ht'];
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return match.group(0)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
if (standardUrl.endsWith('/refs')) {
|
||||
standardUrl = standardUrl
|
||||
.split('/')
|
||||
.reversed
|
||||
.toList()
|
||||
.sublist(1)
|
||||
.reversed
|
||||
.join('/');
|
||||
}
|
||||
Uri standardUri = Uri.parse(standardUrl);
|
||||
String appName = standardUri.pathSegments.last;
|
||||
bool fallbackToOlderReleases =
|
||||
additionalSettings['fallbackToOlderReleases'] == true;
|
||||
Response res =
|
||||
await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var parsedHtml = parse(res.body);
|
||||
List<APKDetails> apkDetailsList = [];
|
||||
int ind = 0;
|
||||
|
||||
for (var entry in parsedHtml.querySelectorAll('item').sublist(0, 6)) {
|
||||
// Limit 5 for speed
|
||||
if (!fallbackToOlderReleases && ind > 0) {
|
||||
break;
|
||||
}
|
||||
String? version = entry.querySelector('title')?.text.trim();
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
String? releaseDateString = entry.querySelector('pubDate')?.innerHtml;
|
||||
String releasePage = '$standardUrl/refs/$version';
|
||||
DateTime? releaseDate;
|
||||
try {
|
||||
releaseDate = releaseDateString != null
|
||||
? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
||||
: null;
|
||||
releaseDate = releaseDateString != null
|
||||
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z')
|
||||
.parse(releaseDateString)
|
||||
: null;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
var res2 = await sourceRequest(releasePage, additionalSettings);
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
if (res2.statusCode == 200) {
|
||||
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
||||
.querySelectorAll('a')
|
||||
.map((e) => e.attributes['href'] ?? '')
|
||||
.where((e) => e.toLowerCase().endsWith('.apk'))
|
||||
.map((e) => ensureAbsoluteUrl(e, standardUri))
|
||||
.toList());
|
||||
}
|
||||
apkDetailsList.add(APKDetails(
|
||||
version,
|
||||
apkUrls,
|
||||
AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName,
|
||||
appName),
|
||||
releaseDate: releaseDate));
|
||||
ind++;
|
||||
}
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
if (fallbackToOlderReleases) {
|
||||
if (additionalSettings['trackOnly'] != true) {
|
||||
apkDetailsList =
|
||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
||||
}
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
return apkDetailsList.first;
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class SteamMobile extends AppSource {
|
||||
SteamMobile() {
|
||||
host = 'store.steampowered.com';
|
||||
name = tr('steam');
|
||||
hosts = ['store.steampowered.com'];
|
||||
name = 'Steam';
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
GeneratedFormDropdown('app', apks.entries.toList(),
|
||||
@@ -20,8 +20,8 @@ class SteamMobile extends AppSource {
|
||||
final apks = {'steam': tr('steamMobile'), 'steam-chat-app': tr('steamChat')};
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
return 'https://$host';
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return 'https://${hosts[0]}';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -29,7 +29,8 @@ class SteamMobile extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res = await get(Uri.parse('https://$host/mobile'));
|
||||
Response res =
|
||||
await sourceRequest('https://${hosts[0]}/mobile', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var apkNamePrefix = additionalSettings['app'] as String?;
|
||||
if (apkNamePrefix == null) {
|
||||
|
||||
@@ -6,13 +6,13 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class TelegramApp extends AppSource {
|
||||
TelegramApp() {
|
||||
host = 'telegram.org';
|
||||
hosts = ['telegram.org'];
|
||||
name = 'Telegram ${tr('app')}';
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
return 'https://$host';
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return 'https://${hosts[0]}';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -20,7 +20,8 @@ class TelegramApp extends AppSource {
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK'));
|
||||
Response res =
|
||||
await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var http = parse(res.body);
|
||||
var messages =
|
||||
|
||||
105
lib/app_sources/uptodown.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:obtainium/app_sources/apkpure.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Uptodown extends AppSource {
|
||||
Uptodown() {
|
||||
hosts = ['uptodown.com'];
|
||||
allowSubDomains = true;
|
||||
naiveStandardVersionDetection = true;
|
||||
showReleaseDateAsVersionToggle = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp(
|
||||
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||
caseSensitive: false);
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return '${match.group(0)!}/android/download';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return (await getAppDetailsFromPage(
|
||||
standardUrl, additionalSettings))['appId'];
|
||||
}
|
||||
|
||||
Future<Map<String, String?>> getAppDetailsFromPage(
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
String? version = html.querySelector('div.version')?.innerHtml;
|
||||
String? apkUrl =
|
||||
'${standardUrl.split('/').reversed.toList().sublist(1).reversed.join('/')}/post-download';
|
||||
String? name = html.querySelector('#detail-app-name')?.innerHtml.trim();
|
||||
String? author = html.querySelector('#author-link')?.innerHtml.trim();
|
||||
var detailElements = html.querySelectorAll('#technical-information td');
|
||||
String? appId = (detailElements.elementAtOrNull(2))?.innerHtml.trim();
|
||||
String? dateStr = (detailElements.elementAtOrNull(29))?.innerHtml.trim();
|
||||
return Map.fromEntries([
|
||||
MapEntry('version', version),
|
||||
MapEntry('apkUrl', apkUrl),
|
||||
MapEntry('appId', appId),
|
||||
MapEntry('name', name),
|
||||
MapEntry('author', author),
|
||||
MapEntry('dateStr', dateStr)
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
var appDetails =
|
||||
await getAppDetailsFromPage(standardUrl, additionalSettings);
|
||||
var version = appDetails['version'];
|
||||
var apkUrl = appDetails['apkUrl'];
|
||||
var appId = appDetails['appId'];
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
if (apkUrl == null) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
if (appId == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String appName = appDetails['name'] ?? tr('app');
|
||||
String author = appDetails['author'] ?? name;
|
||||
String? dateStr = appDetails['dateStr'];
|
||||
DateTime? relDate;
|
||||
if (dateStr != null) {
|
||||
relDate = parseDateTimeMMMddCommayyyy(dateStr);
|
||||
}
|
||||
return APKDetails(
|
||||
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
||||
releaseDate: relDate);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||
Map<String, dynamic> additionalSettings) async {
|
||||
var res = await sourceRequest(apkUrl, additionalSettings);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var html = parse(res.body);
|
||||
var finalUrlKey =
|
||||
html.querySelector('.post-download')?.attributes['data-url'];
|
||||
if (finalUrlKey == null) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
return 'https://dw.${hosts[0]}/dwn/$finalUrlKey';
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,47 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class VLC extends AppSource {
|
||||
VLC() {
|
||||
host = 'videolan.org';
|
||||
hosts = ['videolan.org'];
|
||||
}
|
||||
get dwUrlBase => 'https://get.${hosts[0]}/vlc-android/';
|
||||
|
||||
@override
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
{bool forAPKDownload = false}) async {
|
||||
return {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
return 'https://$host';
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return 'https://${hosts[0]}';
|
||||
}
|
||||
|
||||
Future<String?> getLatestVersion(
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
||||
Response res = await sourceRequest(dwUrlBase, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
var dwLinks = parse(res.body)
|
||||
.querySelectorAll('a')
|
||||
.where((element) => element.attributes['href'] != 'last/')
|
||||
.map((e) => e.attributes['href']?.split('/')[0])
|
||||
.toList();
|
||||
String? version = dwLinks.isNotEmpty ? dwLinks.last : null;
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return version;
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -38,26 +68,43 @@ class VLC extends AppSource {
|
||||
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!)
|
||||
var apkUrls = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64']
|
||||
.map((e) => '${targetUrl}VLC-Android-$version-$e.apk')
|
||||
.toList();
|
||||
} else {
|
||||
throw getObtainiumHttpError(res2);
|
||||
}
|
||||
|
||||
return APKDetails(
|
||||
version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC'));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||
Map<String, dynamic> additionalSettings) async {
|
||||
Response res = await sourceRequest(apkUrl, additionalSettings);
|
||||
if (res.statusCode == 200) {
|
||||
String? apkUrl =
|
||||
parse(res.body).querySelector('#alt_link')?.attributes['href'];
|
||||
if (apkUrl == null) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
return apkUrl;
|
||||
} else if (res.statusCode == 500 &&
|
||||
res.body.toLowerCase().indexOf('mirror') > 0) {
|
||||
var html = parse(res.body);
|
||||
var err = '';
|
||||
html.body?.nodes.forEach((element) {
|
||||
if (element.text != null) {
|
||||
err += '${element.text}\n';
|
||||
}
|
||||
});
|
||||
err = err.trim();
|
||||
if (err.isEmpty) {
|
||||
err = tr('err');
|
||||
}
|
||||
throw ObtainiumError(err);
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,30 +5,31 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class WhatsApp extends AppSource {
|
||||
WhatsApp() {
|
||||
host = 'whatsapp.com';
|
||||
hosts = ['whatsapp.com'];
|
||||
versionDetectionDisallowed = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String standardizeURL(String url) {
|
||||
return 'https://$host';
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return 'https://${hosts[0]}';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
||||
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||
Map<String, dynamic> additionalSettings) async {
|
||||
Response res =
|
||||
await sourceRequest('$standardUrl/android', additionalSettings);
|
||||
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'))
|
||||
.map((e) => e.attributes['href'] ?? '')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.where((e) => e.contains('WhatsApp.apk'))
|
||||
.toList();
|
||||
if (targetLinks.isEmpty) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
return targetLinks[0]!;
|
||||
return targetLinks[0];
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
@@ -39,37 +40,16 @@ class WhatsApp extends AppSource {
|
||||
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,
|
||||
getApkUrlsFromUrls([
|
||||
'https://www.whatsapp.com/android?v=$version&=thisIsaPlaceholder&a=realURLPrefetchedAtDownloadTime'
|
||||
]),
|
||||
// This is a CDN link that is consistent per version
|
||||
// But it has query params that change constantly
|
||||
Uri apkUri = Uri.parse(await apkUrlPrefetchModifier(
|
||||
standardUrl, standardUrl, additionalSettings));
|
||||
var unusableApkUrl = '${apkUri.origin}/${apkUri.path}';
|
||||
// So we use the param-less URL is a pseudo-version to add the app and check for updates
|
||||
// See #357 for why we can't scrape the version number directly
|
||||
// But we re-fetch the URL again with its latest query params at the actual download time
|
||||
String version = unusableApkUrl.hashCode.toString();
|
||||
return APKDetails(version, getApkUrlsFromUrls([unusableApkUrl]),
|
||||
AppNames('Meta', 'WhatsApp'));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:hsluv/hsluv.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
abstract class GeneratedFormItem {
|
||||
late String key;
|
||||
@@ -11,6 +13,7 @@ abstract class GeneratedFormItem {
|
||||
late dynamic defaultValue;
|
||||
List<dynamic> additionalValidators;
|
||||
dynamic ensureType(dynamic val);
|
||||
GeneratedFormItem clone();
|
||||
|
||||
GeneratedFormItem(this.key,
|
||||
{this.label = 'Input',
|
||||
@@ -24,26 +27,38 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
||||
late int max;
|
||||
late String? hint;
|
||||
late bool password;
|
||||
late TextInputType? textInputType;
|
||||
|
||||
GeneratedFormTextField(String key,
|
||||
{String label = 'Input',
|
||||
List<Widget> belowWidgets = const [],
|
||||
String defaultValue = '',
|
||||
List<String? Function(String? value)> additionalValidators = const [],
|
||||
GeneratedFormTextField(super.key,
|
||||
{super.label,
|
||||
super.belowWidgets,
|
||||
String super.defaultValue = '',
|
||||
List<String? Function(String? value)> super.additionalValidators =
|
||||
const [],
|
||||
this.required = true,
|
||||
this.max = 1,
|
||||
this.hint,
|
||||
this.password = false})
|
||||
: super(key,
|
||||
label: label,
|
||||
belowWidgets: belowWidgets,
|
||||
defaultValue: defaultValue,
|
||||
additionalValidators: additionalValidators);
|
||||
this.password = false,
|
||||
this.textInputType});
|
||||
|
||||
@override
|
||||
String ensureType(val) {
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
GeneratedFormTextField clone() {
|
||||
return GeneratedFormTextField(key,
|
||||
label: label,
|
||||
belowWidgets: belowWidgets,
|
||||
defaultValue: defaultValue,
|
||||
additionalValidators: List.from(additionalValidators),
|
||||
required: required,
|
||||
max: max,
|
||||
hint: hint,
|
||||
password: password,
|
||||
textInputType: textInputType);
|
||||
}
|
||||
}
|
||||
|
||||
class GeneratedFormDropdown extends GeneratedFormItem {
|
||||
@@ -51,42 +66,61 @@ class GeneratedFormDropdown extends GeneratedFormItem {
|
||||
List<String>? disabledOptKeys;
|
||||
|
||||
GeneratedFormDropdown(
|
||||
String key,
|
||||
super.key,
|
||||
this.opts, {
|
||||
String label = 'Input',
|
||||
List<Widget> belowWidgets = const [],
|
||||
String defaultValue = '',
|
||||
super.label,
|
||||
super.belowWidgets,
|
||||
String super.defaultValue = '',
|
||||
this.disabledOptKeys,
|
||||
List<String? Function(String? value)> additionalValidators = const [],
|
||||
}) : super(key,
|
||||
label: label,
|
||||
belowWidgets: belowWidgets,
|
||||
defaultValue: defaultValue,
|
||||
additionalValidators: additionalValidators);
|
||||
List<String? Function(String? value)> super.additionalValidators = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
String ensureType(val) {
|
||||
return val.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class GeneratedFormSwitch extends GeneratedFormItem {
|
||||
GeneratedFormSwitch(
|
||||
String key, {
|
||||
String label = 'Input',
|
||||
List<Widget> belowWidgets = const [],
|
||||
bool defaultValue = false,
|
||||
List<String? Function(bool value)> additionalValidators = const [],
|
||||
}) : super(key,
|
||||
@override
|
||||
GeneratedFormDropdown clone() {
|
||||
return GeneratedFormDropdown(
|
||||
key,
|
||||
opts?.map((e) => MapEntry(e.key, e.value)).toList(),
|
||||
label: label,
|
||||
belowWidgets: belowWidgets,
|
||||
defaultValue: defaultValue,
|
||||
additionalValidators: additionalValidators);
|
||||
disabledOptKeys:
|
||||
disabledOptKeys != null ? List.from(disabledOptKeys!) : null,
|
||||
additionalValidators: List.from(additionalValidators),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeneratedFormSwitch extends GeneratedFormItem {
|
||||
bool disabled = false;
|
||||
|
||||
GeneratedFormSwitch(
|
||||
super.key, {
|
||||
super.label,
|
||||
super.belowWidgets,
|
||||
bool super.defaultValue = false,
|
||||
bool disabled = false,
|
||||
List<String? Function(bool value)> super.additionalValidators = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
bool ensureType(val) {
|
||||
return val == true || val == 'true';
|
||||
}
|
||||
|
||||
@override
|
||||
GeneratedFormSwitch clone() {
|
||||
return GeneratedFormSwitch(key,
|
||||
label: label,
|
||||
belowWidgets: belowWidgets,
|
||||
defaultValue: defaultValue,
|
||||
disabled: false,
|
||||
additionalValidators: List.from(additionalValidators));
|
||||
}
|
||||
}
|
||||
|
||||
class GeneratedFormTagInput extends GeneratedFormItem {
|
||||
@@ -95,27 +129,36 @@ class GeneratedFormTagInput extends GeneratedFormItem {
|
||||
late WrapAlignment alignment;
|
||||
late String emptyMessage;
|
||||
late bool showLabelWhenNotEmpty;
|
||||
GeneratedFormTagInput(String key,
|
||||
{String label = 'Input',
|
||||
List<Widget> belowWidgets = const [],
|
||||
Map<String, MapEntry<int, bool>> defaultValue = const {},
|
||||
GeneratedFormTagInput(super.key,
|
||||
{super.label,
|
||||
super.belowWidgets,
|
||||
Map<String, MapEntry<int, bool>> super.defaultValue = const {},
|
||||
List<String? Function(Map<String, MapEntry<int, bool>> value)>
|
||||
additionalValidators = const [],
|
||||
super.additionalValidators = const [],
|
||||
this.deleteConfirmationMessage,
|
||||
this.singleSelect = false,
|
||||
this.alignment = WrapAlignment.start,
|
||||
this.emptyMessage = 'Input',
|
||||
this.showLabelWhenNotEmpty = true})
|
||||
: super(key,
|
||||
label: label,
|
||||
belowWidgets: belowWidgets,
|
||||
defaultValue: defaultValue,
|
||||
additionalValidators: additionalValidators);
|
||||
this.showLabelWhenNotEmpty = true});
|
||||
|
||||
@override
|
||||
Map<String, MapEntry<int, bool>> ensureType(val) {
|
||||
return val is Map<String, MapEntry<int, bool>> ? val : {};
|
||||
}
|
||||
|
||||
@override
|
||||
GeneratedFormTagInput clone() {
|
||||
return GeneratedFormTagInput(key,
|
||||
label: label,
|
||||
belowWidgets: belowWidgets,
|
||||
defaultValue: defaultValue,
|
||||
additionalValidators: List.from(additionalValidators),
|
||||
deleteConfirmationMessage: deleteConfirmationMessage,
|
||||
singleSelect: singleSelect,
|
||||
alignment: alignment,
|
||||
emptyMessage: emptyMessage,
|
||||
showLabelWhenNotEmpty: showLabelWhenNotEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
typedef OnValueChanges = void Function(
|
||||
@@ -132,42 +175,85 @@ class GeneratedForm extends StatefulWidget {
|
||||
State<GeneratedForm> createState() => _GeneratedFormState();
|
||||
}
|
||||
|
||||
// Generates a random light color
|
||||
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
|
||||
Color generateRandomLightColor() {
|
||||
// Create a random number generator
|
||||
final Random random = Random();
|
||||
|
||||
// Generate random hue, saturation, and value values
|
||||
final double hue = random.nextDouble() * 360;
|
||||
final double saturation = 0.5 + random.nextDouble() * 0.5;
|
||||
final double value = 0.9 + random.nextDouble() * 0.1;
|
||||
|
||||
// Create a HSV color with the random values
|
||||
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
|
||||
List<List<GeneratedFormItem>> cloneFormItems(
|
||||
List<List<GeneratedFormItem>> items) {
|
||||
List<List<GeneratedFormItem>> clonedItems = [];
|
||||
for (var row in items) {
|
||||
List<GeneratedFormItem> clonedRow = [];
|
||||
for (var it in row) {
|
||||
clonedRow.add(it.clone());
|
||||
}
|
||||
clonedItems.add(clonedRow);
|
||||
}
|
||||
return clonedItems;
|
||||
}
|
||||
|
||||
class GeneratedFormSubForm extends GeneratedFormItem {
|
||||
final List<List<GeneratedFormItem>> items;
|
||||
|
||||
GeneratedFormSubForm(super.key, this.items,
|
||||
{super.label, super.belowWidgets, super.defaultValue = const []});
|
||||
|
||||
@override
|
||||
ensureType(val) {
|
||||
return val; // Not easy to validate List<Map<String, dynamic>>
|
||||
}
|
||||
|
||||
@override
|
||||
GeneratedFormSubForm clone() {
|
||||
return GeneratedFormSubForm(key, cloneFormItems(items),
|
||||
label: label, belowWidgets: belowWidgets, defaultValue: defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a color in the HSLuv (Pastel) color space
|
||||
// https://pub.dev/documentation/hsluv/latest/hsluv/Hsluv/hpluvToRgb.html
|
||||
Color generateRandomLightColor() {
|
||||
final randomSeed = Random().nextInt(120);
|
||||
// https://en.wikipedia.org/wiki/Golden_angle
|
||||
final goldenAngle = 180 * (3 - sqrt(5));
|
||||
// Generate next golden angle hue
|
||||
final double hue = randomSeed * goldenAngle;
|
||||
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
|
||||
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
|
||||
// Map RBG values from 0-1 to 0-255:
|
||||
final List<int> rgbValues =
|
||||
rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
|
||||
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
|
||||
}
|
||||
|
||||
int generateRandomNumber(int seed1,
|
||||
{int seed2 = 0, int seed3 = 0, max = 10000}) {
|
||||
int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode;
|
||||
Random random = Random(combinedSeed);
|
||||
int randomNumber = random.nextInt(max);
|
||||
return randomNumber;
|
||||
}
|
||||
|
||||
bool validateTextField(TextFormField tf) =>
|
||||
(tf.key as GlobalKey<FormFieldState>).currentState?.isValid == true;
|
||||
|
||||
class _GeneratedFormState extends State<GeneratedForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
Map<String, dynamic> values = {};
|
||||
late List<List<Widget>> formInputs;
|
||||
List<List<Widget>> rows = [];
|
||||
String? initKey;
|
||||
int forceUpdateKeyCount = 0;
|
||||
|
||||
// If any value changes, call this to update the parent with value and validity
|
||||
void someValueChanged({bool isBuilding = false}) {
|
||||
void someValueChanged({bool isBuilding = false, bool forceInvalid = false}) {
|
||||
Map<String, dynamic> returnValues = values;
|
||||
var valid = true;
|
||||
for (int r = 0; r < widget.items.length; r++) {
|
||||
for (int i = 0; i < widget.items[r].length; i++) {
|
||||
for (int r = 0; r < formInputs.length; r++) {
|
||||
for (int i = 0; i < formInputs[r].length; i++) {
|
||||
if (formInputs[r][i] is TextFormField) {
|
||||
var fieldState =
|
||||
(formInputs[r][i].key as GlobalKey<FormFieldState>).currentState;
|
||||
if (fieldState != null) {
|
||||
valid = valid && fieldState.isValid;
|
||||
valid = valid && validateTextField(formInputs[r][i] as TextFormField);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (forceInvalid) {
|
||||
valid = false;
|
||||
}
|
||||
widget.onValueChanges(returnValues, valid, isBuilding);
|
||||
}
|
||||
@@ -189,6 +275,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
if (formItem is GeneratedFormTextField) {
|
||||
final formFieldKey = GlobalKey<FormFieldState>();
|
||||
return TextFormField(
|
||||
keyboardType: formItem.textInputType,
|
||||
obscureText: formItem.password,
|
||||
autocorrect: !formItem.password,
|
||||
enableSuggestions: !formItem.password,
|
||||
@@ -242,6 +329,17 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
someValueChanged();
|
||||
});
|
||||
});
|
||||
} else if (formItem is GeneratedFormSubForm) {
|
||||
values[formItem.key] = [];
|
||||
for (Map<String, dynamic> v
|
||||
in ((formItem.defaultValue ?? []) as List<dynamic>)) {
|
||||
var fullDefaults = getDefaultValuesFromFormItems(formItem.items);
|
||||
for (var element in v.entries) {
|
||||
fullDefaults[element.key] = element.value;
|
||||
}
|
||||
values[formItem.key].add(fullDefaults);
|
||||
}
|
||||
return Container();
|
||||
} else {
|
||||
return Container(); // Some input types added in build
|
||||
}
|
||||
@@ -263,6 +361,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
}
|
||||
for (var r = 0; r < formInputs.length; r++) {
|
||||
for (var e = 0; e < formInputs[r].length; e++) {
|
||||
String fieldKey = widget.items[r][e].key;
|
||||
if (widget.items[r][e] is GeneratedFormSwitch) {
|
||||
formInputs[r][e] = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -272,20 +371,55 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
width: 8,
|
||||
),
|
||||
Switch(
|
||||
value: values[widget.items[r][e].key],
|
||||
onChanged: (value) {
|
||||
value: values[fieldKey],
|
||||
onChanged:
|
||||
(widget.items[r][e] as GeneratedFormSwitch).disabled
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
values[widget.items[r][e].key] = value;
|
||||
values[fieldKey] = value;
|
||||
someValueChanged();
|
||||
});
|
||||
})
|
||||
],
|
||||
);
|
||||
} else if (widget.items[r][e] is GeneratedFormTagInput) {
|
||||
onAddPressed() {
|
||||
showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: widget.items[r][e].label,
|
||||
items: [
|
||||
[GeneratedFormTextField('label', label: tr('label'))]
|
||||
]);
|
||||
}).then((value) {
|
||||
String? label = value?['label'];
|
||||
if (label != null) {
|
||||
setState(() {
|
||||
var temp =
|
||||
values[fieldKey] as Map<String, MapEntry<int, bool>>?;
|
||||
temp ??= {};
|
||||
if (temp[label] == null) {
|
||||
var singleSelect =
|
||||
(widget.items[r][e] as GeneratedFormTagInput)
|
||||
.singleSelect;
|
||||
var someSelected = temp.entries
|
||||
.where((element) => element.value.value)
|
||||
.isNotEmpty;
|
||||
temp[label] = MapEntry(generateRandomLightColor().value,
|
||||
!(someSelected && singleSelect));
|
||||
values[fieldKey] = temp;
|
||||
someValueChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formInputs[r][e] =
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
if ((values[widget.items[r][e].key]
|
||||
as Map<String, MapEntry<int, bool>>?)
|
||||
if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||
?.isNotEmpty ==
|
||||
true &&
|
||||
(widget.items[r][e] as GeneratedFormTagInput)
|
||||
@@ -308,17 +442,15 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
(widget.items[r][e] as GeneratedFormTagInput).alignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
(values[widget.items[r][e].key]
|
||||
as Map<String, MapEntry<int, bool>>?)
|
||||
?.isEmpty ==
|
||||
true
|
||||
? Text(
|
||||
(widget.items[r][e] as GeneratedFormTagInput)
|
||||
.emptyMessage,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
...(values[widget.items[r][e].key]
|
||||
as Map<String, MapEntry<int, bool>>?)
|
||||
// (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||
// ?.isEmpty ==
|
||||
// true
|
||||
// ? Text(
|
||||
// (widget.items[r][e] as GeneratedFormTagInput)
|
||||
// .emptyMessage,
|
||||
// )
|
||||
// : const SizedBox.shrink(),
|
||||
...(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||
?.entries
|
||||
.map((e2) {
|
||||
return Padding(
|
||||
@@ -331,11 +463,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
selected: e2.value.value,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
(values[widget.items[r][e].key] as Map<String,
|
||||
(values[fieldKey] as Map<String,
|
||||
MapEntry<int, bool>>)[e2.key] =
|
||||
MapEntry(
|
||||
(values[widget.items[r][e].key] as Map<
|
||||
String,
|
||||
(values[fieldKey] as Map<String,
|
||||
MapEntry<int, bool>>)[e2.key]!
|
||||
.key,
|
||||
value);
|
||||
@@ -343,20 +474,16 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
as GeneratedFormTagInput)
|
||||
.singleSelect &&
|
||||
value == true) {
|
||||
for (var key in (values[
|
||||
widget.items[r][e].key]
|
||||
for (var key in (values[fieldKey]
|
||||
as Map<String, MapEntry<int, bool>>)
|
||||
.keys) {
|
||||
if (key != e2.key) {
|
||||
(values[widget.items[r][e].key] as Map<
|
||||
String,
|
||||
MapEntry<int, bool>>)[key] =
|
||||
MapEntry(
|
||||
(values[widget.items[r][e].key]
|
||||
as Map<
|
||||
(values[fieldKey] as Map<
|
||||
String,
|
||||
MapEntry<int,
|
||||
bool>>)[key]!
|
||||
bool>>)[key] = MapEntry(
|
||||
(values[fieldKey] as Map<String,
|
||||
MapEntry<int, bool>>)[key]!
|
||||
.key,
|
||||
false);
|
||||
}
|
||||
@@ -368,8 +495,39 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
));
|
||||
}) ??
|
||||
[const SizedBox.shrink()],
|
||||
(values[widget.items[r][e].key]
|
||||
as Map<String, MapEntry<int, bool>>?)
|
||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||
?.values
|
||||
.where((e) => e.value)
|
||||
.length ==
|
||||
1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
var temp = values[fieldKey]
|
||||
as Map<String, MapEntry<int, bool>>;
|
||||
// get selected category str where bool is true
|
||||
final oldEntry = temp.entries
|
||||
.firstWhere((entry) => entry.value.value);
|
||||
// generate new color, ensure it is not the same
|
||||
int newColor = oldEntry.value.key;
|
||||
while (oldEntry.value.key == newColor) {
|
||||
newColor = generateRandomLightColor().value;
|
||||
}
|
||||
// Update entry with new color, remain selected
|
||||
temp.update(oldEntry.key,
|
||||
(old) => MapEntry(newColor, old.value));
|
||||
values[fieldKey] = temp;
|
||||
someValueChanged();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.format_color_fill_rounded),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: tr('colour'),
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||
?.values
|
||||
.where((e) => e.value)
|
||||
.isNotEmpty ==
|
||||
@@ -380,10 +538,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
onPressed: () {
|
||||
fn() {
|
||||
setState(() {
|
||||
var temp = values[widget.items[r][e].key]
|
||||
var temp = values[fieldKey]
|
||||
as Map<String, MapEntry<int, bool>>;
|
||||
temp.removeWhere((key, value) => value.value);
|
||||
values[widget.items[r][e].key] = temp;
|
||||
values[fieldKey] = temp;
|
||||
someValueChanged();
|
||||
});
|
||||
}
|
||||
@@ -415,45 +573,22 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
tooltip: tr('remove'),
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
Padding(
|
||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||
?.isEmpty ==
|
||||
true
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: TextButton.icon(
|
||||
onPressed: onAddPressed,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(
|
||||
(widget.items[r][e] as GeneratedFormTagInput)
|
||||
.label),
|
||||
))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: widget.items[r][e].label,
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('label',
|
||||
label: tr('label'))
|
||||
]
|
||||
]);
|
||||
}).then((value) {
|
||||
String? label = value?['label'];
|
||||
if (label != null) {
|
||||
setState(() {
|
||||
var temp = values[widget.items[r][e].key]
|
||||
as Map<String, MapEntry<int, bool>>?;
|
||||
temp ??= {};
|
||||
if (temp[label] == null) {
|
||||
var singleSelect = (widget.items[r][e]
|
||||
as GeneratedFormTagInput)
|
||||
.singleSelect;
|
||||
var someSelected = temp.entries
|
||||
.where((element) => element.value.value)
|
||||
.isNotEmpty;
|
||||
temp[label] = MapEntry(
|
||||
generateRandomLightColor().value,
|
||||
!(someSelected && singleSelect));
|
||||
values[widget.items[r][e].key] = temp;
|
||||
someValueChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onPressed: onAddPressed,
|
||||
icon: const Icon(Icons.add),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: tr('add'),
|
||||
@@ -461,6 +596,97 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
],
|
||||
)
|
||||
]);
|
||||
} else if (widget.items[r][e] is GeneratedFormSubForm) {
|
||||
List<Widget> subformColumn = [];
|
||||
var compact = (widget.items[r][e] as GeneratedFormSubForm)
|
||||
.items
|
||||
.length ==
|
||||
1 &&
|
||||
(widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1;
|
||||
for (int i = 0; i < values[fieldKey].length; i++) {
|
||||
var internalFormKey = ValueKey(generateRandomNumber(
|
||||
values[fieldKey].length,
|
||||
seed2: i,
|
||||
seed3: forceUpdateKeyCount));
|
||||
subformColumn.add(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!compact)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (!compact)
|
||||
Text(
|
||||
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
GeneratedForm(
|
||||
key: internalFormKey,
|
||||
items: cloneFormItems(
|
||||
(widget.items[r][e] as GeneratedFormSubForm).items)
|
||||
.map((x) => x.map((y) {
|
||||
y.defaultValue = values[fieldKey]?[i]?[y.key];
|
||||
y.key = '${y.key.toString()},$internalFormKey';
|
||||
return y;
|
||||
}).toList())
|
||||
.toList(),
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
values = values.map(
|
||||
(key, value) => MapEntry(key.split(',')[0], value));
|
||||
if (valid) {
|
||||
this.values[fieldKey]?[i] = values;
|
||||
}
|
||||
someValueChanged(
|
||||
isBuilding: isBuilding, forceInvalid: !valid);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.error),
|
||||
onPressed: (values[fieldKey].length > 0)
|
||||
? () {
|
||||
var temp = List.from(values[fieldKey]);
|
||||
temp.removeAt(i);
|
||||
values[fieldKey] = List.from(temp);
|
||||
forceUpdateKeyCount++;
|
||||
someValueChanged();
|
||||
}
|
||||
: null,
|
||||
label: Text(
|
||||
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
subformColumn.add(Padding(
|
||||
padding: const EdgeInsets.only(bottom: 0, top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
values[fieldKey].add(getDefaultValuesFromFormItems(
|
||||
(widget.items[r][e] as GeneratedFormSubForm)
|
||||
.items));
|
||||
forceUpdateKeyCount++;
|
||||
someValueChanged();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text((widget.items[r][e] as GeneratedFormSubForm)
|
||||
.label))),
|
||||
],
|
||||
),
|
||||
));
|
||||
formInputs[r][e] = Column(children: subformColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ class GeneratedFormModal extends StatefulWidget {
|
||||
this.initValid = false,
|
||||
this.message = '',
|
||||
this.additionalWidgets = const [],
|
||||
this.singleNullReturnButton});
|
||||
this.singleNullReturnButton,
|
||||
this.primaryActionColour});
|
||||
|
||||
final String title;
|
||||
final String message;
|
||||
@@ -19,6 +20,7 @@ class GeneratedFormModal extends StatefulWidget {
|
||||
final bool initValid;
|
||||
final List<Widget> additionalWidgets;
|
||||
final String? singleNullReturnButton;
|
||||
final Color? primaryActionColour;
|
||||
|
||||
@override
|
||||
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
||||
@@ -71,6 +73,10 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
||||
: widget.singleNullReturnButton!)),
|
||||
widget.singleNullReturnButton == null
|
||||
? TextButton(
|
||||
style: widget.primaryActionColour == null
|
||||
? null
|
||||
: TextButton.styleFrom(
|
||||
foregroundColor: widget.primaryActionColour),
|
||||
onPressed: !valid
|
||||
? null
|
||||
: () {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:android_package_installer/android_package_installer.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/providers/logs_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -24,8 +28,15 @@ class InvalidURLError extends ObtainiumError {
|
||||
: super(tr('invalidURLForSource', args: [sourceName]));
|
||||
}
|
||||
|
||||
class CredsNeededError extends ObtainiumError {
|
||||
CredsNeededError(String sourceName)
|
||||
: super(tr('requiresCredentialsInSettings', args: [sourceName]));
|
||||
}
|
||||
|
||||
class NoReleasesError extends ObtainiumError {
|
||||
NoReleasesError() : super(tr('noReleaseFound'));
|
||||
NoReleasesError({String? note})
|
||||
: super(
|
||||
'${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}');
|
||||
}
|
||||
|
||||
class NoAPKError extends ObtainiumError {
|
||||
@@ -44,8 +55,13 @@ class DowngradeError extends ObtainiumError {
|
||||
DowngradeError() : super(tr('cantInstallOlderVersion'));
|
||||
}
|
||||
|
||||
class InstallError extends ObtainiumError {
|
||||
InstallError(int code)
|
||||
: super(PackageInstallerStatus.byCode(code).name.substring(7));
|
||||
}
|
||||
|
||||
class IDChangedError extends ObtainiumError {
|
||||
IDChangedError() : super(tr('appIdMismatch'));
|
||||
IDChangedError(String newId) : super('${tr('appIdMismatch')} - $newId');
|
||||
}
|
||||
|
||||
class NotImplementedError extends ObtainiumError {
|
||||
@@ -53,30 +69,43 @@ class NotImplementedError extends ObtainiumError {
|
||||
}
|
||||
|
||||
class MultiAppMultiError extends ObtainiumError {
|
||||
Map<String, List<String>> content = {};
|
||||
Map<String, dynamic> rawErrors = {};
|
||||
Map<String, List<String>> idsByErrorString = {};
|
||||
Map<String, String> appIdNames = {};
|
||||
|
||||
MultiAppMultiError() : super(tr('placeholder'), unexpected: true);
|
||||
|
||||
add(String appId, String string) {
|
||||
var tempIds = content.remove(string);
|
||||
add(String appId, dynamic error, {String? appName}) {
|
||||
if (error is SocketException) {
|
||||
error = error.message;
|
||||
}
|
||||
rawErrors[appId] = error;
|
||||
var string = error.toString();
|
||||
var tempIds = idsByErrorString.remove(string);
|
||||
tempIds ??= [];
|
||||
tempIds.add(appId);
|
||||
content.putIfAbsent(string, () => tempIds!);
|
||||
idsByErrorString.putIfAbsent(string, () => tempIds!);
|
||||
if (appName != null) {
|
||||
appIdNames[appId] = appName;
|
||||
}
|
||||
}
|
||||
|
||||
String errorString(String appId, {bool includeIdsWithNames = false}) =>
|
||||
'${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}';
|
||||
|
||||
String errorsAppsString(String errString, List<String> appIds,
|
||||
{bool includeIdsWithNames = false}) =>
|
||||
'$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
String finalString = '';
|
||||
for (var e in content.keys) {
|
||||
finalString += '$e: ${content[e].toString()}\n\n';
|
||||
}
|
||||
return finalString;
|
||||
}
|
||||
String toString() => idsByErrorString.entries
|
||||
.map((e) => errorsAppsString(e.key, e.value))
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
showError(dynamic e, BuildContext context) {
|
||||
showMessage(dynamic e, BuildContext context, {bool isError = false}) {
|
||||
Provider.of<LogsProvider>(context, listen: false)
|
||||
.add(e.toString(), level: LogLevels.error);
|
||||
.add(e.toString(), level: isError ? LogLevels.error : LogLevels.info);
|
||||
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
@@ -88,9 +117,16 @@ showError(dynamic e, BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(e is MultiAppMultiError
|
||||
? tr('someErrors')
|
||||
: tr('unexpectedError')),
|
||||
content: Text(e.toString()),
|
||||
? tr(isError ? 'someErrors' : 'updates')
|
||||
: tr(isError ? 'unexpectedError' : 'unknown')),
|
||||
content: GestureDetector(
|
||||
onLongPress: () {
|
||||
Clipboard.setData(ClipboardData(text: e.toString()));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(tr('copiedToClipboard')),
|
||||
));
|
||||
},
|
||||
child: Text(e.toString())),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -103,6 +139,10 @@ showError(dynamic e, BuildContext context) {
|
||||
}
|
||||
}
|
||||
|
||||
showError(dynamic e, BuildContext context) {
|
||||
showMessage(e, context, isError: true);
|
||||
}
|
||||
|
||||
String list2FriendlyString(List<String> list) {
|
||||
return list.length == 2
|
||||
? '${list[0]} ${tr('and')} ${list[1]}'
|
||||
|
||||
225
lib/main.dart
@@ -1,12 +1,11 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/pages/home.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/logs_provider.dart';
|
||||
import 'package:obtainium/providers/native_provider.dart';
|
||||
import 'package:obtainium/providers/notifications_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
@@ -14,31 +13,37 @@ import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
||||
import 'package:background_fetch/background_fetch.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:easy_localization/src/easy_localization_controller.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:easy_localization/src/localization.dart';
|
||||
|
||||
const String currentVersion = '0.11.33';
|
||||
const String currentReleaseTag =
|
||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||
|
||||
const int bgUpdateCheckAlarmId = 666;
|
||||
|
||||
const supportedLocales = [
|
||||
Locale('en'),
|
||||
Locale('zh'),
|
||||
Locale('it'),
|
||||
Locale('ja'),
|
||||
Locale('hu'),
|
||||
Locale('de'),
|
||||
Locale('fa'),
|
||||
Locale('fr')
|
||||
List<MapEntry<Locale, String>> supportedLocales = const [
|
||||
MapEntry(Locale('en'), 'English'),
|
||||
MapEntry(Locale('zh'), '简体中文'),
|
||||
MapEntry(Locale('it'), 'Italiano'),
|
||||
MapEntry(Locale('ja'), '日本語'),
|
||||
MapEntry(Locale('hu'), 'Magyar'),
|
||||
MapEntry(Locale('de'), 'Deutsch'),
|
||||
MapEntry(Locale('fa'), 'فارسی'),
|
||||
MapEntry(Locale('fr'), 'Français'),
|
||||
MapEntry(Locale('es'), 'Español'),
|
||||
MapEntry(Locale('pl'), 'Polski'),
|
||||
MapEntry(Locale('ru'), 'Русский'),
|
||||
MapEntry(Locale('bs'), 'Bosanski'),
|
||||
MapEntry(Locale('pt'), 'Brasileiro'),
|
||||
MapEntry(Locale('cs'), 'Česky'),
|
||||
MapEntry(Locale('sv'), 'Svenska'),
|
||||
MapEntry(Locale('nl'), 'Nederlands'),
|
||||
MapEntry(Locale('vi'), 'Tiếng Việt'),
|
||||
MapEntry(Locale('tr'), 'Türkçe'),
|
||||
MapEntry(Locale('uk'), 'Українська'),
|
||||
];
|
||||
const fallbackLocale = Locale('en');
|
||||
const localeDir = 'assets/translations';
|
||||
var fdroid = false;
|
||||
|
||||
final globalNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@@ -52,7 +57,7 @@ Future<void> loadTranslations() async {
|
||||
saveLocale: true,
|
||||
forceLocale: forceLocale != null ? Locale(forceLocale) : null,
|
||||
fallbackLocale: fallbackLocale,
|
||||
supportedLocales: supportedLocales,
|
||||
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||
assetLoader: const RootBundleAssetLoader(),
|
||||
useOnlyLangCode: true,
|
||||
useFallbackTranslations: true,
|
||||
@@ -68,81 +73,16 @@ Future<void> loadTranslations() async {
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
|
||||
await loadTranslations();
|
||||
|
||||
LogsProvider logs = LogsProvider();
|
||||
logs.add(tr('startedBgUpdateTask'));
|
||||
int? ignoreAfterMicroseconds = params?['ignoreAfterMicroseconds'];
|
||||
await AndroidAlarmManager.initialize();
|
||||
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
||||
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
||||
: null;
|
||||
logs.add(tr('bgUpdateIgnoreAfterIs', args: [ignoreAfter.toString()]));
|
||||
var notificationsProvider = NotificationsProvider();
|
||||
await notificationsProvider.notify(checkingUpdatesNotification);
|
||||
try {
|
||||
var appsProvider = AppsProvider();
|
||||
await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
|
||||
await appsProvider.loadApps();
|
||||
List<String> existingUpdateIds =
|
||||
appsProvider.findExistingUpdates(installedOnly: true);
|
||||
DateTime nextIgnoreAfter = DateTime.now();
|
||||
String? err;
|
||||
try {
|
||||
logs.add(tr('startedActualBGUpdateCheck'));
|
||||
await appsProvider.checkUpdates(
|
||||
ignoreAppsCheckedAfter: ignoreAfter, throwErrorsForRetry: true);
|
||||
} catch (e) {
|
||||
if (e is RateLimitError || e is SocketException) {
|
||||
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
|
||||
logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes,
|
||||
args: [e.toString(), remainingMinutes.toString()]));
|
||||
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
|
||||
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
|
||||
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
||||
});
|
||||
} else {
|
||||
err = e.toString();
|
||||
}
|
||||
}
|
||||
List<App> newUpdates = appsProvider
|
||||
.findExistingUpdates(installedOnly: true)
|
||||
.where((id) => !existingUpdateIds.contains(id))
|
||||
.map((e) => appsProvider.apps[e]!.app)
|
||||
.toList();
|
||||
|
||||
// TODO: This silent update code doesn't work yet
|
||||
// List<String> silentlyUpdated = await appsProvider
|
||||
// .downloadAndInstallLatestApp(
|
||||
// [...newUpdates.map((e) => e.id), ...existingUpdateIds], null);
|
||||
// if (silentlyUpdated.isNotEmpty) {
|
||||
// newUpdates = newUpdates
|
||||
// .where((element) => !silentlyUpdated.contains(element.id))
|
||||
// .toList();
|
||||
// notificationsProvider.notify(
|
||||
// SilentUpdateNotification(
|
||||
// silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()),
|
||||
// cancelExisting: true);
|
||||
// }
|
||||
logs.add(
|
||||
plural('bgCheckFoundUpdatesWillNotifyIfNeeded', newUpdates.length));
|
||||
if (newUpdates.isNotEmpty) {
|
||||
notificationsProvider.notify(UpdateNotification(newUpdates));
|
||||
}
|
||||
if (err != null) {
|
||||
throw err;
|
||||
}
|
||||
} catch (e) {
|
||||
notificationsProvider
|
||||
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
||||
} finally {
|
||||
logs.add(tr('bgUpdateTaskFinished'));
|
||||
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
||||
void backgroundFetchHeadlessTask(HeadlessTask task) async {
|
||||
String taskId = task.taskId;
|
||||
bool isTimeout = task.timeout;
|
||||
if (isTimeout) {
|
||||
print('BG update task timed out.');
|
||||
BackgroundFetch.finish(taskId);
|
||||
return;
|
||||
}
|
||||
await bgUpdateCheck(taskId, null);
|
||||
BackgroundFetch.finish(taskId);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
@@ -162,7 +102,6 @@ void main() async {
|
||||
);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
}
|
||||
await AndroidAlarmManager.initialize();
|
||||
runApp(MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => AppsProvider()),
|
||||
@@ -171,16 +110,15 @@ void main() async {
|
||||
Provider(create: (context) => LogsProvider())
|
||||
],
|
||||
child: EasyLocalization(
|
||||
supportedLocales: supportedLocales,
|
||||
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||
path: localeDir,
|
||||
fallbackLocale: fallbackLocale,
|
||||
useOnlyLangCode: true,
|
||||
child: const Obtainium()),
|
||||
));
|
||||
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
|
||||
}
|
||||
|
||||
var defaultThemeColour = Colors.deepPurple;
|
||||
|
||||
class Obtainium extends StatefulWidget {
|
||||
const Obtainium({super.key});
|
||||
|
||||
@@ -191,6 +129,33 @@ class Obtainium extends StatefulWidget {
|
||||
class _ObtainiumState extends State<Obtainium> {
|
||||
var existingUpdateInterval = -1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initPlatformState();
|
||||
}
|
||||
|
||||
Future<void> initPlatformState() async {
|
||||
await BackgroundFetch.configure(
|
||||
BackgroundFetchConfig(
|
||||
minimumFetchInterval: 15,
|
||||
stopOnTerminate: false,
|
||||
startOnBoot: true,
|
||||
enableHeadless: true,
|
||||
requiresBatteryNotLow: false,
|
||||
requiresCharging: false,
|
||||
requiresStorageNotLow: false,
|
||||
requiresDeviceIdle: false,
|
||||
requiredNetworkType: NetworkType.ANY), (String taskId) async {
|
||||
await bgUpdateCheck(taskId, null);
|
||||
BackgroundFetch.finish(taskId);
|
||||
}, (String taskId) async {
|
||||
context.read<LogsProvider>().add('BG update task timed out.');
|
||||
BackgroundFetch.finish(taskId);
|
||||
});
|
||||
if (!mounted) return;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||
@@ -202,50 +167,44 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
} else {
|
||||
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
||||
if (isFirstRun) {
|
||||
logs.add(tr('firstRun'));
|
||||
logs.add('This is the first ever run of Obtainium.');
|
||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||
Permission.notification.request();
|
||||
if (!fdroid) {
|
||||
getInstalledInfo(obtainiumId).then((value) {
|
||||
if (value?.versionName != null) {
|
||||
appsProvider.saveApps([
|
||||
App(
|
||||
obtainiumId,
|
||||
'https://github.com/ImranR98/Obtainium',
|
||||
obtainiumUrl,
|
||||
'ImranR98',
|
||||
'Obtainium',
|
||||
currentReleaseTag,
|
||||
currentReleaseTag,
|
||||
value!.versionName,
|
||||
value.versionName!,
|
||||
[],
|
||||
0,
|
||||
{'includePrereleases': true},
|
||||
{
|
||||
'versionDetection': true,
|
||||
'apkFilterRegEx': 'fdroid',
|
||||
'invertAPKFilter': true
|
||||
},
|
||||
null,
|
||||
false)
|
||||
], onlyIfExists: false);
|
||||
}
|
||||
}).catchError((err) {
|
||||
print(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!supportedLocales
|
||||
.map((e) => e.languageCode)
|
||||
.map((e) => e.key.languageCode)
|
||||
.contains(context.locale.languageCode) ||
|
||||
settingsProvider.forcedLocale == null &&
|
||||
(settingsProvider.forcedLocale == null &&
|
||||
context.deviceLocale.languageCode !=
|
||||
context.locale.languageCode) {
|
||||
context.locale.languageCode)) {
|
||||
settingsProvider.resetLocaleSafe(context);
|
||||
}
|
||||
// Register the background update task according to the user's setting
|
||||
if (existingUpdateInterval != settingsProvider.updateInterval) {
|
||||
if (existingUpdateInterval != -1) {
|
||||
logs.add(tr('settingUpdateCheckIntervalTo',
|
||||
args: [settingsProvider.updateInterval.toString()]));
|
||||
}
|
||||
existingUpdateInterval = settingsProvider.updateInterval;
|
||||
if (existingUpdateInterval == 0) {
|
||||
AndroidAlarmManager.cancel(bgUpdateCheckAlarmId);
|
||||
} else {
|
||||
AndroidAlarmManager.periodic(
|
||||
Duration(minutes: existingUpdateInterval),
|
||||
bgUpdateCheckAlarmId,
|
||||
bgUpdateCheck,
|
||||
rescheduleOnReboot: true,
|
||||
wakeup: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DynamicColorBuilder(
|
||||
@@ -253,15 +212,13 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
// Decide on a colour/brightness scheme based on OS and user settings
|
||||
ColorScheme lightColorScheme;
|
||||
ColorScheme darkColorScheme;
|
||||
if (lightDynamic != null &&
|
||||
darkDynamic != null &&
|
||||
settingsProvider.colour == ColourSettings.materialYou) {
|
||||
if (lightDynamic != null && darkDynamic != null && settingsProvider.useMaterialYou) {
|
||||
lightColorScheme = lightDynamic.harmonized();
|
||||
darkColorScheme = darkDynamic.harmonized();
|
||||
} else {
|
||||
lightColorScheme = ColorScheme.fromSeed(seedColor: defaultThemeColour);
|
||||
lightColorScheme = ColorScheme.fromSeed(seedColor: settingsProvider.themeColor);
|
||||
darkColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: defaultThemeColour, brightness: Brightness.dark);
|
||||
seedColor: settingsProvider.themeColor, brightness: Brightness.dark);
|
||||
}
|
||||
|
||||
// set the background and surface colors to pure black in the amoled theme
|
||||
@@ -271,6 +228,8 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
.harmonized();
|
||||
}
|
||||
|
||||
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Obtainium',
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
@@ -282,14 +241,18 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
colorScheme: settingsProvider.theme == ThemeSettings.dark
|
||||
? darkColorScheme
|
||||
: lightColorScheme,
|
||||
fontFamily: 'Metropolis'),
|
||||
fontFamily:
|
||||
settingsProvider.useSystemFont ? 'SystemFont' : 'Metropolis'),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: settingsProvider.theme == ThemeSettings.light
|
||||
? lightColorScheme
|
||||
: darkColorScheme,
|
||||
fontFamily: 'Metropolis'),
|
||||
home: const HomePage());
|
||||
fontFamily:
|
||||
settingsProvider.useSystemFont ? 'SystemFont' : 'Metropolis'),
|
||||
home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||
}, child: const HomePage()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
6
lib/main_fdroid.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'main.dart' as m;
|
||||
|
||||
void main() async {
|
||||
m.fdroid = true;
|
||||
m.main();
|
||||
}
|
||||
@@ -13,17 +13,22 @@ class GitHubStars implements MassAppUrlSource {
|
||||
@override
|
||||
late List<String> requiredArgs = [tr('uname')];
|
||||
|
||||
Future<Map<String, String>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||
String username, int page) async {
|
||||
Response res = await get(Uri.parse(
|
||||
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
||||
Response res = await get(
|
||||
Uri.parse(
|
||||
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
|
||||
headers: await GitHub().getRequestHeaders({}));
|
||||
if (res.statusCode == 200) {
|
||||
Map<String, String> urlsWithDescriptions = {};
|
||||
Map<String, List<String>> urlsWithDescriptions = {};
|
||||
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||
urlsWithDescriptions.addAll({
|
||||
e['html_url'] as String: e['description'] != null
|
||||
e['html_url'] as String: [
|
||||
e['full_name'] as String,
|
||||
e['description'] != null
|
||||
? e['description'] as String
|
||||
: tr('noDescription')
|
||||
]
|
||||
});
|
||||
}
|
||||
return urlsWithDescriptions;
|
||||
@@ -35,11 +40,12 @@ class GitHubStars implements MassAppUrlSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args) async {
|
||||
Future<Map<String, List<String>>> getUrlsWithDescriptions(
|
||||
List<String> args) async {
|
||||
if (args.length != requiredArgs.length) {
|
||||
throw ObtainiumError(tr('wrongArgNum'));
|
||||
}
|
||||
Map<String, String> urlsWithDescriptions = {};
|
||||
Map<String, List<String>> urlsWithDescriptions = {};
|
||||
var page = 1;
|
||||
while (true) {
|
||||
var pageUrls =
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:obtainium/pages/app.dart';
|
||||
import 'package:obtainium/pages/import_export.dart';
|
||||
import 'package:obtainium/pages/settings.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/notifications_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -19,38 +20,53 @@ class AddAppPage extends StatefulWidget {
|
||||
const AddAppPage({super.key});
|
||||
|
||||
@override
|
||||
State<AddAppPage> createState() => _AddAppPageState();
|
||||
State<AddAppPage> createState() => AddAppPageState();
|
||||
}
|
||||
|
||||
class _AddAppPageState extends State<AddAppPage> {
|
||||
class AddAppPageState extends State<AddAppPage> {
|
||||
bool gettingAppInfo = false;
|
||||
bool searching = false;
|
||||
|
||||
String userInput = '';
|
||||
String searchQuery = '';
|
||||
String? pickedSourceOverride;
|
||||
AppSource? pickedSource;
|
||||
Map<String, dynamic> additionalSettings = {};
|
||||
bool additionalSettingsValid = true;
|
||||
bool inferAppIdIfOptional = true;
|
||||
List<String> pickedCategories = [];
|
||||
int searchnum = 0;
|
||||
int urlInputKey = 0;
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||
|
||||
bool doingSomething = gettingAppInfo || searching;
|
||||
linkFn(String input) {
|
||||
try {
|
||||
if (input.isEmpty) {
|
||||
throw UnsupportedURLError();
|
||||
}
|
||||
sourceProvider.getSource(input);
|
||||
changeUserInput(input, true, false, updateUrlInput: true);
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
}
|
||||
}
|
||||
|
||||
changeUserInput(String input, bool valid, bool isBuilding,
|
||||
{bool isSearch = false}) {
|
||||
{bool updateUrlInput = false}) {
|
||||
userInput = input;
|
||||
if (!isBuilding) {
|
||||
setState(() {
|
||||
if (isSearch) {
|
||||
searchnum++;
|
||||
if (updateUrlInput) {
|
||||
urlInputKey++;
|
||||
}
|
||||
var source = valid ? sourceProvider.getSource(userInput) : null;
|
||||
if (pickedSource.runtimeType != source.runtimeType) {
|
||||
var prevHost = pickedSource?.hosts.isNotEmpty == true
|
||||
? pickedSource?.hosts[0]
|
||||
: null;
|
||||
var source = valid
|
||||
? sourceProvider.getSource(userInput,
|
||||
overrideSource: pickedSourceOverride)
|
||||
: null;
|
||||
if (pickedSource.runtimeType != source.runtimeType ||
|
||||
(prevHost != null && prevHost != source?.hosts[0])) {
|
||||
pickedSource = source;
|
||||
additionalSettings = source != null
|
||||
? getDefaultValuesFromFormItems(
|
||||
@@ -59,35 +75,54 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
additionalSettingsValid = source != null
|
||||
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
||||
: true;
|
||||
inferAppIdIfOptional = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly) async {
|
||||
return (!((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
|
||||
bool doingSomething = gettingAppInfo || searching;
|
||||
|
||||
Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
|
||||
{bool ignoreHideSetting = false}) async {
|
||||
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
|
||||
if (useTrackOnly &&
|
||||
(!settingsProvider.hideTrackOnlyWarning || ignoreHideSetting)) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog(
|
||||
var values = await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
initValid: true,
|
||||
title: tr('xIsTrackOnly', args: [
|
||||
pickedSource!.enforceTrackOnly
|
||||
? tr('source')
|
||||
: tr('app')
|
||||
pickedSource!.enforceTrackOnly ? tr('source') : tr('app')
|
||||
]),
|
||||
items: const [],
|
||||
items: [
|
||||
[GeneratedFormSwitch('hide', label: tr('dontShowAgain'))]
|
||||
],
|
||||
message:
|
||||
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
||||
);
|
||||
}) ==
|
||||
null));
|
||||
});
|
||||
if (values != null) {
|
||||
settingsProvider.hideTrackOnlyWarning = values['hide'] == true;
|
||||
}
|
||||
return useTrackOnly && values != null;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getReleaseDateAsVersionConfirmationIfNeeded(
|
||||
bool userPickedTrackOnly) async {
|
||||
return (!(additionalSettings['versionDetection'] ==
|
||||
'releaseDateAsVersion' &&
|
||||
return (!(additionalSettings['releaseDateAsVersion'] == true &&
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog(
|
||||
context: context,
|
||||
@@ -106,7 +141,6 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
gettingAppInfo = true;
|
||||
});
|
||||
try {
|
||||
var settingsProvider = context.read<SettingsProvider>();
|
||||
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
||||
App? app;
|
||||
if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
|
||||
@@ -114,30 +148,38 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
userPickedTrackOnly))) {
|
||||
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||
app = await sourceProvider.getApp(
|
||||
pickedSource!, userInput, additionalSettings,
|
||||
trackOnlyOverride: trackOnly);
|
||||
if (!trackOnly) {
|
||||
await settingsProvider.getInstallPermission();
|
||||
}
|
||||
pickedSource!, userInput.trim(), additionalSettings,
|
||||
trackOnlyOverride: trackOnly,
|
||||
sourceIsOverriden: pickedSourceOverride != null,
|
||||
inferAppIdIfOptional: inferAppIdIfOptional);
|
||||
// Only download the APK here if you need to for the package ID
|
||||
if (sourceProvider.isTempId(app) &&
|
||||
app.additionalSettings['trackOnly'] != true) {
|
||||
if (isTempId(app) && app.additionalSettings['trackOnly'] != true) {
|
||||
// ignore: use_build_context_synchronously
|
||||
var apkUrl = await appsProvider.confirmApkUrl(app, context);
|
||||
var apkUrl =
|
||||
await appsProvider.confirmAppFileUrl(app, context, false);
|
||||
if (apkUrl == null) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
app.preferredApkIndex =
|
||||
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
||||
// ignore: use_build_context_synchronously
|
||||
var downloadedApk = await appsProvider.downloadApp(
|
||||
app, globalNavigatorKey.currentContext);
|
||||
app.id = downloadedApk.appId;
|
||||
var downloadedArtifact = await appsProvider.downloadApp(
|
||||
app, globalNavigatorKey.currentContext,
|
||||
notificationsProvider: notificationsProvider);
|
||||
DownloadedApk? downloadedFile;
|
||||
DownloadedXApkDir? downloadedDir;
|
||||
if (downloadedArtifact is DownloadedApk) {
|
||||
downloadedFile = downloadedArtifact;
|
||||
} else {
|
||||
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||
}
|
||||
app.id = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||
}
|
||||
if (appsProvider.apps.containsKey(app.id)) {
|
||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||
}
|
||||
if (app.additionalSettings['trackOnly'] == true) {
|
||||
if (app.additionalSettings['trackOnly'] == true ||
|
||||
app.additionalSettings['versionDetection'] != true) {
|
||||
app.installedVersion = app.latestVersion;
|
||||
}
|
||||
app.categories = pickedCategories;
|
||||
@@ -163,7 +205,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeneratedForm(
|
||||
key: Key(searchnum.toString()),
|
||||
key: Key(urlInputKey.toString()),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('appSourceURL',
|
||||
@@ -173,9 +215,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
(value) {
|
||||
try {
|
||||
sourceProvider
|
||||
.getSource(value ?? '')
|
||||
.standardizeURL(
|
||||
preStandardizeUrl(value ?? ''));
|
||||
.getSource(value ?? '',
|
||||
overrideSource: pickedSourceOverride)
|
||||
.standardizeUrl(value ?? '');
|
||||
} catch (e) {
|
||||
return e is String
|
||||
? e
|
||||
@@ -212,18 +254,52 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
],
|
||||
);
|
||||
|
||||
runSearch() async {
|
||||
runSearch({bool filtered = true}) async {
|
||||
setState(() {
|
||||
searching = true;
|
||||
});
|
||||
var sourceStrings = <String, List<String>>{};
|
||||
sourceProvider.sources
|
||||
.where((e) => e.canSearch && !e.excludeFromMassSearch)
|
||||
.forEach((s) {
|
||||
sourceStrings[s.name] = [s.name];
|
||||
});
|
||||
try {
|
||||
var searchSources = await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return SelectionModal(
|
||||
title: tr('selectX', args: [plural('source', 2)]),
|
||||
entries: sourceStrings,
|
||||
selectedByDefault: true,
|
||||
onlyOneSelectionAllowed: false,
|
||||
titlesAreLinks: false,
|
||||
deselectThese: settingsProvider.searchDeselected,
|
||||
);
|
||||
}) ??
|
||||
[];
|
||||
if (searchSources.isNotEmpty) {
|
||||
settingsProvider.searchDeselected = sourceStrings.keys
|
||||
.where((s) => !searchSources.contains(s))
|
||||
.toList();
|
||||
var results = await Future.wait(sourceProvider.sources
|
||||
.where((e) => e.canSearch)
|
||||
.map((e) => e.search(searchQuery)));
|
||||
.where((e) => searchSources.contains(e.name))
|
||||
.map((e) async {
|
||||
try {
|
||||
return await e.search(searchQuery);
|
||||
} catch (err) {
|
||||
if (err is! CredsNeededError) {
|
||||
rethrow;
|
||||
} else {
|
||||
err.unexpected = true;
|
||||
showError(err, context);
|
||||
return <String, List<String>>{};
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// .then((results) async {
|
||||
// Interleave results instead of simple reduce
|
||||
Map<String, String> res = {};
|
||||
Map<String, List<String>> res = {};
|
||||
var si = 0;
|
||||
var done = false;
|
||||
while (!done) {
|
||||
@@ -236,20 +312,24 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}
|
||||
si++;
|
||||
}
|
||||
if (res.isEmpty) {
|
||||
throw ObtainiumError(tr('noResults'));
|
||||
}
|
||||
List<String>? selectedUrls = res.isEmpty
|
||||
? []
|
||||
// ignore: use_build_context_synchronously
|
||||
: await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions: res,
|
||||
return SelectionModal(
|
||||
entries: res,
|
||||
selectedByDefault: false,
|
||||
onlyOneSelectionAllowed: true,
|
||||
);
|
||||
});
|
||||
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
||||
changeUserInput(selectedUrls[0], true, false, isSearch: true);
|
||||
changeUserInput(selectedUrls[0], true, false, updateUrlInput: true);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
@@ -260,6 +340,49 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget getHTMLSourceOverrideDropdown() => Column(children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeneratedForm(
|
||||
items: [
|
||||
[
|
||||
GeneratedFormDropdown(
|
||||
'overrideSource',
|
||||
defaultValue: '',
|
||||
[
|
||||
MapEntry('', tr('none')),
|
||||
...sourceProvider.sources.map(
|
||||
(s) => MapEntry(s.runtimeType.toString(), s.name))
|
||||
],
|
||||
label: tr('overrideSource'))
|
||||
]
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
fn() {
|
||||
pickedSourceOverride = (values['overrideSource'] == null ||
|
||||
values['overrideSource'] == '')
|
||||
? null
|
||||
: values['overrideSource'];
|
||||
}
|
||||
|
||||
if (!isBuilding) {
|
||||
setState(() {
|
||||
fn();
|
||||
});
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
changeUserInput(userInput, valid, isBuilding);
|
||||
},
|
||||
))
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
]);
|
||||
|
||||
bool shouldShowSearchBar() =>
|
||||
sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
|
||||
pickedSource == null &&
|
||||
@@ -286,7 +409,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
ElevatedButton(
|
||||
searching
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: searchQuery.isEmpty || doingSomething
|
||||
? null
|
||||
: () {
|
||||
@@ -299,19 +424,27 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
Widget getAdditionalOptsCol() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Divider(
|
||||
height: 64,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
tr('additionalOptsFor',
|
||||
args: [pickedSource?.name ?? tr('source')]),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
GeneratedForm(
|
||||
key: Key(pickedSource.runtimeType.toString()),
|
||||
items: pickedSource!.combinedAppSpecificSettingFormItems,
|
||||
items: [
|
||||
...pickedSource!.combinedAppSpecificSettingFormItems,
|
||||
...(pickedSourceOverride != null
|
||||
? pickedSource!.sourceConfigSettingFormItems
|
||||
.map((e) => [e])
|
||||
: [])
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
if (!isBuilding) {
|
||||
setState(() {
|
||||
@@ -332,43 +465,94 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}),
|
||||
],
|
||||
),
|
||||
if (pickedSource != null && pickedSource!.appIdInferIsOptional)
|
||||
GeneratedForm(
|
||||
key: const Key('inferAppIdIfOptional'),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormSwitch('inferAppIdIfOptional',
|
||||
label: tr('tryInferAppIdFromCode'),
|
||||
defaultValue: inferAppIdIfOptional)
|
||||
]
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
if (!isBuilding) {
|
||||
setState(() {
|
||||
inferAppIdIfOptional = values['inferAppIdIfOptional'];
|
||||
});
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
Widget getSourcesListWidget() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
Widget getSourcesListWidget() => Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
Text(
|
||||
tr('supportedSourcesBelow'),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
...sourceProvider.sources
|
||||
.map((e) => GestureDetector(
|
||||
onTap: e.host != null
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return GeneratedFormModal(
|
||||
singleNullReturnButton: tr('ok'),
|
||||
title: tr('supportedSources'),
|
||||
items: const [],
|
||||
additionalWidgets: [
|
||||
...sourceProvider.sources.map(
|
||||
(e) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4),
|
||||
child: GestureDetector(
|
||||
onTap: e.hosts.isNotEmpty
|
||||
? () {
|
||||
launchUrlString('https://${e.host}',
|
||||
mode: LaunchMode.externalApplication);
|
||||
launchUrlString(
|
||||
'https://${e.hosts[0]}',
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
||||
style: TextStyle(
|
||||
decoration: e.host != null
|
||||
decoration: e.hosts.isNotEmpty
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none,
|
||||
: TextDecoration.none),
|
||||
))),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
'${tr('note')}:',
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(tr('selfHostedNote',
|
||||
args: [tr('overrideSource')])),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
tr('supportedSources'),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.underline,
|
||||
fontStyle: FontStyle.italic),
|
||||
)))
|
||||
.toList()
|
||||
]);
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
bottomNavigationBar:
|
||||
pickedSource == null ? getSourcesListWidget() : null,
|
||||
body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[
|
||||
CustomAppBar(title: tr('addApp')),
|
||||
SliverToBoxAdapter(
|
||||
@@ -379,18 +563,24 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
getUrlInputRow(),
|
||||
if (shouldShowSearchBar())
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (pickedSource != null) getHTMLSourceOverrideDropdown(),
|
||||
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||
if (pickedSource != null)
|
||||
getAdditionalOptsCol()
|
||||
else
|
||||
getSourcesListWidget(),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
FutureBuilder(
|
||||
builder: (ctx, val) {
|
||||
return val.data != null && val.data!.isNotEmpty
|
||||
? Text(
|
||||
val.data!,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
future: pickedSource?.getSourceNote()),
|
||||
if (pickedSource != null) getAdditionalOptsCol(),
|
||||
])),
|
||||
)
|
||||
]));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/main.dart';
|
||||
import 'package:obtainium/pages/apps.dart';
|
||||
import 'package:obtainium/pages/settings.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
@@ -24,36 +24,235 @@ class AppPage extends StatefulWidget {
|
||||
|
||||
class _AppPageState extends State<AppPage> {
|
||||
AppInMemory? prevApp;
|
||||
bool updating = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var appsProvider = context.watch<AppsProvider>();
|
||||
var settingsProvider = context.watch<SettingsProvider>();
|
||||
getUpdate(String id) {
|
||||
appsProvider.checkUpdate(id).catchError((e) {
|
||||
showError(e, context);
|
||||
getUpdate(String id, {bool resetVersion = false}) async {
|
||||
try {
|
||||
setState(() {
|
||||
updating = true;
|
||||
});
|
||||
await appsProvider.checkUpdate(id);
|
||||
if (resetVersion) {
|
||||
appsProvider.apps[id]?.app.additionalSettings['versionDetection'] =
|
||||
true;
|
||||
if (appsProvider.apps[id]?.app.installedVersion != null) {
|
||||
appsProvider.apps[id]?.app.installedVersion =
|
||||
appsProvider.apps[id]?.app.latestVersion;
|
||||
}
|
||||
appsProvider.saveApps([appsProvider.apps[id]!.app]);
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(err, context);
|
||||
} finally {
|
||||
setState(() {
|
||||
updating = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool areDownloadsRunning = appsProvider.areDownloadsRunning();
|
||||
|
||||
var sourceProvider = SourceProvider();
|
||||
AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy();
|
||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
||||
if (!areDownloadsRunning && prevApp == null && app != null) {
|
||||
var source = app != null
|
||||
? sourceProvider.getSource(app.app.url,
|
||||
overrideSource: app.app.overrideSource)
|
||||
: null;
|
||||
if (!areDownloadsRunning &&
|
||||
prevApp == null &&
|
||||
app != null &&
|
||||
settingsProvider.checkUpdateOnDetailPage) {
|
||||
prevApp = app;
|
||||
getUpdate(app.app.id);
|
||||
}
|
||||
var trackOnly = app?.app.additionalSettings['trackOnly'] == true;
|
||||
|
||||
bool isVersionDetectionStandard =
|
||||
app?.app.additionalSettings['versionDetection'] ==
|
||||
'standardVersionDetection';
|
||||
app?.app.additionalSettings['versionDetection'] == true;
|
||||
|
||||
getInfoColumn() => Column(
|
||||
bool installedVersionIsEstimate = trackOnly ||
|
||||
(app?.app.installedVersion != null &&
|
||||
app?.app.additionalSettings['versionDetection'] != true);
|
||||
|
||||
getInfoColumn() {
|
||||
String versionLines = '';
|
||||
bool installed = app?.app.installedVersion != null;
|
||||
bool upToDate = app?.app.installedVersion == app?.app.latestVersion;
|
||||
if (installed) {
|
||||
versionLines = '${app?.app.installedVersion} ${tr('installed')}';
|
||||
if (upToDate) {
|
||||
versionLines += '/${tr('latest')}';
|
||||
}
|
||||
} else {
|
||||
versionLines = tr('notInstalled');
|
||||
}
|
||||
if (!upToDate) {
|
||||
versionLines += '\n${app?.app.latestVersion} ${tr('latest')}';
|
||||
}
|
||||
String infoLines = tr('lastUpdateCheckX', args: [
|
||||
app?.app.lastUpdateCheck == null
|
||||
? tr('never')
|
||||
: '${app?.app.lastUpdateCheck?.toLocal()}'
|
||||
]);
|
||||
if (trackOnly) {
|
||||
infoLines = '${tr('xIsTrackOnly', args: [tr('app')])}\n$infoLines';
|
||||
}
|
||||
if (installedVersionIsEstimate) {
|
||||
infoLines = '${tr('pseudoVersionInUse')}\n$infoLines';
|
||||
}
|
||||
if ((app?.app.apkUrls.length ?? 0) > 0) {
|
||||
infoLines =
|
||||
'$infoLines\n${app?.app.apkUrls.length == 1 ? app?.app.apkUrls[0].key : plural('apk', app?.app.apkUrls.length ?? 0)}';
|
||||
}
|
||||
var changeLogFn = app != null ? getChangeLogFn(context, app.app) : null;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Text(versionLines,
|
||||
textAlign: TextAlign.start,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.copyWith(fontWeight: FontWeight.bold)),
|
||||
changeLogFn != null || app?.app.releaseDate != null
|
||||
? GestureDetector(
|
||||
onTap: changeLogFn,
|
||||
child: Text(
|
||||
app?.app.releaseDate == null
|
||||
? tr('changes')
|
||||
: app!.app.releaseDate.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||
decoration: changeLogFn != null
|
||||
? TextDecoration.underline
|
||||
: null,
|
||||
fontStyle: changeLogFn != null
|
||||
? FontStyle.italic
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
infoLines,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||
),
|
||||
if (app?.app.apkUrls.isNotEmpty == true ||
|
||||
app?.app.otherAssetUrls.isNotEmpty == true)
|
||||
GestureDetector(
|
||||
onTap: app?.app == null || updating
|
||||
? null
|
||||
: () async {
|
||||
try {
|
||||
await appsProvider
|
||||
.downloadAppAssets([app!.app.id], context);
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
tr('downloadX', args: [tr('releaseAsset').toLowerCase()]),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||
decoration:
|
||||
changeLogFn != null ? TextDecoration.underline : null,
|
||||
fontStyle: changeLogFn != null ? FontStyle.italic : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
CategoryEditorSelector(
|
||||
alignment: WrapAlignment.center,
|
||||
preselected: app?.app.categories != null
|
||||
? app!.app.categories.toSet()
|
||||
: {},
|
||||
onSelected: (categories) {
|
||||
if (app != null) {
|
||||
app.app.categories = categories;
|
||||
appsProvider.saveApps([app.app]);
|
||||
}
|
||||
}),
|
||||
if (app?.app.additionalSettings['about'] is String &&
|
||||
app?.app.additionalSettings['about'].isNotEmpty)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
GestureDetector(
|
||||
onLongPress: () {
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: app?.app.additionalSettings['about'] ?? ''));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(tr('copiedToClipboard')),
|
||||
));
|
||||
},
|
||||
child: Text(
|
||||
app?.app.additionalSettings['about'],
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
getFullInfoColumn() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
app?.icon != null
|
||||
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
GestureDetector(
|
||||
child: Image.memory(
|
||||
app!.icon!,
|
||||
height: 150,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
onTap: () => pm.openApp(app.app.id),
|
||||
)
|
||||
])
|
||||
: Container(),
|
||||
const SizedBox(
|
||||
height: 25,
|
||||
),
|
||||
Text(
|
||||
app?.name ?? tr('app'),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.displayLarge,
|
||||
),
|
||||
Text(tr('byX', args: [app?.app.author ?? tr('unknown')]),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (app?.app.url != null) {
|
||||
@@ -70,116 +269,15 @@ class _AppPageState extends State<AppPage> {
|
||||
child: Text(
|
||||
app?.app.url ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 12),
|
||||
fontStyle: FontStyle.italic),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
Text(
|
||||
tr('latestVersionX',
|
||||
args: [app?.app.latestVersion ?? tr('unknown')]),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
Text(
|
||||
'${tr('installedVersionX', args: [
|
||||
app?.app.installedVersion ?? tr('none')
|
||||
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
||||
tr('app')
|
||||
])}' : ''}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (app?.app.installedVersion != null &&
|
||||
!isVersionDetectionStandard)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
tr('noVersionDetection'),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
Text(
|
||||
tr('lastUpdateCheckX', args: [
|
||||
app?.app.lastUpdateCheck == null
|
||||
? tr('never')
|
||||
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
|
||||
]),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
CategoryEditorSelector(
|
||||
alignment: WrapAlignment.center,
|
||||
preselected: app?.app.categories != null
|
||||
? app!.app.categories.toSet()
|
||||
: {},
|
||||
onSelected: (categories) {
|
||||
if (app != null) {
|
||||
app.app.categories = categories;
|
||||
appsProvider.saveApps([app.app]);
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
getFullInfoColumn() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 125),
|
||||
app?.installedInfo != null
|
||||
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Image.memory(
|
||||
app!.installedInfo!.icon!,
|
||||
height: 150,
|
||||
gaplessPlayback: true,
|
||||
)
|
||||
])
|
||||
: Container(),
|
||||
const SizedBox(
|
||||
height: 25,
|
||||
),
|
||||
Text(
|
||||
app?.name ?? tr('app'),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.displayLarge,
|
||||
),
|
||||
Text(
|
||||
tr('byX', args: [app?.app.author ?? tr('unknown')]),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 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(
|
||||
height: 32,
|
||||
),
|
||||
getInfoColumn(),
|
||||
const SizedBox(height: 150)
|
||||
],
|
||||
@@ -248,25 +346,6 @@ class _AppPageState extends State<AppPage> {
|
||||
return row;
|
||||
}).toList();
|
||||
|
||||
items = items.map((row) {
|
||||
row = row.map((e) {
|
||||
if (e.key == 'versionDetection' && e is GeneratedFormDropdown) {
|
||||
e.disabledOptKeys ??= [];
|
||||
if (app?.app.installedVersion != null &&
|
||||
app?.app.additionalSettings['versionDetection'] !=
|
||||
'releaseDateAsVersion' &&
|
||||
!appsProvider.isVersionDetectionPossible(app)) {
|
||||
e.disabledOptKeys!.add('standardVersionDetection');
|
||||
}
|
||||
if (app?.app.releaseDate == null) {
|
||||
e.disabledOptKeys!.add('releaseDateAsVersion');
|
||||
}
|
||||
}
|
||||
return e;
|
||||
}).toList();
|
||||
return row;
|
||||
}).toList();
|
||||
|
||||
return GeneratedFormModal(
|
||||
title: tr('additionalOptions'), items: items);
|
||||
});
|
||||
@@ -279,57 +358,64 @@ class _AppPageState extends State<AppPage> {
|
||||
if (source?.enforceTrackOnly == true) {
|
||||
app.app.additionalSettings['trackOnly'] = true;
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(tr('appsFromSourceAreTrackOnly'), context);
|
||||
showMessage(tr('appsFromSourceAreTrackOnly'), context);
|
||||
}
|
||||
if (app.app.additionalSettings['versionDetection'] ==
|
||||
'releaseDateAsVersion') {
|
||||
if (originalSettings['versionDetection'] != 'releaseDateAsVersion') {
|
||||
var versionDetectionEnabled =
|
||||
app.app.additionalSettings['versionDetection'] == true &&
|
||||
originalSettings['versionDetection'] != true;
|
||||
var releaseDateVersionEnabled =
|
||||
app.app.additionalSettings['releaseDateAsVersion'] == true &&
|
||||
originalSettings['releaseDateAsVersion'] != true;
|
||||
var releaseDateVersionDisabled =
|
||||
app.app.additionalSettings['releaseDateAsVersion'] != true &&
|
||||
originalSettings['releaseDateAsVersion'] == true;
|
||||
if (releaseDateVersionEnabled) {
|
||||
if (app.app.releaseDate != null) {
|
||||
bool isUpdated =
|
||||
app.app.installedVersion == app.app.latestVersion;
|
||||
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') {
|
||||
} else if (releaseDateVersionDisabled) {
|
||||
app.app.installedVersion =
|
||||
app.installedInfo?.versionName ?? app.app.installedVersion;
|
||||
}
|
||||
if (versionDetectionEnabled) {
|
||||
app.app.additionalSettings['versionDetection'] = true;
|
||||
app.app.additionalSettings['releaseDateAsVersion'] = false;
|
||||
}
|
||||
appsProvider.saveApps([app.app]).then((value) {
|
||||
getUpdate(app.app.id);
|
||||
getUpdate(app.app.id, resetVersion: versionDetectionEnabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getResetInstallStatusButton() => TextButton(
|
||||
onPressed: app?.app == null
|
||||
? null
|
||||
: () {
|
||||
app!.app.installedVersion = null;
|
||||
appsProvider.saveApps([app.app]);
|
||||
},
|
||||
child: Text(tr('resetInstallStatus')));
|
||||
|
||||
getInstallOrUpdateButton() => TextButton(
|
||||
onPressed: (app?.app.installedVersion == null ||
|
||||
onPressed: !updating &&
|
||||
(app?.app.installedVersion == null ||
|
||||
app?.app.installedVersion != app?.app.latestVersion) &&
|
||||
!areDownloadsRunning
|
||||
? () async {
|
||||
try {
|
||||
var successMessage = app?.app.installedVersion == null
|
||||
? tr('installed')
|
||||
: tr('appsUpdated');
|
||||
HapticFeedback.heavyImpact();
|
||||
if (app?.app.additionalSettings['trackOnly'] != true) {
|
||||
await settingsProvider.getInstallPermission();
|
||||
}
|
||||
var res = await appsProvider.downloadAndInstallLatestApps(
|
||||
[app!.app.id], globalNavigatorKey.currentContext);
|
||||
app?.app.id != null ? [app!.app.id] : [],
|
||||
globalNavigatorKey.currentContext,
|
||||
);
|
||||
if (res.isNotEmpty && !trackOnly) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showMessage(successMessage, context);
|
||||
}
|
||||
if (res.isNotEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(e, context);
|
||||
}
|
||||
}
|
||||
@@ -353,20 +439,10 @@ class _AppPageState extends State<AppPage> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (app?.app.installedVersion != null &&
|
||||
app?.app.installedVersion != app?.app.latestVersion &&
|
||||
!isVersionDetectionStandard &&
|
||||
!trackOnly)
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: showMarkUpdatedDialog,
|
||||
tooltip: tr('markUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
if (source != null &&
|
||||
source.combinedAppSpecificSettingFormItems.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
onPressed: app?.downloadProgress != null || updating
|
||||
? null
|
||||
: () async {
|
||||
var values =
|
||||
@@ -408,45 +484,69 @@ class _AppPageState extends State<AppPage> {
|
||||
},
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
tooltip: tr('more')),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: !isVersionDetectionStandard &&
|
||||
if (app?.app.installedVersion != null &&
|
||||
app?.app.installedVersion != app?.app.latestVersion &&
|
||||
!isVersionDetectionStandard &&
|
||||
!trackOnly)
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null || updating
|
||||
? null
|
||||
: showMarkUpdatedDialog,
|
||||
tooltip: tr('markUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
if ((!isVersionDetectionStandard || trackOnly) &&
|
||||
app?.app.installedVersion != null &&
|
||||
app?.app.installedVersion ==
|
||||
app?.app.latestVersion
|
||||
? getResetInstallStatusButton()
|
||||
: getInstallOrUpdateButton()),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
app?.app.installedVersion == app?.app.latestVersion)
|
||||
IconButton(
|
||||
onPressed: app?.app == null || updating
|
||||
? null
|
||||
: () {
|
||||
appsProvider.removeAppsWithModal(
|
||||
context, [app!.app]).then((value) {
|
||||
app!.app.installedVersion = null;
|
||||
appsProvider.saveApps([app.app]);
|
||||
},
|
||||
icon: const Icon(Icons.restore_rounded),
|
||||
tooltip: tr('resetInstallStatus')),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(child: getInstallOrUpdateButton()),
|
||||
const SizedBox(width: 16.0),
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null || updating
|
||||
? null
|
||||
: () {
|
||||
appsProvider
|
||||
.removeAppsWithModal(
|
||||
context, app != null ? [app.app] : [])
|
||||
.then((value) {
|
||||
if (value == true) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.error,
|
||||
surfaceTintColor:
|
||||
Theme.of(context).colorScheme.error),
|
||||
child: Text(tr('remove')),
|
||||
)),
|
||||
tooltip: tr('remove'),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
])),
|
||||
if (app?.downloadProgress != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||
child: LinearProgressIndicator(
|
||||
value: app!.downloadProgress! / 100))
|
||||
value: app!.downloadProgress! >= 0
|
||||
? app.downloadProgress! / 100
|
||||
: null))
|
||||
],
|
||||
));
|
||||
|
||||
appScreenAppBar() => AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||
appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: RefreshIndicator(
|
||||
child: settingsProvider.showAppWebpage
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -24,6 +26,92 @@ class AppsPage extends StatefulWidget {
|
||||
State<AppsPage> createState() => AppsPageState();
|
||||
}
|
||||
|
||||
showChangeLogDialog(BuildContext context, App app, String? changesUrl,
|
||||
AppSource appSource, String changeLog) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('changes'),
|
||||
items: const [],
|
||||
message: app.latestVersion,
|
||||
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(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'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getChangeLogFn(BuildContext context, App app) {
|
||||
AppSource appSource =
|
||||
SourceProvider().getSource(app.url, overrideSource: app.overrideSource);
|
||||
String? changesUrl = appSource.changeLogPageFromStandardUrl(app.url);
|
||||
String? changeLog = app.changeLog;
|
||||
if (changeLog?.split('\n').length == 1) {
|
||||
if (RegExp(
|
||||
'(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?')
|
||||
.hasMatch(changeLog!)) {
|
||||
if (changesUrl == null) {
|
||||
changesUrl = changeLog;
|
||||
changeLog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (changeLog == null && changesUrl == null)
|
||||
? null
|
||||
: () {
|
||||
if (changeLog != null) {
|
||||
showChangeLogDialog(context, app, changesUrl, appSource, changeLog);
|
||||
} else {
|
||||
launchUrlString(changesUrl!, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class AppsPageState extends State<AppsPage> {
|
||||
AppsFilter filter = AppsFilter();
|
||||
final AppsFilter neutralFilter = AppsFilter();
|
||||
@@ -52,14 +140,37 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
|
||||
GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var appsProvider = context.watch<AppsProvider>();
|
||||
var settingsProvider = context.watch<SettingsProvider>();
|
||||
var sourceProvider = SourceProvider();
|
||||
var listedApps = appsProvider.getAppValues().toList();
|
||||
var currentFilterIsUpdatesOnly =
|
||||
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
||||
|
||||
refresh() {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
refreshingSince = DateTime.now();
|
||||
});
|
||||
return appsProvider.checkUpdates().catchError((e) {
|
||||
showError(e is Map ? e['errors'] : e, context);
|
||||
return <App>[];
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
refreshingSince = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!appsProvider.loadingApps &&
|
||||
appsProvider.apps.isNotEmpty &&
|
||||
settingsProvider.checkJustStarted() &&
|
||||
settingsProvider.checkOnStart) {
|
||||
_refreshIndicatorKey.currentState?.show();
|
||||
}
|
||||
|
||||
selectedAppIds = selectedAppIds
|
||||
.where((element) => listedApps.map((e) => e.app.id).contains(element))
|
||||
@@ -104,6 +215,11 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filter.idFilter.isNotEmpty) {
|
||||
if (!app.app.id.contains(filter.idFilter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filter.categoryFilter.isNotEmpty &&
|
||||
filter.categoryFilter
|
||||
.intersection(app.app.categories.toSet())
|
||||
@@ -111,7 +227,11 @@ class AppsPageState extends State<AppsPage> {
|
||||
return false;
|
||||
}
|
||||
if (filter.sourceFilter.isNotEmpty &&
|
||||
sourceProvider.getSource(app.app.url).runtimeType.toString() !=
|
||||
sourceProvider
|
||||
.getSource(app.app.url,
|
||||
overrideSource: app.app.overrideSource)
|
||||
.runtimeType
|
||||
.toString() !=
|
||||
filter.sourceFilter) {
|
||||
return false;
|
||||
}
|
||||
@@ -181,6 +301,18 @@ class AppsPageState extends State<AppsPage> {
|
||||
listedApps = [...temp, ...listedApps];
|
||||
}
|
||||
|
||||
if (settingsProvider.buryNonInstalled) {
|
||||
var temp = [];
|
||||
listedApps = listedApps.where((sa) {
|
||||
if (sa.app.installedVersion == null) {
|
||||
temp.add(sa);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
listedApps = [...listedApps, ...temp];
|
||||
}
|
||||
|
||||
var tempPinned = [];
|
||||
var tempNotPinned = [];
|
||||
for (var a in listedApps) {
|
||||
@@ -216,113 +348,35 @@ class AppsPageState extends State<AppsPage> {
|
||||
.where((a) => selectedAppIds.contains(a.id))
|
||||
.toSet();
|
||||
|
||||
showChangeLogDialog(
|
||||
String? changesUrl, AppSource appSource, String changeLog, int index) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('changes'),
|
||||
items: const [],
|
||||
message: listedApps[index].app.latestVersion,
|
||||
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'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getLoadingWidgets() {
|
||||
return [
|
||||
if (appsProvider.loadingApps || listedApps.isEmpty)
|
||||
if (listedApps.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: appsProvider.loadingApps
|
||||
? const CircularProgressIndicator()
|
||||
: Text(
|
||||
appsProvider.apps.isEmpty
|
||||
? tr('noApps')
|
||||
: tr('noAppsForFilter'),
|
||||
child: Text(
|
||||
appsProvider.apps.isEmpty ? tr('noApps') : tr('noAppsForFilter'),
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
))),
|
||||
if (refreshingSince != null)
|
||||
if (refreshingSince != null || appsProvider.loadingApps)
|
||||
SliverToBoxAdapter(
|
||||
child: LinearProgressIndicator(
|
||||
value: appsProvider
|
||||
value: appsProvider.loadingApps
|
||||
? null
|
||||
: appsProvider
|
||||
.getAppValues()
|
||||
.where((element) => !(element.app.lastUpdateCheck
|
||||
?.isBefore(refreshingSince!) ??
|
||||
true))
|
||||
.length /
|
||||
appsProvider.apps.length,
|
||||
(appsProvider.apps.isNotEmpty
|
||||
? appsProvider.apps.length
|
||||
: 1),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
getChangeLogFn(int appIndex) {
|
||||
AppSource appSource =
|
||||
SourceProvider().getSource(listedApps[appIndex].app.url);
|
||||
String? changesUrl =
|
||||
appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url);
|
||||
String? changeLog = listedApps[appIndex].app.changeLog;
|
||||
return (changeLog == null && changesUrl == null)
|
||||
? null
|
||||
: () {
|
||||
if (changeLog != null) {
|
||||
showChangeLogDialog(changesUrl, appSource, changeLog, appIndex);
|
||||
} else {
|
||||
launchUrlString(changesUrl!,
|
||||
mode: LaunchMode.externalApplication);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getUpdateButton(int appIndex) {
|
||||
return IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
@@ -338,6 +392,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
[listedApps[appIndex].app.id],
|
||||
globalNavigatorKey.currentContext).catchError((e) {
|
||||
showError(e, context);
|
||||
return <String>[];
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
@@ -347,9 +402,9 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
|
||||
getAppIcon(int appIndex) {
|
||||
return listedApps[appIndex].installedInfo != null
|
||||
return listedApps[appIndex].icon != null
|
||||
? Image.memory(
|
||||
listedApps[appIndex].installedInfo!.icon!,
|
||||
listedApps[appIndex].icon!,
|
||||
gaplessPlayback: true,
|
||||
)
|
||||
: Row(
|
||||
@@ -364,7 +419,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
child: Image(
|
||||
image: const AssetImage(
|
||||
'assets/graphics/icon_small.png'),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
colorBlendMode: BlendMode.modulate,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
@@ -373,7 +428,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
|
||||
getVersionText(int appIndex) {
|
||||
return '${listedApps[appIndex].app.installedVersion ?? tr('notInstalled')}${listedApps[appIndex].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}';
|
||||
return '${listedApps[appIndex].app.installedVersion ?? tr('notInstalled')}${listedApps[appIndex].app.additionalSettings['trackOnly'] == true ? ' ${tr('pseudoVersion')}' : ''}';
|
||||
}
|
||||
|
||||
getChangesButtonString(int appIndex, bool hasChangeLogFn) {
|
||||
@@ -386,7 +441,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
|
||||
getSingleAppHorizTile(int index) {
|
||||
var showChangesFn = getChangeLogFn(index);
|
||||
var showChangesFn = getChangeLogFn(context, listedApps[index].app);
|
||||
var hasUpdate = listedApps[index].app.installedVersion != null &&
|
||||
listedApps[index].app.installedVersion !=
|
||||
listedApps[index].app.latestVersion;
|
||||
@@ -400,51 +455,58 @@ class AppsPageState extends State<AppsPage> {
|
||||
width: 10,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Column(
|
||||
GestureDetector(
|
||||
onTap: showChangesFn,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: settingsProvider.highlightTouchTargets &&
|
||||
showChangesFn != null
|
||||
? (Theme.of(context).brightness == Brightness.light
|
||||
? Theme.of(context).primaryColor
|
||||
: Theme.of(context).primaryColorLight)
|
||||
.withAlpha(20)
|
||||
: null),
|
||||
padding: settingsProvider.highlightTouchTargets
|
||||
? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
|
||||
: const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width / 4),
|
||||
child: Text(
|
||||
getVersionText(index),
|
||||
maxWidth:
|
||||
MediaQuery.of(context).size.width / 4),
|
||||
child: Text(getVersionText(index),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.end,
|
||||
)),
|
||||
textAlign: TextAlign.end)),
|
||||
]),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: showChangesFn,
|
||||
child: Text(
|
||||
getChangesButtonString(index, showChangesFn != null),
|
||||
Text(
|
||||
getChangesButtonString(
|
||||
index, showChangesFn != null),
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
decoration: showChangesFn != null
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none),
|
||||
))
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
)))
|
||||
],
|
||||
);
|
||||
|
||||
var transparent =
|
||||
Theme.of(context).colorScheme.background.withAlpha(0).value;
|
||||
List<double> stops = [
|
||||
...listedApps[index]
|
||||
.app
|
||||
.categories
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) =>
|
||||
((e.key / (listedApps[index].app.categories.length - 1))))
|
||||
.toList(),
|
||||
...listedApps[index].app.categories.asMap().entries.map(
|
||||
(e) => ((e.key / (listedApps[index].app.categories.length - 1)))),
|
||||
1
|
||||
];
|
||||
if (stops.length == 2) {
|
||||
@@ -457,13 +519,9 @@ class AppsPageState extends State<AppsPage> {
|
||||
begin: const Alignment(-1, 0),
|
||||
end: const Alignment(-0.97, 0),
|
||||
colors: [
|
||||
...listedApps[index]
|
||||
.app
|
||||
.categories
|
||||
.map((e) =>
|
||||
...listedApps[index].app.categories.map((e) =>
|
||||
Color(settingsProvider.categories[e] ?? transparent)
|
||||
.withAlpha(255))
|
||||
.toList(),
|
||||
.withAlpha(255)),
|
||||
Color(transparent)
|
||||
])),
|
||||
child: ListTile(
|
||||
@@ -498,10 +556,20 @@ class AppsPageState extends State<AppsPage> {
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal)),
|
||||
trailing: listedApps[index].downloadProgress != null
|
||||
? Text(tr('percentProgress', args: [
|
||||
listedApps[index].downloadProgress?.toInt().toString() ??
|
||||
'100'
|
||||
]))
|
||||
? SizedBox(
|
||||
child: Text(
|
||||
listedApps[index].downloadProgress! >= 0
|
||||
? tr('percentProgress', args: [
|
||||
listedApps[index]
|
||||
.downloadProgress!
|
||||
.toInt()
|
||||
.toString()
|
||||
])
|
||||
: tr('installing'),
|
||||
textAlign: (listedApps[index].downloadProgress! >= 0)
|
||||
? TextAlign.start
|
||||
: TextAlign.end,
|
||||
))
|
||||
: trailingRow,
|
||||
onTap: () {
|
||||
if (selectedAppIds.isNotEmpty) {
|
||||
@@ -589,15 +657,15 @@ class AppsPageState extends State<AppsPage> {
|
||||
label: tr('installX', args: [
|
||||
plural('apps', newInstallIdsAllOrSelected.length)
|
||||
]),
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty));
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isEmpty));
|
||||
}
|
||||
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
|
||||
formItems.add(GeneratedFormSwitch('trackonlies',
|
||||
label: tr('markXTrackOnlyAsUpdated', args: [
|
||||
plural('apps', trackOnlyUpdateIdsAllOrSelected.length)
|
||||
]),
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty ||
|
||||
newInstallIdsAllOrSelected.isNotEmpty));
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isEmpty &&
|
||||
newInstallIdsAllOrSelected.isEmpty));
|
||||
}
|
||||
showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
@@ -610,7 +678,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
items: formItems.map((e) => [e]).toList(),
|
||||
initValid: true,
|
||||
);
|
||||
}).then((values) {
|
||||
}).then((values) async {
|
||||
if (values != null) {
|
||||
if (values.isEmpty) {
|
||||
values = getDefaultValuesFromFormItems([formItems]);
|
||||
@@ -618,12 +686,6 @@ class AppsPageState extends State<AppsPage> {
|
||||
bool shouldInstallUpdates = values['updates'] == true;
|
||||
bool shouldInstallNew = values['installs'] == true;
|
||||
bool shouldMarkTrackOnlies = values['trackonlies'] == true;
|
||||
(() async {
|
||||
if (shouldInstallNew || shouldInstallUpdates) {
|
||||
await settingsProvider.getInstallPermission();
|
||||
}
|
||||
})()
|
||||
.then((_) {
|
||||
List<String> toInstall = [];
|
||||
if (shouldInstallUpdates) {
|
||||
toInstall.addAll(existingUpdateIdsAllOrSelected);
|
||||
@@ -639,7 +701,11 @@ class AppsPageState extends State<AppsPage> {
|
||||
toInstall, globalNavigatorKey.currentContext)
|
||||
.catchError((e) {
|
||||
showError(e, context);
|
||||
});
|
||||
return <String>[];
|
||||
}).then((value) {
|
||||
if (value.isNotEmpty && shouldInstallUpdates) {
|
||||
showMessage(tr('appsUpdated'), context);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -766,7 +832,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
items: const [],
|
||||
initValid: true,
|
||||
message: tr('installStatusOfXWillBeResetExplanation',
|
||||
args: [plural('app', selectedAppIds.length)]),
|
||||
args: [plural('apps', selectedAppIds.length)]),
|
||||
);
|
||||
});
|
||||
if (values != null) {
|
||||
@@ -788,29 +854,18 @@ class AppsPageState extends State<AppsPage> {
|
||||
scrollable: true,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Row(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: appsProvider.areDownloadsRunning()
|
||||
? null
|
||||
: showMassMarkDialog,
|
||||
tooltip: tr('markSelectedAppsUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
IconButton(
|
||||
TextButton(
|
||||
onPressed: pinSelectedApps,
|
||||
tooltip: selectedApps
|
||||
child: Text(selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? tr('pinToTop')
|
||||
: tr('unpinFromTop'),
|
||||
icon: Icon(selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? Icons.bookmark_outline_rounded
|
||||
: Icons.bookmark_remove_outlined),
|
||||
),
|
||||
IconButton(
|
||||
: tr('unpinFromTop'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
String urls = '';
|
||||
for (var a in selectedApps) {
|
||||
@@ -818,27 +873,73 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
urls = urls.substring(0, urls.length - 1);
|
||||
Share.share(urls,
|
||||
subject: tr('selectedAppURLsFromObtainium'));
|
||||
subject: 'Obtainium - ${tr('appsString')}');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
tooltip: tr('shareSelectedAppURLs'),
|
||||
icon: const Icon(Icons.share),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: resetSelectedAppsInstallStatuses,
|
||||
tooltip: tr('resetInstallStatus'),
|
||||
icon: const Icon(Icons.restore_page_outlined),
|
||||
),
|
||||
child: Text(tr('shareSelectedAppURLs'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: selectedAppIds.isEmpty
|
||||
? null
|
||||
: () {
|
||||
String urls = '';
|
||||
for (var a in selectedApps) {
|
||||
urls +=
|
||||
'https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/${Uri.encodeComponent(jsonEncode({
|
||||
'id': a.id,
|
||||
'url': a.url,
|
||||
'author': a.author,
|
||||
'name': a.name,
|
||||
'preferredApkIndex':
|
||||
a.preferredApkIndex,
|
||||
'additionalSettings':
|
||||
jsonEncode(a.additionalSettings)
|
||||
}))}\n\n';
|
||||
}
|
||||
Share.share(urls,
|
||||
subject:
|
||||
'Obtainium - ${tr('appsString')}');
|
||||
},
|
||||
child: Text(tr('shareAppConfigLinks'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
appsProvider
|
||||
.downloadAppAssets(
|
||||
selectedApps.map((e) => e.id).toList(),
|
||||
globalNavigatorKey.currentContext ??
|
||||
context)
|
||||
.catchError((e) => showError(
|
||||
e,
|
||||
globalNavigatorKey.currentContext ??
|
||||
context));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(tr('downloadX',
|
||||
args: [tr('releaseAsset').toLowerCase()]))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: appsProvider.areDownloadsRunning()
|
||||
? null
|
||||
: showMassMarkDialog,
|
||||
child: Text(tr('markSelectedAppsUpdated'))),
|
||||
]),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getMainBottomButtonsRow() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
getMainBottomButtons() {
|
||||
return [
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: getMassObtainFunction(),
|
||||
tooltip: selectedAppIds.isEmpty
|
||||
? tr('installUpdateApps')
|
||||
: tr('installUpdateSelectedApps'),
|
||||
icon: const Icon(
|
||||
Icons.file_download_outlined,
|
||||
)),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: selectedAppIds.isEmpty
|
||||
@@ -850,15 +951,6 @@ class AppsPageState extends State<AppsPage> {
|
||||
tooltip: tr('removeSelectedApps'),
|
||||
icon: const Icon(Icons.delete_outline_outlined),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: getMassObtainFunction(),
|
||||
tooltip: selectedAppIds.isEmpty
|
||||
? tr('installUpdateApps')
|
||||
: tr('installUpdateSelectedApps'),
|
||||
icon: const Icon(
|
||||
Icons.file_download_outlined,
|
||||
)),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: selectedAppIds.isEmpty ? null : launchCategorizeDialog(),
|
||||
@@ -871,8 +963,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
tooltip: tr('more'),
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
),
|
||||
],
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
showFilterDialog() async {
|
||||
@@ -894,6 +985,12 @@ class AppsPageState extends State<AppsPage> {
|
||||
required: false,
|
||||
defaultValue: vals['author'])
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('appId',
|
||||
label: tr('appId'),
|
||||
required: false,
|
||||
defaultValue: vals['appId'])
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('upToDateApps',
|
||||
label: tr('upToDateApps'),
|
||||
@@ -911,10 +1008,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
defaultValue: filter.sourceFilter,
|
||||
[
|
||||
MapEntry('', tr('none')),
|
||||
...sourceProvider.sources
|
||||
.map((e) =>
|
||||
MapEntry(e.runtimeType.toString(), e.name))
|
||||
.toList()
|
||||
...sourceProvider.sources.map(
|
||||
(e) => MapEntry(e.runtimeType.toString(), e.name))
|
||||
])
|
||||
]
|
||||
],
|
||||
@@ -939,50 +1034,33 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
|
||||
getFilterButtonsRow() {
|
||||
var isFilterOff = filter.isIdenticalTo(neutralFilter, settingsProvider);
|
||||
return Row(
|
||||
children: [
|
||||
getSelectAllButton(),
|
||||
const VerticalDivider(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: getMainBottomButtonsRow())),
|
||||
const VerticalDivider(),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
tooltip: '${tr('filter')}${isFilterOff ? '' : ' *'}',
|
||||
onPressed: isFilterOff
|
||||
? showFilterDialog
|
||||
: () {
|
||||
setState(() {
|
||||
if (currentFilterIsUpdatesOnly) {
|
||||
filter = AppsFilter();
|
||||
} else {
|
||||
filter = updatesOnlyFilter;
|
||||
}
|
||||
});
|
||||
},
|
||||
tooltip: currentFilterIsUpdatesOnly
|
||||
? tr('removeOutdatedFilter')
|
||||
: tr('showOutdatedOnly'),
|
||||
icon: Icon(
|
||||
currentFilterIsUpdatesOnly
|
||||
? Icons.update_disabled_rounded
|
||||
: Icons.update_rounded,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: Icon(isFilterOff
|
||||
? Icons.filter_list_rounded
|
||||
: Icons.filter_list_off_rounded)),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
label: Text(
|
||||
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
||||
? tr('filter')
|
||||
: tr('filterActive'),
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
||||
? FontWeight.normal
|
||||
: FontWeight.bold),
|
||||
),
|
||||
onPressed: showFilterDialog,
|
||||
icon: const Icon(Icons.filter_list_rounded))
|
||||
const VerticalDivider(),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: getMainBottomButtons(),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1006,19 +1084,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
refreshingSince = DateTime.now();
|
||||
});
|
||||
return appsProvider.checkUpdates().catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
refreshingSince = null;
|
||||
});
|
||||
});
|
||||
},
|
||||
key: _refreshIndicatorKey,
|
||||
onRefresh: refresh,
|
||||
child: CustomScrollView(slivers: <Widget>[
|
||||
CustomAppBar(title: tr('appsString')),
|
||||
...getLoadingWidgets(),
|
||||
@@ -1036,6 +1103,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
class AppsFilter {
|
||||
late String nameFilter;
|
||||
late String authorFilter;
|
||||
late String idFilter;
|
||||
late bool includeUptodate;
|
||||
late bool includeNonInstalled;
|
||||
late Set<String> categoryFilter;
|
||||
@@ -1044,6 +1112,7 @@ class AppsFilter {
|
||||
AppsFilter(
|
||||
{this.nameFilter = '',
|
||||
this.authorFilter = '',
|
||||
this.idFilter = '',
|
||||
this.includeUptodate = true,
|
||||
this.includeNonInstalled = true,
|
||||
this.categoryFilter = const {},
|
||||
@@ -1053,6 +1122,7 @@ class AppsFilter {
|
||||
return {
|
||||
'appName': nameFilter,
|
||||
'author': authorFilter,
|
||||
'appId': idFilter,
|
||||
'upToDateApps': includeUptodate,
|
||||
'nonInstalledApps': includeNonInstalled,
|
||||
'sourceFilter': sourceFilter
|
||||
@@ -1062,6 +1132,7 @@ class AppsFilter {
|
||||
setFormValuesFromMap(Map<String, dynamic> values) {
|
||||
nameFilter = values['appName']!;
|
||||
authorFilter = values['author']!;
|
||||
idFilter = values['appId']!;
|
||||
includeUptodate = values['upToDateApps'];
|
||||
includeNonInstalled = values['nonInstalledApps'];
|
||||
sourceFilter = values['sourceFilter'];
|
||||
@@ -1070,6 +1141,7 @@ class AppsFilter {
|
||||
bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
|
||||
authorFilter.trim() == other.authorFilter.trim() &&
|
||||
nameFilter.trim() == other.nameFilter.trim() &&
|
||||
idFilter.trim() == other.idFilter.trim() &&
|
||||
includeUptodate == other.includeUptodate &&
|
||||
includeNonInstalled == other.includeNonInstalled &&
|
||||
settingsProvider.setEqual(categoryFilter, other.categoryFilter) &&
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/pages/add_app.dart';
|
||||
import 'package:obtainium/pages/apps.dart';
|
||||
import 'package:obtainium/pages/import_export.dart';
|
||||
import 'package:obtainium/pages/settings.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
@@ -24,22 +32,162 @@ class NavigationPageItem {
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
List<int> selectedIndexHistory = [];
|
||||
bool isReversing = false;
|
||||
int prevAppCount = -1;
|
||||
bool prevIsLoading = true;
|
||||
late AppLinks _appLinks;
|
||||
StreamSubscription<Uri>? _linkSubscription;
|
||||
bool isLinkActivity = false;
|
||||
|
||||
List<NavigationPageItem> pages = [
|
||||
NavigationPageItem(tr('appsString'), Icons.apps,
|
||||
AppsPage(key: GlobalKey<AppsPageState>())),
|
||||
NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()),
|
||||
NavigationPageItem(
|
||||
tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
|
||||
NavigationPageItem(
|
||||
tr('importExport'), Icons.import_export, const ImportExportPage()),
|
||||
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initDeepLinks();
|
||||
}
|
||||
|
||||
Future<void> initDeepLinks() async {
|
||||
_appLinks = AppLinks();
|
||||
|
||||
goToAddApp(String data) async {
|
||||
switchToPage(1);
|
||||
while (
|
||||
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
|
||||
null) {
|
||||
await Future.delayed(const Duration(microseconds: 1));
|
||||
}
|
||||
(pages[1].widget.key as GlobalKey<AddAppPageState>?)
|
||||
?.currentState
|
||||
?.linkFn(data);
|
||||
}
|
||||
|
||||
interpretLink(Uri uri) async {
|
||||
isLinkActivity = true;
|
||||
var action = uri.host;
|
||||
var data = uri.path.length > 1 ? uri.path.substring(1) : "";
|
||||
try {
|
||||
if (action == 'add') {
|
||||
await goToAddApp(data);
|
||||
} else if (action == 'app' || action == 'apps') {
|
||||
var dataStr = Uri.decodeComponent(data);
|
||||
if (await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('importX', args: [
|
||||
action == 'app' ? tr('app') : tr('appsString')
|
||||
]),
|
||||
items: const [],
|
||||
additionalWidgets: [
|
||||
ExpansionTile(
|
||||
title: const Text('Raw JSON'),
|
||||
children: [
|
||||
Text(
|
||||
dataStr,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}) !=
|
||||
null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
var result = await context.read<AppsProvider>().import(
|
||||
action == 'app'
|
||||
? '{ "apps": [$dataStr] }'
|
||||
: '{ "apps": $dataStr }');
|
||||
// ignore: use_build_context_synchronously
|
||||
showMessage(
|
||||
tr('importedX', args: [plural('apps', result.key)]), context);
|
||||
}
|
||||
} else {
|
||||
throw ObtainiumError(tr('unknown'));
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
}
|
||||
}
|
||||
|
||||
// Check initial link if app was in cold state (terminated)
|
||||
final appLink = await _appLinks.getInitialAppLink();
|
||||
if (appLink != null) {
|
||||
await interpretLink(appLink);
|
||||
}
|
||||
|
||||
// Handle link when app is in warm state (front or background)
|
||||
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
|
||||
await interpretLink(uri);
|
||||
});
|
||||
}
|
||||
|
||||
setIsReversing(int targetIndex) {
|
||||
bool reversing = selectedIndexHistory.isNotEmpty &&
|
||||
selectedIndexHistory.last > targetIndex;
|
||||
setState(() {
|
||||
isReversing = reversing;
|
||||
});
|
||||
}
|
||||
|
||||
switchToPage(int index) async {
|
||||
setIsReversing(index);
|
||||
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();
|
||||
});
|
||||
} else if (selectedIndexHistory.isEmpty ||
|
||||
(selectedIndexHistory.isNotEmpty &&
|
||||
selectedIndexHistory.last != index)) {
|
||||
setState(() {
|
||||
int existingInd = selectedIndexHistory.indexOf(index);
|
||||
if (existingInd >= 0) {
|
||||
selectedIndexHistory.removeAt(existingInd);
|
||||
}
|
||||
selectedIndexHistory.add(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppsProvider appsProvider = context.watch<AppsProvider>();
|
||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||
|
||||
if (!prevIsLoading &&
|
||||
prevAppCount >= 0 &&
|
||||
appsProvider.apps.length > prevAppCount &&
|
||||
selectedIndexHistory.isNotEmpty &&
|
||||
selectedIndexHistory.last == 1 &&
|
||||
!isLinkActivity) {
|
||||
switchToPage(0);
|
||||
}
|
||||
prevAppCount = appsProvider.apps.length;
|
||||
prevIsLoading = appsProvider.loadingApps;
|
||||
|
||||
return WillPopScope(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: PageTransitionSwitcher(
|
||||
duration: Duration(
|
||||
milliseconds:
|
||||
settingsProvider.disablePageTransitions ? 0 : 300),
|
||||
reverse: settingsProvider.reversePageTransitions
|
||||
? !isReversing
|
||||
: isReversing,
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
@@ -65,33 +213,21 @@ class _HomePageState extends State<HomePage> {
|
||||
.toList(),
|
||||
onDestinationSelected: (int index) async {
|
||||
HapticFeedback.selectionClick();
|
||||
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();
|
||||
});
|
||||
} else if (selectedIndexHistory.isEmpty ||
|
||||
(selectedIndexHistory.isNotEmpty &&
|
||||
selectedIndexHistory.last != index)) {
|
||||
setState(() {
|
||||
int existingInd = selectedIndexHistory.indexOf(index);
|
||||
if (existingInd >= 0) {
|
||||
selectedIndexHistory.removeAt(existingInd);
|
||||
}
|
||||
selectedIndexHistory.add(index);
|
||||
});
|
||||
}
|
||||
switchToPage(index);
|
||||
},
|
||||
selectedIndex:
|
||||
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||
),
|
||||
),
|
||||
onWillPop: () async {
|
||||
if (isLinkActivity &&
|
||||
selectedIndexHistory.length == 1 &&
|
||||
selectedIndexHistory.last == 1) {
|
||||
return true;
|
||||
}
|
||||
setIsReversing(selectedIndexHistory.length >= 2
|
||||
? selectedIndexHistory.reversed.toList()[1]
|
||||
: 0);
|
||||
if (selectedIndexHistory.isNotEmpty) {
|
||||
setState(() {
|
||||
selectedIndexHistory.removeLast();
|
||||
@@ -103,4 +239,10 @@ class _HomePageState extends State<HomePage> {
|
||||
?.clearSelected();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_linkSubscription?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
@@ -28,8 +29,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
var appsProvider = context.read<AppsProvider>();
|
||||
var settingsProvider = context.read<SettingsProvider>();
|
||||
var appsProvider = context.watch<AppsProvider>();
|
||||
var settingsProvider = context.watch<SettingsProvider>();
|
||||
|
||||
var outlineButtonStyle = ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
@@ -81,7 +82,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
appsProvider.addAppsByURL(urls).then((errors) {
|
||||
if (errors.isEmpty) {
|
||||
showError(tr('importedX', args: [plural('apps', urls.length)]),
|
||||
showMessage(tr('importedX', args: [plural('apps', urls.length)]),
|
||||
context);
|
||||
} else {
|
||||
showDialog(
|
||||
@@ -102,10 +103,17 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
}
|
||||
|
||||
runObtainiumExport() {
|
||||
runObtainiumExport({bool pickOnly = false}) async {
|
||||
HapticFeedback.selectionClick();
|
||||
appsProvider.exportApps().then((String path) {
|
||||
showError(tr('exportedTo', args: [path]), context);
|
||||
appsProvider
|
||||
.export(
|
||||
pickOnly:
|
||||
pickOnly || (await settingsProvider.getExportDir()) == null,
|
||||
sp: settingsProvider)
|
||||
.then((String? result) {
|
||||
if (result != null) {
|
||||
showMessage(tr('exportedTo', args: [result]), context);
|
||||
}
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
});
|
||||
@@ -124,7 +132,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
} catch (e) {
|
||||
throw ObtainiumError(tr('invalidInput'));
|
||||
}
|
||||
appsProvider.importApps(data).then((value) {
|
||||
appsProvider.import(data).then((value) {
|
||||
var cats = settingsProvider.categories;
|
||||
appsProvider.apps.forEach((key, value) {
|
||||
for (var c in value.app.categories) {
|
||||
@@ -134,7 +142,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
}
|
||||
});
|
||||
appsProvider.addMissingCategories(settingsProvider);
|
||||
showError(tr('importedX', args: [plural('apps', value)]), context);
|
||||
showMessage(
|
||||
'${tr('importedX', args: [
|
||||
plural('apps', value.key)
|
||||
])}${value.value ? ' + ${tr('settings')}' : ''}',
|
||||
context);
|
||||
});
|
||||
} else {
|
||||
// User canceled the picker
|
||||
@@ -181,35 +193,51 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('searchQuery',
|
||||
label: tr('searchQuery'))
|
||||
]
|
||||
label: tr('searchQuery'),
|
||||
required: source.name != FDroidRepo().name)
|
||||
],
|
||||
...source.searchQuerySettingFormItems.map((e) => [e]),
|
||||
[
|
||||
GeneratedFormTextField('url',
|
||||
label: source.hosts.isNotEmpty
|
||||
? tr('overrideSource')
|
||||
: plural('url', 1).substring(2),
|
||||
defaultValue:
|
||||
source.hosts.isNotEmpty ? source.hosts[0] : '',
|
||||
required: true)
|
||||
],
|
||||
],
|
||||
);
|
||||
});
|
||||
if (values != null &&
|
||||
(values['searchQuery'] as String?)?.isNotEmpty == true) {
|
||||
if (values != null) {
|
||||
setState(() {
|
||||
importInProgress = true;
|
||||
});
|
||||
var urlsWithDescriptions =
|
||||
await source.search(values['searchQuery'] as String);
|
||||
if (source.hosts.isEmpty || values['url'] != source.hosts[0]) {
|
||||
source = sourceProvider.getSource(values['url'],
|
||||
overrideSource: source.runtimeType.toString());
|
||||
}
|
||||
var urlsWithDescriptions = await source
|
||||
.search(values['searchQuery'] as String, querySettings: values);
|
||||
if (urlsWithDescriptions.isNotEmpty) {
|
||||
var selectedUrls =
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions: urlsWithDescriptions,
|
||||
return SelectionModal(
|
||||
entries: urlsWithDescriptions,
|
||||
selectedByDefault: false,
|
||||
);
|
||||
});
|
||||
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
||||
var errors = await appsProvider.addAppsByURL(selectedUrls);
|
||||
var errors = await appsProvider.addAppsByURL(selectedUrls,
|
||||
sourceOverride: source);
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
tr('importedX', args: [plural('app', selectedUrls.length)]),
|
||||
showMessage(
|
||||
tr('importedX',
|
||||
args: [plural('apps', selectedUrls.length)]),
|
||||
context);
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
@@ -258,15 +286,14 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions: urlsWithDescriptions);
|
||||
return SelectionModal(entries: urlsWithDescriptions);
|
||||
});
|
||||
if (selectedUrls != null) {
|
||||
var errors = await appsProvider.addAppsByURL(selectedUrls);
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
tr('importedX', args: [plural('app', selectedUrls.length)]),
|
||||
showMessage(
|
||||
tr('importedX', args: [plural('apps', selectedUrls.length)]),
|
||||
context);
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
@@ -289,6 +316,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
}
|
||||
|
||||
var sourceStrings = <String, List<String>>{};
|
||||
sourceProvider.sources.where((e) => e.canSearch).forEach((s) {
|
||||
sourceStrings[s.name] = [s.name];
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
@@ -299,6 +331,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: settingsProvider.getExportDir(),
|
||||
builder: (context, snapshot) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -308,23 +345,93 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
onPressed: appsProvider.apps.isEmpty ||
|
||||
importInProgress
|
||||
? null
|
||||
: runObtainiumExport,
|
||||
child: Text(tr('obtainiumExport')))),
|
||||
: () {
|
||||
runObtainiumExport(pickOnly: true);
|
||||
},
|
||||
child: Text(tr('pickExportDir'),
|
||||
textAlign: TextAlign.center),
|
||||
)),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: outlineButtonStyle,
|
||||
onPressed: appsProvider.apps.isEmpty ||
|
||||
importInProgress ||
|
||||
snapshot.data == null
|
||||
? null
|
||||
: runObtainiumExport,
|
||||
child: Text(tr('obtainiumExport'),
|
||||
textAlign: TextAlign.center),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: outlineButtonStyle,
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: runObtainiumImport,
|
||||
child: Text(tr('obtainiumImport'))))
|
||||
child: Text(tr('obtainiumImport'),
|
||||
textAlign: TextAlign.center))),
|
||||
],
|
||||
),
|
||||
if (importInProgress)
|
||||
if (snapshot.data != null)
|
||||
Column(
|
||||
children: const [
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
GeneratedForm(
|
||||
items: [
|
||||
[
|
||||
GeneratedFormSwitch(
|
||||
'autoExportOnChanges',
|
||||
label: tr('autoExportOnChanges'),
|
||||
defaultValue: settingsProvider
|
||||
.autoExportOnChanges,
|
||||
)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch(
|
||||
'exportSettings',
|
||||
label: tr('includeSettings'),
|
||||
defaultValue: settingsProvider
|
||||
.exportSettings,
|
||||
)
|
||||
]
|
||||
],
|
||||
onValueChanges:
|
||||
(value, valid, isBuilding) {
|
||||
if (valid && !isBuilding) {
|
||||
if (value['autoExportOnChanges'] !=
|
||||
null) {
|
||||
settingsProvider
|
||||
.autoExportOnChanges = value[
|
||||
'autoExportOnChanges'] ==
|
||||
true;
|
||||
}
|
||||
if (value['exportSettings'] !=
|
||||
null) {
|
||||
settingsProvider.exportSettings =
|
||||
value['exportSettings'] ==
|
||||
true;
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (importInProgress)
|
||||
const Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 14,
|
||||
),
|
||||
@@ -340,6 +447,54 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
const Divider(
|
||||
height: 32,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: () async {
|
||||
var searchSourceName =
|
||||
await showDialog<
|
||||
List<String>?>(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext
|
||||
ctx) {
|
||||
return SelectionModal(
|
||||
title: tr(
|
||||
'selectX',
|
||||
args: [
|
||||
tr('source')
|
||||
]),
|
||||
entries:
|
||||
sourceStrings,
|
||||
selectedByDefault:
|
||||
false,
|
||||
onlyOneSelectionAllowed:
|
||||
true,
|
||||
titlesAreLinks:
|
||||
false,
|
||||
);
|
||||
}) ??
|
||||
[];
|
||||
var searchSource =
|
||||
sourceProvider.sources
|
||||
.where((e) =>
|
||||
searchSourceName
|
||||
.contains(
|
||||
e.name))
|
||||
.toList();
|
||||
if (searchSource.isNotEmpty) {
|
||||
runSourceSearch(
|
||||
searchSource[0]);
|
||||
}
|
||||
},
|
||||
child: Text(tr('searchX',
|
||||
args: [tr('source')])))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed:
|
||||
importInProgress ? null : urlListImport,
|
||||
@@ -355,27 +510,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
)),
|
||||
],
|
||||
),
|
||||
...sourceProvider.sources
|
||||
.where((element) => element.canSearch)
|
||||
.map((source) => Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: () {
|
||||
runSourceSearch(source);
|
||||
},
|
||||
child: Text(
|
||||
tr('searchX', args: [source.name])))
|
||||
]))
|
||||
.toList(),
|
||||
...sourceProvider.massUrlSources
|
||||
.map((source) => Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
...sourceProvider.massUrlSources.map((source) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
@@ -386,8 +522,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
},
|
||||
child: Text(
|
||||
tr('importX', args: [source.name])))
|
||||
]))
|
||||
.toList(),
|
||||
])),
|
||||
const Spacer(),
|
||||
const Divider(
|
||||
height: 32,
|
||||
@@ -398,7 +533,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
fontStyle: FontStyle.italic, fontSize: 12)),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
)
|
||||
),
|
||||
],
|
||||
)))
|
||||
]));
|
||||
@@ -449,114 +584,191 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
)
|
||||
]);
|
||||
}).toList()
|
||||
})
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: Text(tr('okay')))
|
||||
child: Text(tr('ok')))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class UrlSelectionModal extends StatefulWidget {
|
||||
UrlSelectionModal(
|
||||
class SelectionModal extends StatefulWidget {
|
||||
SelectionModal(
|
||||
{super.key,
|
||||
required this.urlsWithDescriptions,
|
||||
required this.entries,
|
||||
this.selectedByDefault = true,
|
||||
this.onlyOneSelectionAllowed = false});
|
||||
this.onlyOneSelectionAllowed = false,
|
||||
this.titlesAreLinks = true,
|
||||
this.title,
|
||||
this.deselectThese = const []});
|
||||
|
||||
Map<String, String> urlsWithDescriptions;
|
||||
String? title;
|
||||
Map<String, List<String>> entries;
|
||||
bool selectedByDefault;
|
||||
List<String> deselectThese;
|
||||
bool onlyOneSelectionAllowed;
|
||||
bool titlesAreLinks;
|
||||
|
||||
@override
|
||||
State<UrlSelectionModal> createState() => _UrlSelectionModalState();
|
||||
State<SelectionModal> createState() => _SelectionModalState();
|
||||
}
|
||||
|
||||
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||
Map<MapEntry<String, String>, bool> urlWithDescriptionSelections = {};
|
||||
class _SelectionModalState extends State<SelectionModal> {
|
||||
Map<MapEntry<String, List<String>>, bool> entrySelections = {};
|
||||
String filterRegex = '';
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
for (var url in widget.urlsWithDescriptions.entries) {
|
||||
urlWithDescriptionSelections.putIfAbsent(url,
|
||||
() => widget.selectedByDefault && !widget.onlyOneSelectionAllowed);
|
||||
for (var entry in widget.entries.entries) {
|
||||
entrySelections.putIfAbsent(
|
||||
entry,
|
||||
() =>
|
||||
widget.selectedByDefault &&
|
||||
!widget.onlyOneSelectionAllowed &&
|
||||
!widget.deselectThese.contains(entry.key));
|
||||
}
|
||||
if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
|
||||
selectOnlyOne(widget.urlsWithDescriptions.entries.first.key);
|
||||
selectOnlyOne(widget.entries.entries.first.key);
|
||||
}
|
||||
}
|
||||
|
||||
selectOnlyOne(String url) {
|
||||
for (var uwd in urlWithDescriptionSelections.keys) {
|
||||
urlWithDescriptionSelections[uwd] = uwd.key == url;
|
||||
for (var e in entrySelections.keys) {
|
||||
entrySelections[e] = e.key == url;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Map<MapEntry<String, List<String>>, bool> filteredEntrySelections = {};
|
||||
entrySelections.forEach((key, value) {
|
||||
var searchableText = key.value.isEmpty ? key.key : key.value[0];
|
||||
if (filterRegex.isEmpty || RegExp(filterRegex).hasMatch(searchableText)) {
|
||||
filteredEntrySelections.putIfAbsent(key, () => value);
|
||||
}
|
||||
});
|
||||
if (filterRegex.isNotEmpty && filteredEntrySelections.isEmpty) {
|
||||
entrySelections.forEach((key, value) {
|
||||
var searchableText = key.value.isEmpty ? key.key : key.value[0];
|
||||
if (filterRegex.isEmpty ||
|
||||
RegExp(filterRegex, caseSensitive: false)
|
||||
.hasMatch(searchableText)) {
|
||||
filteredEntrySelections.putIfAbsent(key, () => value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(
|
||||
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
||||
title: Text(widget.title ?? tr('pick')),
|
||||
content: Column(children: [
|
||||
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
||||
GeneratedForm(
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('filter',
|
||||
label: tr('filter'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
]
|
||||
],
|
||||
onValueChanges: (value, valid, isBuilding) {
|
||||
if (valid && !isBuilding) {
|
||||
if (value['filter'] != null) {
|
||||
setState(() {
|
||||
filterRegex = value['filter'];
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
...filteredEntrySelections.keys.map((entry) {
|
||||
selectThis(bool? value) {
|
||||
setState(() {
|
||||
value ??= false;
|
||||
if (value! && widget.onlyOneSelectionAllowed) {
|
||||
selectOnlyOne(urlWithD.key);
|
||||
selectOnlyOne(entry.key);
|
||||
} else {
|
||||
urlWithDescriptionSelections[urlWithD] = value!;
|
||||
entrySelections[entry] = value!;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var urlLink = GestureDetector(
|
||||
onTap: () {
|
||||
launchUrlString(urlWithD.key,
|
||||
onTap: !widget.titlesAreLinks
|
||||
? null
|
||||
: () {
|
||||
launchUrlString(entry.key,
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text(
|
||||
Uri.parse(urlWithD.key).path.substring(1),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
entry.value.isEmpty ? entry.key : entry.value[0],
|
||||
style: TextStyle(
|
||||
decoration: widget.titlesAreLinks
|
||||
? TextDecoration.underline
|
||||
: null,
|
||||
fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
if (widget.titlesAreLinks)
|
||||
Text(
|
||||
Uri.parse(entry.key).host,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline, fontSize: 12),
|
||||
)
|
||||
],
|
||||
));
|
||||
|
||||
var descriptionText = Text(
|
||||
urlWithD.value.length > 128
|
||||
? '${urlWithD.value.substring(0, 128)}...'
|
||||
: urlWithD.value,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||
var descriptionText = entry.value.length <= 1
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
entry.value[1].length > 128
|
||||
? '${entry.value[1].substring(0, 128)}...'
|
||||
: entry.value[1],
|
||||
style: const TextStyle(
|
||||
fontStyle: FontStyle.italic, fontSize: 12),
|
||||
);
|
||||
|
||||
var selectedUrlsWithDs = urlWithDescriptionSelections.entries
|
||||
.where((e) => e.value)
|
||||
.toList();
|
||||
var selectedEntries =
|
||||
entrySelections.entries.where((e) => e.value).toList();
|
||||
|
||||
var singleSelectTile = ListTile(
|
||||
title: urlLink,
|
||||
subtitle: GestureDetector(
|
||||
title: GestureDetector(
|
||||
onTap: widget.titlesAreLinks
|
||||
? null
|
||||
: () {
|
||||
selectThis(!(entrySelections[entry] ?? false));
|
||||
},
|
||||
child: urlLink,
|
||||
),
|
||||
subtitle: entry.value.length <= 1
|
||||
? null
|
||||
: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectOnlyOne(urlWithD.key);
|
||||
selectOnlyOne(entry.key);
|
||||
});
|
||||
},
|
||||
child: descriptionText,
|
||||
),
|
||||
leading: Radio<String>(
|
||||
value: urlWithD.key,
|
||||
groupValue: selectedUrlsWithDs.isEmpty
|
||||
value: entry.key,
|
||||
groupValue: selectedEntries.isEmpty
|
||||
? null
|
||||
: selectedUrlsWithDs.first.key.key,
|
||||
: selectedEntries.first.key.key,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectOnlyOne(urlWithD.key);
|
||||
selectOnlyOne(entry.key);
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -564,7 +776,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||
|
||||
var multiSelectTile = Row(children: [
|
||||
Checkbox(
|
||||
value: urlWithDescriptionSelections[urlWithD],
|
||||
value: entrySelections[entry],
|
||||
onChanged: (value) {
|
||||
selectThis(value);
|
||||
}),
|
||||
@@ -579,11 +791,19 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
urlLink,
|
||||
GestureDetector(
|
||||
onTap: widget.titlesAreLinks
|
||||
? null
|
||||
: () {
|
||||
selectThis(!(entrySelections[entry] ?? false));
|
||||
},
|
||||
child: urlLink,
|
||||
),
|
||||
entry.value.length <= 1
|
||||
? const SizedBox.shrink()
|
||||
: GestureDetector(
|
||||
onTap: () {
|
||||
selectThis(
|
||||
!(urlWithDescriptionSelections[urlWithD] ?? false));
|
||||
selectThis(!(entrySelections[entry] ?? false));
|
||||
},
|
||||
child: descriptionText,
|
||||
),
|
||||
@@ -606,24 +826,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||
},
|
||||
child: Text(tr('cancel'))),
|
||||
TextButton(
|
||||
onPressed:
|
||||
urlWithDescriptionSelections.values.where((b) => b).isEmpty
|
||||
onPressed: entrySelections.values.where((b) => b).isEmpty
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(context).pop(urlWithDescriptionSelections
|
||||
.entries
|
||||
Navigator.of(context).pop(entrySelections.entries
|
||||
.where((entry) => entry.value)
|
||||
.map((e) => e.key.key)
|
||||
.toList());
|
||||
},
|
||||
child: Text(widget.onlyOneSelectionAllowed
|
||||
? tr('pick')
|
||||
: tr('importX', args: [
|
||||
plural(
|
||||
'url',
|
||||
urlWithDescriptionSelections.values
|
||||
.where((b) => b)
|
||||
.length)
|
||||
: tr('selectX', args: [
|
||||
entrySelections.values.where((b) => b).length.toString()
|
||||
])))
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:equations/equations.dart';
|
||||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
@@ -8,10 +9,12 @@ import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/logs_provider.dart';
|
||||
import 'package:obtainium/providers/native_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:shizuku_apk_installer/shizuku_apk_installer.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
@@ -21,71 +24,179 @@ class SettingsPage extends StatefulWidget {
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
// Generates a random light color
|
||||
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
|
||||
Color generateRandomLightColor() {
|
||||
// Create a random number generator
|
||||
final Random random = Random();
|
||||
|
||||
// Generate random hue, saturation, and value values
|
||||
final double hue = random.nextDouble() * 360;
|
||||
final double saturation = 0.5 + random.nextDouble() * 0.5;
|
||||
final double value = 0.9 + random.nextDouble() * 0.1;
|
||||
|
||||
// Create a HSV color with the random values
|
||||
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
List<int> updateIntervalNodes = [
|
||||
15, 30, 60, 120, 180, 360, 720, 1440, 4320, 10080, 20160, 43200];
|
||||
int updateInterval = 0;
|
||||
late SplineInterpolation updateIntervalInterpolator; // 🤓
|
||||
String updateIntervalLabel = tr('neverManualOnly');
|
||||
bool showIntervalLabel = true;
|
||||
final Map<ColorSwatch<Object>, String> colorsNameMap =
|
||||
<ColorSwatch<Object>, String> {
|
||||
ColorTools.createPrimarySwatch(obtainiumThemeColor): 'Obtainium'
|
||||
};
|
||||
|
||||
void initUpdateIntervalInterpolator() {
|
||||
List<InterpolationNode> nodes = [];
|
||||
for (final (index, element) in updateIntervalNodes.indexed) {
|
||||
nodes.add(InterpolationNode(x: index.toDouble()+1, y: element.toDouble()));
|
||||
}
|
||||
updateIntervalInterpolator = SplineInterpolation(nodes: nodes);
|
||||
}
|
||||
|
||||
void processIntervalSliderValue(double val) {
|
||||
if (val < 0.5) {
|
||||
updateInterval = 0;
|
||||
updateIntervalLabel = tr('neverManualOnly');
|
||||
return;
|
||||
}
|
||||
int valInterpolated = 0;
|
||||
if (val < 1) {
|
||||
valInterpolated = 15;
|
||||
} else {
|
||||
valInterpolated = updateIntervalInterpolator.compute(val).round();
|
||||
}
|
||||
if (valInterpolated < 60) {
|
||||
updateInterval = valInterpolated;
|
||||
updateIntervalLabel = plural('minute', valInterpolated);
|
||||
} else if (valInterpolated < 8 * 60) {
|
||||
int valRounded = (valInterpolated / 15).floor() * 15;
|
||||
updateInterval = valRounded;
|
||||
updateIntervalLabel = plural('hour', valRounded ~/ 60);
|
||||
int mins = valRounded % 60;
|
||||
if (mins != 0) updateIntervalLabel += " ${plural('minute', mins)}";
|
||||
} else if (valInterpolated < 24 * 60) {
|
||||
int valRounded = (valInterpolated / 30).floor() * 30;
|
||||
updateInterval = valRounded;
|
||||
updateIntervalLabel = plural('hour', valRounded / 60);
|
||||
} else if (valInterpolated < 7 * 24 * 60){
|
||||
int valRounded = (valInterpolated / (12 * 60)).floor() * 12 * 60;
|
||||
updateInterval = valRounded;
|
||||
updateIntervalLabel = plural('day', valRounded / (24 * 60));
|
||||
} else {
|
||||
int valRounded = (valInterpolated / (24 * 60)).floor() * 24 * 60;
|
||||
updateInterval = valRounded;
|
||||
updateIntervalLabel = plural('day', valRounded ~/ (24 * 60));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
if (settingsProvider.prefs == null) {
|
||||
settingsProvider.initializeSettings();
|
||||
if (settingsProvider.prefs == null) settingsProvider.initializeSettings();
|
||||
initUpdateIntervalInterpolator();
|
||||
processIntervalSliderValue(settingsProvider.updateIntervalSliderVal);
|
||||
|
||||
var followSystemThemeExplanation = FutureBuilder(
|
||||
builder: (ctx, val) {
|
||||
return ((val.data?.version.sdkInt ?? 30) < 29) ?
|
||||
Text(tr('followSystemThemeExplanation'),
|
||||
style: Theme.of(context).textTheme.labelSmall)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
future: DeviceInfoPlugin().androidInfo
|
||||
);
|
||||
|
||||
Future<bool> colorPickerDialog() async {
|
||||
return ColorPicker(
|
||||
color: settingsProvider.themeColor,
|
||||
onColorChanged: (Color color) =>
|
||||
setState(() =>
|
||||
settingsProvider.themeColor = color
|
||||
),
|
||||
actionButtons: const ColorPickerActionButtons(
|
||||
okButton: true,
|
||||
closeButton: true,
|
||||
dialogActionButtons: false,
|
||||
),
|
||||
pickersEnabled: const <ColorPickerType, bool>{
|
||||
ColorPickerType.both: false,
|
||||
ColorPickerType.primary: false,
|
||||
ColorPickerType.accent: false,
|
||||
ColorPickerType.bw: false,
|
||||
ColorPickerType.custom: true,
|
||||
ColorPickerType.wheel: true,
|
||||
},
|
||||
pickerTypeLabels: <ColorPickerType, String>{
|
||||
ColorPickerType.custom: tr('standard'),
|
||||
ColorPickerType.wheel: tr('custom')
|
||||
},
|
||||
title: Text(tr('selectX', args: [tr('colour')]),
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
wheelDiameter: 192,
|
||||
wheelSquareBorderRadius: 32,
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
enableShadesSelection: false,
|
||||
customColorSwatchesAndNames: colorsNameMap,
|
||||
showMaterialName: true,
|
||||
showColorName: true,
|
||||
materialNameTextStyle: Theme.of(context).textTheme.bodySmall,
|
||||
colorNameTextStyle: Theme.of(context).textTheme.bodySmall,
|
||||
copyPasteBehavior: const ColorPickerCopyPasteBehavior(longPressMenu: true),
|
||||
).showPickerDialog(
|
||||
context,
|
||||
transitionBuilder: (BuildContext context,
|
||||
Animation<double> a1, Animation<double> a2, Widget widget) {
|
||||
final double curvedValue = Curves.easeInCubic.transform(a1.value);
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.diagonal3Values(curvedValue, curvedValue, 1),
|
||||
child: Opacity(
|
||||
opacity: curvedValue,
|
||||
child: widget
|
||||
),
|
||||
);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 250),
|
||||
);
|
||||
}
|
||||
|
||||
var themeDropdown = DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('theme')),
|
||||
value: settingsProvider.theme,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.dark,
|
||||
child: Text(tr('dark')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.light,
|
||||
child: Text(tr('light')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.system,
|
||||
child: Text(tr('followSystem')),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.theme = value;
|
||||
}
|
||||
var colorPicker = ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(tr('selectX', args: [tr('colour')])),
|
||||
subtitle: Text("${ColorTools.nameThatColor(settingsProvider.themeColor)} "
|
||||
"(${ColorTools.materialNameAndCode(settingsProvider.themeColor,
|
||||
colorSwatchNameMap: colorsNameMap)})"),
|
||||
trailing: ColorIndicator(
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
color: settingsProvider.themeColor,
|
||||
onSelectFocus: false,
|
||||
onSelect: () async {
|
||||
final Color colorBeforeDialog = settingsProvider.themeColor;
|
||||
if (!(await colorPickerDialog())) {
|
||||
setState(() {
|
||||
settingsProvider.themeColor = colorBeforeDialog;
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
var colourDropdown = DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('colour')),
|
||||
value: settingsProvider.colour,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ColourSettings.basic,
|
||||
child: Text(tr('obtainium')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ColourSettings.materialYou,
|
||||
child: Text(tr('materialYou')),
|
||||
)
|
||||
],
|
||||
var useMaterialThemeSwitch = FutureBuilder(
|
||||
builder: (ctx, val) {
|
||||
return ((val.data?.version.sdkInt ?? 0) >= 31) ?
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('useMaterialYou'))),
|
||||
Switch(
|
||||
value: settingsProvider.useMaterialYou,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.colour = value;
|
||||
}
|
||||
});
|
||||
settingsProvider.useMaterialYou = value;
|
||||
})
|
||||
],
|
||||
) : const SizedBox.shrink();
|
||||
},
|
||||
future: DeviceInfoPlugin().androidInfo
|
||||
);
|
||||
|
||||
var sortDropdown = DropdownButtonFormField(
|
||||
isExpanded: true,
|
||||
@@ -144,8 +255,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Text(tr('followSystem')),
|
||||
),
|
||||
...supportedLocales.map((e) => DropdownMenuItem(
|
||||
value: e.toLanguageTag(),
|
||||
child: Text(e.toLanguageTag().toUpperCase()),
|
||||
value: e.key.toLanguageTag(),
|
||||
child: Text(e.value),
|
||||
))
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -157,35 +268,34 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
});
|
||||
|
||||
var intervalDropdown = DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
|
||||
value: settingsProvider.updateInterval,
|
||||
items: updateIntervals.map((e) {
|
||||
int displayNum = (e < 60
|
||||
? e
|
||||
: e < 1440
|
||||
? e / 60
|
||||
: e / 1440)
|
||||
.round();
|
||||
String display = e == 0
|
||||
? tr('neverManualOnly')
|
||||
: (e < 60
|
||||
? plural('minute', displayNum)
|
||||
: e < 1440
|
||||
? plural('hour', displayNum)
|
||||
: plural('day', displayNum));
|
||||
return DropdownMenuItem(value: e, child: Text(display));
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.updateInterval = value;
|
||||
}
|
||||
var intervalSlider = Slider(
|
||||
value: settingsProvider.updateIntervalSliderVal,
|
||||
max: updateIntervalNodes.length.toDouble(),
|
||||
divisions: updateIntervalNodes.length * 20,
|
||||
label: updateIntervalLabel,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
settingsProvider.updateIntervalSliderVal = value;
|
||||
processIntervalSliderValue(value);
|
||||
});
|
||||
},
|
||||
onChangeStart: (double value) {
|
||||
setState(() {
|
||||
showIntervalLabel = false;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double value) {
|
||||
setState(() {
|
||||
showIntervalLabel = true;
|
||||
settingsProvider.updateInterval = updateInterval;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
var sourceSpecificFields = sourceProvider.sources.map((e) {
|
||||
if (e.additionalSourceSpecificSettingFormItems.isNotEmpty) {
|
||||
if (e.sourceConfigSettingFormItems.isNotEmpty) {
|
||||
return GeneratedForm(
|
||||
items: e.additionalSourceSpecificSettingFormItems.map((e) {
|
||||
items: e.sourceConfigSettingFormItems.map((e) {
|
||||
e.defaultValue = settingsProvider.getSettingString(e.key);
|
||||
return [e];
|
||||
}).toList(),
|
||||
@@ -201,10 +311,18 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
});
|
||||
|
||||
const height8 = SizedBox(
|
||||
height: 8,
|
||||
);
|
||||
|
||||
const height16 = SizedBox(
|
||||
height: 16,
|
||||
);
|
||||
|
||||
const height32 = SizedBox(
|
||||
height: 32,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
@@ -218,25 +336,277 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr('appearance'),
|
||||
tr('updates'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
themeDropdown,
|
||||
//intervalDropdown,
|
||||
height16,
|
||||
if (showIntervalLabel) SizedBox(
|
||||
child: Text("${tr('bgUpdateCheckInterval')}: $updateIntervalLabel")
|
||||
) else const SizedBox(height: 16),
|
||||
intervalSlider,
|
||||
FutureBuilder(
|
||||
builder: (ctx, val) {
|
||||
return (settingsProvider.updateInterval > 0) && (((val.data?.version.sdkInt ?? 0) >= 30) || settingsProvider.useShizuku)
|
||||
? Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr(
|
||||
'enableBackgroundUpdates'))),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.enableBackgroundUpdates,
|
||||
onChanged: (value) {
|
||||
settingsProvider
|
||||
.enableBackgroundUpdates =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height8,
|
||||
Text(tr('backgroundUpdateReqsExplanation'),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall),
|
||||
Text(tr('backgroundUpdateLimitsExplanation'),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall),
|
||||
height8,
|
||||
if (settingsProvider
|
||||
.enableBackgroundUpdates)
|
||||
Column(
|
||||
children: [
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr(
|
||||
'bgUpdatesOnWiFiOnly'))),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.bgUpdatesOnWiFiOnly,
|
||||
onChanged: (value) {
|
||||
settingsProvider
|
||||
.bgUpdatesOnWiFiOnly =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
future: DeviceInfoPlugin().androidInfo),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(tr('useBlackTheme')),
|
||||
Flexible(child: Text(tr('checkOnStart'))),
|
||||
Switch(
|
||||
value: settingsProvider.checkOnStart,
|
||||
onChanged: (value) {
|
||||
settingsProvider.checkOnStart = value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr('checkUpdateOnDetailPage'))),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.checkUpdateOnDetailPage,
|
||||
onChanged: (value) {
|
||||
settingsProvider.checkUpdateOnDetailPage =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr(
|
||||
'onlyCheckInstalledOrTrackOnlyApps'))),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.onlyCheckInstalledOrTrackOnlyApps,
|
||||
onChanged: (value) {
|
||||
settingsProvider
|
||||
.onlyCheckInstalledOrTrackOnlyApps =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child:
|
||||
Text(tr('removeOnExternalUninstall'))),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.removeOnExternalUninstall,
|
||||
onChanged: (value) {
|
||||
settingsProvider
|
||||
.removeOnExternalUninstall = value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('parallelDownloads'))),
|
||||
Switch(
|
||||
value: settingsProvider.parallelDownloads,
|
||||
onChanged: (value) {
|
||||
settingsProvider.parallelDownloads =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(tr(
|
||||
'beforeNewInstallsShareToAppVerifier')),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
launchUrlString(
|
||||
'https://github.com/soupslurpr/AppVerifier',
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
},
|
||||
child: Text(
|
||||
tr('about'),
|
||||
style: const TextStyle(
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
fontSize: 12),
|
||||
)),
|
||||
],
|
||||
)),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.beforeNewInstallsShareToAppVerifier,
|
||||
onChanged: (value) {
|
||||
settingsProvider
|
||||
.beforeNewInstallsShareToAppVerifier =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('useShizuku'))),
|
||||
Switch(
|
||||
value: settingsProvider.useShizuku,
|
||||
onChanged: (useShizuku) {
|
||||
if (useShizuku) {
|
||||
ShizukuApkInstaller.checkPermission().then((resCode) {
|
||||
settingsProvider.useShizuku = resCode!.startsWith('granted');
|
||||
switch(resCode){
|
||||
case 'binder_not_found':
|
||||
showError(ObtainiumError(tr('shizukuBinderNotFound')), context);
|
||||
case 'old_shizuku':
|
||||
showError(ObtainiumError(tr('shizukuOld')), context);
|
||||
case 'old_android_with_adb':
|
||||
showError(ObtainiumError(tr('shizukuOldAndroidWithADB')), context);
|
||||
case 'denied':
|
||||
showError(ObtainiumError(tr('cancelled')), context);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
settingsProvider.useShizuku = false;
|
||||
}
|
||||
})
|
||||
],
|
||||
),
|
||||
height32,
|
||||
Text(
|
||||
tr('sourceSpecific'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
...sourceSpecificFields,
|
||||
height32,
|
||||
Text(
|
||||
tr('appearance'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('theme')),
|
||||
value: settingsProvider.theme,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.system,
|
||||
child: Text(tr('followSystem')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.light,
|
||||
child: Text(tr('light')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.dark,
|
||||
child: Text(tr('dark')),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.theme = value;
|
||||
}
|
||||
}),
|
||||
height8,
|
||||
if (settingsProvider.theme == ThemeSettings.system)
|
||||
followSystemThemeExplanation,
|
||||
height16,
|
||||
if (settingsProvider.theme != ThemeSettings.light)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('useBlackTheme'))),
|
||||
Switch(
|
||||
value: settingsProvider.useBlackTheme,
|
||||
onChanged: (value) {
|
||||
settingsProvider.useBlackTheme = value;
|
||||
})
|
||||
],
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
colourDropdown,
|
||||
height16,
|
||||
height8,
|
||||
useMaterialThemeSwitch,
|
||||
if (!settingsProvider.useMaterialYou) colorPicker,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -250,11 +620,40 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
height16,
|
||||
localeDropdown,
|
||||
FutureBuilder(
|
||||
builder: (ctx, val) {
|
||||
return (val.data?.version.sdkInt ?? 0) >= 34
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(tr('showWebInAppView')),
|
||||
Flexible(child: Text(tr('useSystemFont'))),
|
||||
Switch(
|
||||
value: settingsProvider.useSystemFont,
|
||||
onChanged: (useSystemFont) {
|
||||
if (useSystemFont) {
|
||||
NativeFeatures.loadSystemFont().then((val) {
|
||||
settingsProvider.useSystemFont = true;
|
||||
});
|
||||
} else {
|
||||
settingsProvider.useSystemFont = false;
|
||||
}
|
||||
})
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
future: DeviceInfoPlugin().androidInfo),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('showWebInAppView'))),
|
||||
Switch(
|
||||
value: settingsProvider.showAppWebpage,
|
||||
onChanged: (value) {
|
||||
@@ -266,7 +665,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(tr('pinUpdates')),
|
||||
Flexible(child: Text(tr('pinUpdates'))),
|
||||
Switch(
|
||||
value: settingsProvider.pinUpdates,
|
||||
onChanged: (value) {
|
||||
@@ -278,7 +677,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(tr('groupByCategory')),
|
||||
Flexible(
|
||||
child: Text(
|
||||
tr('moveNonInstalledAppsToBottom'))),
|
||||
Switch(
|
||||
value: settingsProvider.buryNonInstalled,
|
||||
onChanged: (value) {
|
||||
settingsProvider.buryNonInstalled = value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('groupByCategory'))),
|
||||
Switch(
|
||||
value: settingsProvider.groupByCategory,
|
||||
onChanged: (value) {
|
||||
@@ -286,31 +699,91 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
})
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 16,
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child:
|
||||
Text(tr('dontShowTrackOnlyWarnings'))),
|
||||
Switch(
|
||||
value:
|
||||
settingsProvider.hideTrackOnlyWarning,
|
||||
onChanged: (value) {
|
||||
settingsProvider.hideTrackOnlyWarning =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Text(
|
||||
tr('updates'),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child:
|
||||
Text(tr('dontShowAPKOriginWarnings'))),
|
||||
Switch(
|
||||
value:
|
||||
settingsProvider.hideAPKOriginWarning,
|
||||
onChanged: (value) {
|
||||
settingsProvider.hideAPKOriginWarning =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
intervalDropdown,
|
||||
const Divider(
|
||||
height: 48,
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr('disablePageTransitions'))),
|
||||
Switch(
|
||||
value:
|
||||
settingsProvider.disablePageTransitions,
|
||||
onChanged: (value) {
|
||||
settingsProvider.disablePageTransitions =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
Text(
|
||||
tr('sourceSpecific'),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr('reversePageTransitions'))),
|
||||
Switch(
|
||||
value:
|
||||
settingsProvider.reversePageTransitions,
|
||||
onChanged: settingsProvider
|
||||
.disablePageTransitions
|
||||
? null
|
||||
: (value) {
|
||||
settingsProvider
|
||||
.reversePageTransitions = value;
|
||||
})
|
||||
],
|
||||
),
|
||||
...sourceSpecificFields,
|
||||
const Divider(
|
||||
height: 48,
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr('highlightTouchTargets'))),
|
||||
Switch(
|
||||
value:
|
||||
settingsProvider.highlightTouchTargets,
|
||||
onChanged: (value) {
|
||||
settingsProvider.highlightTouchTargets =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height32,
|
||||
Text(
|
||||
tr('categories'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
height16,
|
||||
@@ -342,7 +815,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
onPressed: () {
|
||||
context.read<LogsProvider>().get().then((logs) {
|
||||
if (logs.isEmpty) {
|
||||
showError(ObtainiumError(tr('noLogs')), context);
|
||||
showMessage(
|
||||
ObtainiumError(tr('noLogs')), context);
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -356,7 +830,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
label: Text(tr('appLogs'))),
|
||||
],
|
||||
),
|
||||
height16,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
22
lib/providers/native_provider.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:android_system_font/android_system_font.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class NativeFeatures {
|
||||
static bool _systemFontLoaded = false;
|
||||
|
||||
static Future<ByteData> _readFileBytes(String path) async {
|
||||
var bytes = await File(path).readAsBytes();
|
||||
return ByteData.view(bytes.buffer);
|
||||
}
|
||||
|
||||
static Future loadSystemFont() async {
|
||||
if (_systemFontLoaded) return;
|
||||
var fontLoader = FontLoader('SystemFont');
|
||||
var fontFilePath = await AndroidSystemFont().getFilePath();
|
||||
fontLoader.addFont(_readFileBytes(fontFilePath!));
|
||||
fontLoader.load();
|
||||
_systemFontLoaded = true;
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,13 @@ class ObtainiumNotification {
|
||||
}
|
||||
|
||||
class UpdateNotification extends ObtainiumNotification {
|
||||
UpdateNotification(List<App> updates)
|
||||
UpdateNotification(List<App> updates, {int? id})
|
||||
: super(
|
||||
2,
|
||||
id ?? 2,
|
||||
tr('updatesAvailable'),
|
||||
'',
|
||||
'UPDATES_AVAILABLE',
|
||||
tr('updatesAvailable'),
|
||||
tr('updatesAvailableNotifChannel'),
|
||||
tr('updatesAvailableNotifDescription'),
|
||||
Importance.max) {
|
||||
message = updates.isEmpty
|
||||
@@ -41,33 +41,69 @@ class UpdateNotification extends ObtainiumNotification {
|
||||
}
|
||||
|
||||
class SilentUpdateNotification extends ObtainiumNotification {
|
||||
SilentUpdateNotification(List<App> updates)
|
||||
: super(3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'),
|
||||
tr('appsUpdatedNotifDescription'), Importance.defaultImportance) {
|
||||
SilentUpdateNotification(List<App> updates, bool succeeded, {int? id})
|
||||
: super(
|
||||
id ?? 3,
|
||||
succeeded
|
||||
? tr('appsUpdated')
|
||||
: tr('appsNotUpdated'),
|
||||
'',
|
||||
'APPS_UPDATED',
|
||||
tr('appsUpdatedNotifChannel'),
|
||||
tr('appsUpdatedNotifDescription'),
|
||||
Importance.defaultImportance) {
|
||||
message = updates.length == 1
|
||||
? tr('xWasUpdatedToY',
|
||||
? tr(succeeded
|
||||
? 'xWasUpdatedToY'
|
||||
: 'xWasNotUpdatedToY',
|
||||
args: [updates[0].finalName, updates[0].latestVersion])
|
||||
: plural('xAndNMoreUpdatesInstalled', updates.length - 1,
|
||||
: plural(succeeded
|
||||
? 'xAndNMoreUpdatesInstalled'
|
||||
: "xAndNMoreUpdatesFailed",
|
||||
updates.length - 1, args: [updates[0].finalName, (updates.length - 1).toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
class SilentUpdateAttemptNotification extends ObtainiumNotification {
|
||||
SilentUpdateAttemptNotification(List<App> updates, {int? id})
|
||||
: super(
|
||||
id ?? 3,
|
||||
tr('appsPossiblyUpdated'),
|
||||
'',
|
||||
'APPS_POSSIBLY_UPDATED',
|
||||
tr('appsPossiblyUpdatedNotifChannel'),
|
||||
tr('appsPossiblyUpdatedNotifDescription'),
|
||||
Importance.defaultImportance) {
|
||||
message = updates.length == 1
|
||||
? tr('xWasPossiblyUpdatedToY',
|
||||
args: [updates[0].finalName, updates[0].latestVersion])
|
||||
: plural('xAndNMoreUpdatesPossiblyInstalled', updates.length - 1,
|
||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
||||
ErrorCheckingUpdatesNotification(String error)
|
||||
ErrorCheckingUpdatesNotification(String error, {int? id})
|
||||
: super(
|
||||
5,
|
||||
id ?? 5,
|
||||
tr('errorCheckingUpdates'),
|
||||
error,
|
||||
'BG_UPDATE_CHECK_ERROR',
|
||||
tr('errorCheckingUpdates'),
|
||||
tr('errorCheckingUpdatesNotifChannel'),
|
||||
tr('errorCheckingUpdatesNotifDescription'),
|
||||
Importance.high);
|
||||
}
|
||||
|
||||
class AppsRemovedNotification extends ObtainiumNotification {
|
||||
AppsRemovedNotification(List<List<String>> namedReasons)
|
||||
: super(6, tr('appsRemoved'), '', 'APPS_REMOVED', tr('appsRemoved'),
|
||||
tr('appsRemovedNotifDescription'), Importance.max) {
|
||||
: super(
|
||||
6,
|
||||
tr('appsRemoved'),
|
||||
'',
|
||||
'APPS_REMOVED',
|
||||
tr('appsRemovedNotifChannel'),
|
||||
tr('appsRemovedNotifDescription'),
|
||||
Importance.max) {
|
||||
message = '';
|
||||
for (var r in namedReasons) {
|
||||
message += '${tr('xWasRemovedDueToErrorY', args: [r[0], r[1]])} \n';
|
||||
@@ -83,30 +119,45 @@ class DownloadNotification extends ObtainiumNotification {
|
||||
tr('downloadingX', args: [appName]),
|
||||
'',
|
||||
'APP_DOWNLOADING',
|
||||
tr('downloadingX', args: [tr('app')]),
|
||||
tr('downloadingXNotifChannel', args: [tr('app')]),
|
||||
tr('downloadNotifDescription'),
|
||||
Importance.low,
|
||||
onlyAlertOnce: true,
|
||||
progPercent: progPercent);
|
||||
}
|
||||
|
||||
class DownloadedNotification extends ObtainiumNotification {
|
||||
DownloadedNotification(String fileName, String downloadUrl)
|
||||
: super(
|
||||
downloadUrl.hashCode,
|
||||
tr('downloadedX', args: [fileName]),
|
||||
'',
|
||||
'FILE_DOWNLOADED',
|
||||
tr('downloadedXNotifChannel', args: [tr('app')]),
|
||||
tr('downloadedX', args: [tr('app')]),
|
||||
Importance.defaultImportance);
|
||||
}
|
||||
|
||||
final completeInstallationNotification = ObtainiumNotification(
|
||||
1,
|
||||
tr('completeAppInstallation'),
|
||||
tr('obtainiumMustBeOpenToInstallApps'),
|
||||
'COMPLETE_INSTALL',
|
||||
tr('completeAppInstallation'),
|
||||
tr('completeAppInstallationNotifChannel'),
|
||||
tr('completeAppInstallationNotifDescription'),
|
||||
Importance.max);
|
||||
|
||||
final checkingUpdatesNotification = ObtainiumNotification(
|
||||
class CheckingUpdatesNotification extends ObtainiumNotification {
|
||||
CheckingUpdatesNotification(String appName)
|
||||
: super(
|
||||
4,
|
||||
tr('checkingForUpdates'),
|
||||
'',
|
||||
appName,
|
||||
'BG_UPDATE_CHECK',
|
||||
tr('checkingForUpdates'),
|
||||
tr('checkingForUpdatesNotifChannel'),
|
||||
tr('checkingForUpdatesNotifDescription'),
|
||||
Importance.min);
|
||||
}
|
||||
|
||||
class NotificationsProvider {
|
||||
FlutterLocalNotificationsPlugin notifications =
|
||||
@@ -167,7 +218,8 @@ class NotificationsProvider {
|
||||
progress: progPercent ?? 0,
|
||||
maxProgress: 100,
|
||||
showProgress: progPercent != null,
|
||||
onlyAlertOnce: onlyAlertOnce)));
|
||||
onlyAlertOnce: onlyAlertOnce,
|
||||
indeterminate: progPercent != null && progPercent < 0)));
|
||||
}
|
||||
|
||||
Future<void> notify(ObtainiumNotification notif,
|
||||
|
||||
@@ -9,38 +9,51 @@ import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:shared_storage/shared_storage.dart' as saf;
|
||||
|
||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().hosts[0]}';
|
||||
String obtainiumId = 'dev.imranr.obtainium';
|
||||
String obtainiumUrl = 'https://github.com/ImranR98/Obtainium';
|
||||
Color obtainiumThemeColor = const Color(0xFF6438B5);
|
||||
|
||||
enum ThemeSettings { system, light, dark }
|
||||
|
||||
enum ColourSettings { basic, materialYou }
|
||||
|
||||
enum SortColumnSettings { added, nameAuthor, authorName, releaseDate }
|
||||
|
||||
enum SortOrderSettings { ascending, descending }
|
||||
|
||||
const maxAPIRateLimitMinutes = 30;
|
||||
const minUpdateIntervalMinutes = maxAPIRateLimitMinutes + 30;
|
||||
const maxUpdateIntervalMinutes = 4320;
|
||||
List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
|
||||
.where((element) =>
|
||||
(element >= minUpdateIntervalMinutes &&
|
||||
element <= maxUpdateIntervalMinutes) ||
|
||||
element == 0)
|
||||
.toList();
|
||||
|
||||
class SettingsProvider with ChangeNotifier {
|
||||
SharedPreferences? prefs;
|
||||
String? defaultAppDir;
|
||||
bool justStarted = true;
|
||||
|
||||
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
||||
|
||||
// Not done in constructor as we want to be able to await it
|
||||
Future<void> initializeSettings() async {
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
defaultAppDir = (await getExternalStorageDirectory())!.path;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get useSystemFont {
|
||||
return prefs?.getBool('useSystemFont') ?? false;
|
||||
}
|
||||
|
||||
set useSystemFont(bool useSystemFont) {
|
||||
prefs?.setBool('useSystemFont', useSystemFont);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get useShizuku{
|
||||
return prefs?.getBool('useShizuku') ?? false;
|
||||
}
|
||||
|
||||
set useShizuku(bool useShizuku) {
|
||||
prefs?.setBool('useShizuku', useShizuku);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -54,13 +67,23 @@ class SettingsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ColourSettings get colour {
|
||||
return ColourSettings
|
||||
.values[prefs?.getInt('colour') ?? ColourSettings.basic.index];
|
||||
Color get themeColor {
|
||||
int? colorCode = prefs?.getInt('themeColor');
|
||||
return (colorCode != null) ?
|
||||
Color(colorCode) : obtainiumThemeColor;
|
||||
}
|
||||
|
||||
set colour(ColourSettings t) {
|
||||
prefs?.setInt('colour', t.index);
|
||||
set themeColor(Color themeColor) {
|
||||
prefs?.setInt('themeColor', themeColor.value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get useMaterialYou {
|
||||
return prefs?.getBool('useMaterialYou') ?? false;
|
||||
}
|
||||
|
||||
set useMaterialYou(bool useMaterialYou) {
|
||||
prefs?.setBool('useMaterialYou', useMaterialYou);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -74,21 +97,29 @@ class SettingsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
int get updateInterval {
|
||||
var min = prefs?.getInt('updateInterval') ?? 360;
|
||||
if (!updateIntervals.contains(min)) {
|
||||
var temp = updateIntervals[0];
|
||||
for (var i in updateIntervals) {
|
||||
if (min > i && i != 0) {
|
||||
temp = i;
|
||||
}
|
||||
}
|
||||
min = temp;
|
||||
}
|
||||
return min;
|
||||
return prefs?.getInt('updateInterval') ?? 360;
|
||||
}
|
||||
|
||||
set updateInterval(int min) {
|
||||
prefs?.setInt('updateInterval', (min < 15 && min != 0) ? 15 : min);
|
||||
prefs?.setInt('updateInterval', min);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double get updateIntervalSliderVal {
|
||||
return prefs?.getDouble('updateIntervalSliderVal') ?? 6.0;
|
||||
}
|
||||
|
||||
set updateIntervalSliderVal(double val) {
|
||||
prefs?.setDouble('updateIntervalSliderVal', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get checkOnStart {
|
||||
return prefs?.getBool('checkOnStart') ?? false;
|
||||
}
|
||||
|
||||
set checkOnStart(bool checkOnStart) {
|
||||
prefs?.setBool('checkOnStart', checkOnStart);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -120,16 +151,28 @@ class SettingsProvider with ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> getInstallPermission() async {
|
||||
bool checkJustStarted() {
|
||||
if (justStarted) {
|
||||
justStarted = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> getInstallPermission({bool enforce = false}) async {
|
||||
while (!(await Permission.requestInstallPackages.isGranted)) {
|
||||
// Explicit request as InstallPlugin request sometimes bugged
|
||||
Fluttertoast.showToast(
|
||||
msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG);
|
||||
if ((await Permission.requestInstallPackages.request()) ==
|
||||
PermissionStatus.granted) {
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
if (!enforce) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get showAppWebpage {
|
||||
@@ -150,6 +193,15 @@ class SettingsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get buryNonInstalled {
|
||||
return prefs?.getBool('buryNonInstalled') ?? false;
|
||||
}
|
||||
|
||||
set buryNonInstalled(bool show) {
|
||||
prefs?.setBool('buryNonInstalled', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get groupByCategory {
|
||||
return prefs?.getBool('groupByCategory') ?? false;
|
||||
}
|
||||
@@ -159,8 +211,27 @@ class SettingsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get hideTrackOnlyWarning {
|
||||
return prefs?.getBool('hideTrackOnlyWarning') ?? false;
|
||||
}
|
||||
|
||||
set hideTrackOnlyWarning(bool show) {
|
||||
prefs?.setBool('hideTrackOnlyWarning', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get hideAPKOriginWarning {
|
||||
return prefs?.getBool('hideAPKOriginWarning') ?? false;
|
||||
}
|
||||
|
||||
set hideAPKOriginWarning(bool show) {
|
||||
prefs?.setBool('hideAPKOriginWarning', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String? getSettingString(String settingId) {
|
||||
return prefs?.getString(settingId);
|
||||
String? str = prefs?.getString(settingId);
|
||||
return str?.isNotEmpty == true ? str : null;
|
||||
}
|
||||
|
||||
void setSettingString(String settingId, String value) {
|
||||
@@ -184,8 +255,7 @@ class SettingsProvider with ChangeNotifier {
|
||||
.map((e) => e as App)
|
||||
.toList();
|
||||
if (changedApps.isNotEmpty) {
|
||||
appsProvider.saveApps(changedApps,
|
||||
attemptToCorrectInstallStatus: false);
|
||||
appsProvider.saveApps(changedApps);
|
||||
}
|
||||
}
|
||||
prefs?.setString('categories', jsonEncode(cats));
|
||||
@@ -195,7 +265,7 @@ class SettingsProvider with ChangeNotifier {
|
||||
String? get forcedLocale {
|
||||
var fl = prefs?.getString('forcedLocale');
|
||||
return supportedLocales
|
||||
.where((element) => element.toLanguageTag() == fl)
|
||||
.where((element) => element.key.toLanguageTag() == fl)
|
||||
.isNotEmpty
|
||||
? fl
|
||||
: null;
|
||||
@@ -205,7 +275,7 @@ class SettingsProvider with ChangeNotifier {
|
||||
if (fl == null) {
|
||||
prefs?.remove('forcedLocale');
|
||||
} else if (supportedLocales
|
||||
.where((element) => element.toLanguageTag() == fl)
|
||||
.where((element) => element.key.toLanguageTag() == fl)
|
||||
.isNotEmpty) {
|
||||
prefs?.setString('forcedLocale', fl);
|
||||
}
|
||||
@@ -225,4 +295,178 @@ class SettingsProvider with ChangeNotifier {
|
||||
context.deleteSaveLocale();
|
||||
}
|
||||
}
|
||||
|
||||
bool get removeOnExternalUninstall {
|
||||
return prefs?.getBool('removeOnExternalUninstall') ?? false;
|
||||
}
|
||||
|
||||
set removeOnExternalUninstall(bool show) {
|
||||
prefs?.setBool('removeOnExternalUninstall', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get checkUpdateOnDetailPage {
|
||||
return prefs?.getBool('checkUpdateOnDetailPage') ?? true;
|
||||
}
|
||||
|
||||
set checkUpdateOnDetailPage(bool show) {
|
||||
prefs?.setBool('checkUpdateOnDetailPage', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get disablePageTransitions {
|
||||
return prefs?.getBool('disablePageTransitions') ?? false;
|
||||
}
|
||||
|
||||
set disablePageTransitions(bool show) {
|
||||
prefs?.setBool('disablePageTransitions', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get reversePageTransitions {
|
||||
return prefs?.getBool('reversePageTransitions') ?? false;
|
||||
}
|
||||
|
||||
set reversePageTransitions(bool show) {
|
||||
prefs?.setBool('reversePageTransitions', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get enableBackgroundUpdates {
|
||||
return prefs?.getBool('enableBackgroundUpdates') ?? true;
|
||||
}
|
||||
|
||||
set enableBackgroundUpdates(bool val) {
|
||||
prefs?.setBool('enableBackgroundUpdates', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get bgUpdatesOnWiFiOnly {
|
||||
return prefs?.getBool('bgUpdatesOnWiFiOnly') ?? false;
|
||||
}
|
||||
|
||||
set bgUpdatesOnWiFiOnly(bool val) {
|
||||
prefs?.setBool('bgUpdatesOnWiFiOnly', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
DateTime get lastCompletedBGCheckTime {
|
||||
int? temp = prefs?.getInt('lastCompletedBGCheckTime');
|
||||
return temp != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(temp)
|
||||
: DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
|
||||
set lastCompletedBGCheckTime(DateTime val) {
|
||||
prefs?.setInt('lastCompletedBGCheckTime', val.millisecondsSinceEpoch);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get showDebugOpts {
|
||||
return prefs?.getBool('showDebugOpts') ?? false;
|
||||
}
|
||||
|
||||
set showDebugOpts(bool val) {
|
||||
prefs?.setBool('showDebugOpts', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get highlightTouchTargets {
|
||||
return prefs?.getBool('highlightTouchTargets') ?? false;
|
||||
}
|
||||
|
||||
set highlightTouchTargets(bool val) {
|
||||
prefs?.setBool('highlightTouchTargets', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<Uri?> getExportDir() async {
|
||||
var uriString = prefs?.getString('exportDir');
|
||||
if (uriString != null) {
|
||||
Uri? uri = Uri.parse(uriString);
|
||||
if (!(await saf.canRead(uri) ?? false) ||
|
||||
!(await saf.canWrite(uri) ?? false)) {
|
||||
uri = null;
|
||||
prefs?.remove('exportDir');
|
||||
notifyListeners();
|
||||
}
|
||||
return uri;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickExportDir({bool remove = false}) async {
|
||||
var existingSAFPerms = (await saf.persistedUriPermissions()) ?? [];
|
||||
var currentOneWayDataSyncDir = await getExportDir();
|
||||
Uri? newOneWayDataSyncDir;
|
||||
if (!remove) {
|
||||
newOneWayDataSyncDir = (await saf.openDocumentTree());
|
||||
}
|
||||
if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) {
|
||||
if (newOneWayDataSyncDir == null) {
|
||||
prefs?.remove('exportDir');
|
||||
} else {
|
||||
prefs?.setString('exportDir', newOneWayDataSyncDir.toString());
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
for (var e in existingSAFPerms) {
|
||||
await saf.releasePersistableUriPermission(e.uri);
|
||||
}
|
||||
}
|
||||
|
||||
bool get autoExportOnChanges {
|
||||
return prefs?.getBool('autoExportOnChanges') ?? false;
|
||||
}
|
||||
|
||||
set autoExportOnChanges(bool val) {
|
||||
prefs?.setBool('autoExportOnChanges', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get onlyCheckInstalledOrTrackOnlyApps {
|
||||
return prefs?.getBool('onlyCheckInstalledOrTrackOnlyApps') ?? false;
|
||||
}
|
||||
|
||||
set onlyCheckInstalledOrTrackOnlyApps(bool val) {
|
||||
prefs?.setBool('onlyCheckInstalledOrTrackOnlyApps', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get exportSettings {
|
||||
return prefs?.getBool('exportSettings') ?? false;
|
||||
}
|
||||
|
||||
set exportSettings(bool val) {
|
||||
prefs?.setBool('exportSettings', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get parallelDownloads {
|
||||
return prefs?.getBool('parallelDownloads') ?? false;
|
||||
}
|
||||
|
||||
set parallelDownloads(bool val) {
|
||||
prefs?.setBool('parallelDownloads', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<String> get searchDeselected {
|
||||
return prefs?.getStringList('searchDeselected') ?? [];
|
||||
}
|
||||
|
||||
set searchDeselected(List<String> list) {
|
||||
prefs?.setStringList('searchDeselected', list);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get beforeNewInstallsShareToAppVerifier {
|
||||
return prefs?.getBool('beforeNewInstallsShareToAppVerifier') ?? true;
|
||||
}
|
||||
|
||||
set beforeNewInstallsShareToAppVerifier(bool val) {
|
||||
prefs?.setBool('beforeNewInstallsShareToAppVerifier', val);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,23 +8,31 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||
import 'package:obtainium/app_sources/apkpure.dart';
|
||||
import 'package:obtainium/app_sources/aptoide.dart';
|
||||
import 'package:obtainium/app_sources/codeberg.dart';
|
||||
import 'package:obtainium/app_sources/directAPKLink.dart';
|
||||
import 'package:obtainium/app_sources/fdroid.dart';
|
||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/app_sources/gitlab.dart';
|
||||
import 'package:obtainium/app_sources/huaweiappgallery.dart';
|
||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/app_sources/mullvad.dart';
|
||||
import 'package:obtainium/app_sources/jenkins.dart';
|
||||
import 'package:obtainium/app_sources/neutroncode.dart';
|
||||
import 'package:obtainium/app_sources/signal.dart';
|
||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||
import 'package:obtainium/app_sources/sourcehut.dart';
|
||||
import 'package:obtainium/app_sources/steammobile.dart';
|
||||
import 'package:obtainium/app_sources/telegramapp.dart';
|
||||
import 'package:obtainium/app_sources/uptodown.dart';
|
||||
import 'package:obtainium/app_sources/vlc.dart';
|
||||
import 'package:obtainium/app_sources/whatsapp.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
|
||||
class AppNames {
|
||||
late String author;
|
||||
@@ -39,82 +47,32 @@ class APKDetails {
|
||||
late AppNames names;
|
||||
late DateTime? releaseDate;
|
||||
late String? changeLog;
|
||||
late List<MapEntry<String, String>> allAssetUrls;
|
||||
|
||||
APKDetails(this.version, this.apkUrls, this.names,
|
||||
{this.releaseDate, this.changeLog});
|
||||
{this.releaseDate, this.changeLog, this.allAssetUrls = const []});
|
||||
}
|
||||
|
||||
class App {
|
||||
late String id;
|
||||
late String url;
|
||||
late String author;
|
||||
late String name;
|
||||
String? installedVersion;
|
||||
late String latestVersion;
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
late int preferredApkIndex;
|
||||
late Map<String, dynamic> additionalSettings;
|
||||
late DateTime? lastUpdateCheck;
|
||||
bool pinned = false;
|
||||
List<String> categories;
|
||||
late DateTime? releaseDate;
|
||||
late String? changeLog;
|
||||
App(
|
||||
this.id,
|
||||
this.url,
|
||||
this.author,
|
||||
this.name,
|
||||
this.installedVersion,
|
||||
this.latestVersion,
|
||||
this.apkUrls,
|
||||
this.preferredApkIndex,
|
||||
this.additionalSettings,
|
||||
this.lastUpdateCheck,
|
||||
this.pinned,
|
||||
{this.categories = const [],
|
||||
this.releaseDate,
|
||||
this.changeLog});
|
||||
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
|
||||
mapList.map((e) => [e.key, e.value]).toList();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned';
|
||||
}
|
||||
assumed2DlistToStringMapList(List<dynamic> arr) =>
|
||||
arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList();
|
||||
|
||||
String? get overrideName =>
|
||||
additionalSettings['appName']?.toString().trim().isNotEmpty == true
|
||||
? additionalSettings['appName']
|
||||
: null;
|
||||
|
||||
String get finalName {
|
||||
return overrideName ?? name;
|
||||
}
|
||||
|
||||
App deepCopy() => App(
|
||||
id,
|
||||
url,
|
||||
author,
|
||||
name,
|
||||
installedVersion,
|
||||
latestVersion,
|
||||
apkUrls,
|
||||
preferredApkIndex,
|
||||
Map.from(additionalSettings),
|
||||
lastUpdateCheck,
|
||||
pinned,
|
||||
categories: categories,
|
||||
changeLog: changeLog,
|
||||
releaseDate: releaseDate);
|
||||
|
||||
factory App.fromJson(Map<String, dynamic> json) {
|
||||
var source = SourceProvider().getSource(json['url']);
|
||||
// App JSON schema has changed multiple times over the many versions of Obtainium
|
||||
// This function takes an App JSON and modifies it if needed to conform to the latest (current) version
|
||||
appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
||||
var source = SourceProvider()
|
||||
.getSource(json['url'], overrideSource: json['overrideSource']);
|
||||
var formItems = source.combinedAppSpecificSettingFormItems
|
||||
.reduce((value, element) => [...value, ...element]);
|
||||
Map<String, dynamic> additionalSettings =
|
||||
getDefaultValuesFromFormItems([formItems]);
|
||||
Map<String, dynamic> originalAdditionalSettings = {};
|
||||
if (json['additionalSettings'] != null) {
|
||||
additionalSettings.addEntries(
|
||||
Map<String, dynamic>.from(jsonDecode(json['additionalSettings']))
|
||||
.entries);
|
||||
originalAdditionalSettings =
|
||||
Map<String, dynamic>.from(jsonDecode(json['additionalSettings']));
|
||||
additionalSettings.addEntries(originalAdditionalSettings.entries);
|
||||
}
|
||||
// If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
|
||||
if (json['additionalData'] != null) {
|
||||
@@ -147,6 +105,21 @@ class App {
|
||||
additionalSettings.remove('releaseDateAsVersion');
|
||||
}
|
||||
}
|
||||
// Convert dropdown style version detection options back into bool style
|
||||
if (additionalSettings['versionDetection'] == 'standardVersionDetection') {
|
||||
additionalSettings['versionDetection'] = true;
|
||||
} else if (additionalSettings['versionDetection'] == 'noVersionDetection') {
|
||||
additionalSettings['versionDetection'] = false;
|
||||
} else if (additionalSettings['versionDetection'] == 'releaseDateAsVersion') {
|
||||
additionalSettings['versionDetection'] = false;
|
||||
additionalSettings['releaseDateAsVersion'] = true;
|
||||
}
|
||||
// Convert bool style pseudo version method to dropdown style
|
||||
if (originalAdditionalSettings['supportFixedAPKURL'] == true) {
|
||||
additionalSettings['defaultPseudoVersioningMethod'] = 'partialAPKHash';
|
||||
} else if (originalAdditionalSettings['supportFixedAPKURL'] == false) {
|
||||
additionalSettings['defaultPseudoVersioningMethod'] = 'APKLinkHash';
|
||||
}
|
||||
// Ensure additionalSettings are correctly typed
|
||||
for (var item in formItems) {
|
||||
if (additionalSettings[item.key] != null) {
|
||||
@@ -154,12 +127,12 @@ class App {
|
||||
item.ensureType(additionalSettings[item.key]);
|
||||
}
|
||||
}
|
||||
int preferredApkIndex = json['preferredApkIndex'] == null
|
||||
? 0
|
||||
: json['preferredApkIndex'] as int;
|
||||
int preferredApkIndex =
|
||||
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int;
|
||||
if (preferredApkIndex < 0) {
|
||||
preferredApkIndex = 0;
|
||||
}
|
||||
json['preferredApkIndex'] = preferredApkIndex;
|
||||
// apkUrls can either be old list or new named list apkUrls
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
if (json['apkUrls'] != null) {
|
||||
@@ -167,16 +140,154 @@ class App {
|
||||
try {
|
||||
apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson));
|
||||
} catch (e) {
|
||||
apkUrls = assumed2DlistToStringMapList(List<dynamic>.from(apkUrlJson));
|
||||
apkUrls = List<dynamic>.from(apkUrlJson)
|
||||
.map((e) => MapEntry(e[0] as String, e[1] as String))
|
||||
.toList();
|
||||
}
|
||||
json['apkUrls'] = jsonEncode(stringMapListTo2DList(apkUrls));
|
||||
}
|
||||
// Arch based APK filter option should be disabled if it previously did not exist
|
||||
if (json['additionalSettings'] != null &&
|
||||
jsonDecode(json['additionalSettings'])['autoApkFilterByArch'] == null) {
|
||||
if (additionalSettings['autoApkFilterByArch'] == null) {
|
||||
additionalSettings['autoApkFilterByArch'] = false;
|
||||
}
|
||||
if (source.runtimeType == HTML().runtimeType) {
|
||||
// HTML 'fixed URL' support should be disabled if it previously did not exist
|
||||
if (originalAdditionalSettings['supportFixedAPKURL'] == null) {
|
||||
additionalSettings['supportFixedAPKURL'] = false;
|
||||
}
|
||||
// HTML key rename
|
||||
if (originalAdditionalSettings['sortByFileNamesNotLinks'] != null) {
|
||||
additionalSettings['sortByLastLinkSegment'] =
|
||||
originalAdditionalSettings['sortByFileNamesNotLinks'];
|
||||
}
|
||||
// HTML single 'intermediate link' should be converted to multi-support version
|
||||
if (originalAdditionalSettings['intermediateLinkRegex'] != null &&
|
||||
additionalSettings['intermediateLinkRegex']?.isNotEmpty != true) {
|
||||
additionalSettings['intermediateLink'] = [
|
||||
{
|
||||
'customLinkFilterRegex':
|
||||
originalAdditionalSettings['intermediateLinkRegex'],
|
||||
'filterByLinkText':
|
||||
originalAdditionalSettings['intermediateLinkByText']
|
||||
}
|
||||
];
|
||||
}
|
||||
if ((additionalSettings['intermediateLink']?.length ?? 0) > 0) {
|
||||
additionalSettings['intermediateLink'] =
|
||||
additionalSettings['intermediateLink'].where((e) {
|
||||
return e['customLinkFilterRegex']?.isNotEmpty == true;
|
||||
}).toList();
|
||||
}
|
||||
// Steam source apps should be converted to HTML (#1244)
|
||||
var legacySteamSourceApps = SteamMobile().apks.keys;
|
||||
if (legacySteamSourceApps.contains(additionalSettings['app'] ?? '')) {
|
||||
json['url'] = '${json['url']}/mobile';
|
||||
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
||||
HTML().combinedAppSpecificSettingFormItems);
|
||||
for (var s in replacementAdditionalSettings.keys) {
|
||||
if (additionalSettings.containsKey(s)) {
|
||||
replacementAdditionalSettings[s] = additionalSettings[s];
|
||||
}
|
||||
}
|
||||
replacementAdditionalSettings['customLinkFilterRegex'] =
|
||||
'/${additionalSettings['app']}-(([0-9]+\\.?){1,})\\.apk';
|
||||
replacementAdditionalSettings['versionExtractionRegEx'] =
|
||||
replacementAdditionalSettings['customLinkFilterRegex'];
|
||||
replacementAdditionalSettings['matchGroupToUse'] = '\$1';
|
||||
additionalSettings = replacementAdditionalSettings;
|
||||
}
|
||||
}
|
||||
json['additionalSettings'] = jsonEncode(additionalSettings);
|
||||
// F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately
|
||||
// This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid)
|
||||
// While not causing problems for existing apps from that source that were added in a previous version
|
||||
var overrideSourceWasUndefined = !json.keys.contains('overrideSource');
|
||||
if ((json['url'] as String).startsWith('https://cloudflare.f-droid.org')) {
|
||||
json['overrideSource'] = FDroid().runtimeType.toString();
|
||||
} else if (overrideSourceWasUndefined) {
|
||||
// Similar to above, but for third-party F-Droid repos
|
||||
RegExpMatch? match = RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)')
|
||||
.firstMatch(json['url'] as String);
|
||||
if (match != null) {
|
||||
json['overrideSource'] = FDroidRepo().runtimeType.toString();
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
class App {
|
||||
late String id;
|
||||
late String url;
|
||||
late String author;
|
||||
late String name;
|
||||
String? installedVersion;
|
||||
late String latestVersion;
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
List<MapEntry<String, String>> otherAssetUrls = [];
|
||||
late int preferredApkIndex;
|
||||
late Map<String, dynamic> additionalSettings;
|
||||
late DateTime? lastUpdateCheck;
|
||||
bool pinned = false;
|
||||
List<String> categories;
|
||||
late DateTime? releaseDate;
|
||||
late String? changeLog;
|
||||
late String? overrideSource;
|
||||
bool allowIdChange = false;
|
||||
App(
|
||||
this.id,
|
||||
this.url,
|
||||
this.author,
|
||||
this.name,
|
||||
this.installedVersion,
|
||||
this.latestVersion,
|
||||
this.apkUrls,
|
||||
this.preferredApkIndex,
|
||||
this.additionalSettings,
|
||||
this.lastUpdateCheck,
|
||||
this.pinned,
|
||||
{this.categories = const [],
|
||||
this.releaseDate,
|
||||
this.changeLog,
|
||||
this.overrideSource,
|
||||
this.allowIdChange = false,
|
||||
this.otherAssetUrls = const []});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned';
|
||||
}
|
||||
|
||||
String? get overrideName =>
|
||||
additionalSettings['appName']?.toString().trim().isNotEmpty == true
|
||||
? additionalSettings['appName']
|
||||
: null;
|
||||
|
||||
String get finalName {
|
||||
return overrideName ?? name;
|
||||
}
|
||||
|
||||
App deepCopy() => App(
|
||||
id,
|
||||
url,
|
||||
author,
|
||||
name,
|
||||
installedVersion,
|
||||
latestVersion,
|
||||
apkUrls,
|
||||
preferredApkIndex,
|
||||
Map.from(additionalSettings),
|
||||
lastUpdateCheck,
|
||||
pinned,
|
||||
categories: categories,
|
||||
changeLog: changeLog,
|
||||
releaseDate: releaseDate,
|
||||
overrideSource: overrideSource,
|
||||
allowIdChange: allowIdChange,
|
||||
otherAssetUrls: otherAssetUrls);
|
||||
|
||||
factory App.fromJson(Map<String, dynamic> json) {
|
||||
json = appJSONCompatibilityModifiers(json);
|
||||
return App(
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
@@ -185,10 +296,11 @@ class App {
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
json['latestVersion'] as String,
|
||||
apkUrls,
|
||||
preferredApkIndex,
|
||||
additionalSettings,
|
||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||
assumed2DlistToStringMapList(
|
||||
jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||
(json['preferredApkIndex'] ?? -1) as int,
|
||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||
json['lastUpdateCheck'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||
@@ -203,8 +315,12 @@ class App {
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||
changeLog:
|
||||
json['changeLog'] == null ? null : json['changeLog'] as String);
|
||||
changeLog: json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||
overrideSource: json['overrideSource'],
|
||||
allowIdChange: json['allowIdChange'] ?? false,
|
||||
otherAssetUrls: assumed2DlistToStringMapList(
|
||||
jsonDecode((json['otherAssetUrls'] ?? '[]'))),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -214,14 +330,17 @@ class App {
|
||||
'name': name,
|
||||
'installedVersion': installedVersion,
|
||||
'latestVersion': latestVersion,
|
||||
'apkUrls': jsonEncode(apkUrls.map((e) => [e.key, e.value]).toList()),
|
||||
'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)),
|
||||
'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)),
|
||||
'preferredApkIndex': preferredApkIndex,
|
||||
'additionalSettings': jsonEncode(additionalSettings),
|
||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||
'pinned': pinned,
|
||||
'categories': categories,
|
||||
'releaseDate': releaseDate?.microsecondsSinceEpoch,
|
||||
'changeLog': changeLog
|
||||
'changeLog': changeLog,
|
||||
'overrideSource': overrideSource,
|
||||
'allowIdChange': allowIdChange
|
||||
};
|
||||
}
|
||||
|
||||
@@ -235,9 +354,6 @@ preStandardizeUrl(String url) {
|
||||
url.toLowerCase().indexOf('https://') != 0) {
|
||||
url = 'https://$url';
|
||||
}
|
||||
if (url.toLowerCase().indexOf('https://www.') == 0) {
|
||||
url = 'https://${url.substring(12)}';
|
||||
}
|
||||
url = url
|
||||
.split('/')
|
||||
.where((e) => e.isNotEmpty)
|
||||
@@ -273,17 +389,79 @@ List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
|
||||
return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e);
|
||||
}).toList();
|
||||
|
||||
class AppSource {
|
||||
String? host;
|
||||
getSourceRegex(List<String> hosts) {
|
||||
return '(${hosts.join('|').replaceAll('.', '\\.')})';
|
||||
}
|
||||
|
||||
abstract class AppSource {
|
||||
List<String> hosts = [];
|
||||
bool hostChanged = false;
|
||||
late String name;
|
||||
bool enforceTrackOnly = false;
|
||||
bool changeLogIfAnyIsMarkDown = true;
|
||||
bool appIdInferIsOptional = false;
|
||||
bool allowSubDomains = false;
|
||||
bool naiveStandardVersionDetection = false;
|
||||
bool neverAutoSelect = false;
|
||||
bool showReleaseDateAsVersionToggle = false;
|
||||
bool versionDetectionDisallowed = false;
|
||||
List<String> excludeCommonSettingKeys = [];
|
||||
|
||||
AppSource() {
|
||||
name = runtimeType.toString();
|
||||
}
|
||||
|
||||
String standardizeURL(String url) {
|
||||
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(String key,
|
||||
{bool disabled = true, bool defaultValue = true}) {
|
||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
|
||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||
.map((e) {
|
||||
return e.map((e2) {
|
||||
if (e2.key == key) {
|
||||
var item = e2 as GeneratedFormSwitch;
|
||||
item.disabled = disabled;
|
||||
item.defaultValue = defaultValue;
|
||||
}
|
||||
return e2;
|
||||
}).toList();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
String standardizeUrl(String url) {
|
||||
url = preStandardizeUrl(url);
|
||||
if (!hostChanged) {
|
||||
url = sourceSpecificStandardizeURL(url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
{bool forAPKDownload = false}) async {
|
||||
return null;
|
||||
}
|
||||
|
||||
App endOfGetAppChanges(App app) {
|
||||
return app;
|
||||
}
|
||||
|
||||
Future<Response> sourceRequest(
|
||||
String url, Map<String, dynamic> additionalSettings,
|
||||
{bool followRedirects = true}) async {
|
||||
var requestHeaders = await getRequestHeaders(additionalSettings);
|
||||
if (requestHeaders != null || followRedirects == false) {
|
||||
var req = Request('GET', Uri.parse(url));
|
||||
req.followRedirects = followRedirects;
|
||||
if (requestHeaders != null) {
|
||||
req.headers.addAll(requestHeaders);
|
||||
}
|
||||
return Response.fromStream(await Client().send(req));
|
||||
} else {
|
||||
return get(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
throw NotImplementedError();
|
||||
}
|
||||
|
||||
@@ -297,8 +475,8 @@ class AppSource {
|
||||
[];
|
||||
|
||||
// Some additional data may be needed for Apps regardless of Source
|
||||
final List<List<GeneratedFormItem>>
|
||||
additionalAppSpecificSourceAgnosticSettingFormItems = [
|
||||
List<List<GeneratedFormItem>>
|
||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = [
|
||||
[
|
||||
GeneratedFormSwitch(
|
||||
'trackOnly',
|
||||
@@ -306,16 +484,22 @@ class AppSource {
|
||||
)
|
||||
],
|
||||
[
|
||||
GeneratedFormDropdown(
|
||||
'versionDetection',
|
||||
[
|
||||
MapEntry(
|
||||
'standardVersionDetection', tr('standardVersionDetection')),
|
||||
MapEntry('releaseDateAsVersion', tr('releaseDateAsVersion')),
|
||||
MapEntry('noVersionDetection', tr('noVersionDetection'))
|
||||
GeneratedFormTextField('versionExtractionRegEx',
|
||||
label: tr('versionExtractionRegEx'),
|
||||
required: false,
|
||||
additionalValidators: [(value) => regExValidator(value)]),
|
||||
],
|
||||
label: tr('versionDetection'),
|
||||
defaultValue: 'standardVersionDetection')
|
||||
[
|
||||
GeneratedFormTextField('matchGroupToUse',
|
||||
label: tr('matchGroupToUse'), required: false, hint: '\$0')
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('versionDetection',
|
||||
label: tr('versionDetectionExplanation'), defaultValue: true)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('useVersionCodeAsOSVersion',
|
||||
label: tr('useVersionCodeAsOSVersion'), defaultValue: false)
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('apkFilterRegEx',
|
||||
@@ -327,52 +511,136 @@ class AppSource {
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('invertAPKFilter',
|
||||
label: '${tr('invertRegEx')} (${tr('filterAPKsByRegEx')})',
|
||||
defaultValue: false)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('autoApkFilterByArch',
|
||||
label: tr('autoApkFilterByArch'), defaultValue: true)
|
||||
],
|
||||
[GeneratedFormTextField('appName', label: tr('appName'), required: false)]
|
||||
[GeneratedFormTextField('appName', label: tr('appName'), required: false)],
|
||||
[
|
||||
GeneratedFormSwitch('shizukuPretendToBeGooglePlay',
|
||||
label: tr('shizukuPretendToBeGooglePlay'),
|
||||
defaultValue: false)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('exemptFromBackgroundUpdates',
|
||||
label: tr('exemptFromBackgroundUpdates'))
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('skipUpdateNotifications',
|
||||
label: tr('skipUpdateNotifications'))
|
||||
],
|
||||
[GeneratedFormTextField('about', label: tr('about'), required: false)]
|
||||
];
|
||||
|
||||
// Previous 2 variables combined into one at runtime for convenient usage
|
||||
List<List<GeneratedFormItem>> get combinedAppSpecificSettingFormItems {
|
||||
if (showReleaseDateAsVersionToggle == true) {
|
||||
if (additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||
.indexWhere((List<GeneratedFormItem> e) =>
|
||||
e.indexWhere((GeneratedFormItem i) =>
|
||||
i.key == 'releaseDateAsVersion') >=
|
||||
0) <
|
||||
0) {
|
||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly.insert(
|
||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||
.indexWhere((List<GeneratedFormItem> e) =>
|
||||
e.indexWhere((GeneratedFormItem i) =>
|
||||
i.key == 'versionDetection') >=
|
||||
0) +
|
||||
1,
|
||||
[
|
||||
GeneratedFormSwitch('releaseDateAsVersion',
|
||||
label:
|
||||
'${tr('releaseDateAsVersion')} (${tr('pseudoVersion')})',
|
||||
defaultValue: false)
|
||||
]);
|
||||
}
|
||||
}
|
||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
|
||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||
.map((e) => e
|
||||
.where((ee) => !excludeCommonSettingKeys.contains(ee.key))
|
||||
.toList())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
if (versionDetectionDisallowed) {
|
||||
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
|
||||
'versionDetection',
|
||||
disabled: true,
|
||||
defaultValue: false);
|
||||
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
|
||||
'useVersionCodeAsOSVersion',
|
||||
disabled: true,
|
||||
defaultValue: false);
|
||||
}
|
||||
return [
|
||||
...additionalSourceAppSpecificSettingFormItems,
|
||||
...additionalAppSpecificSourceAgnosticSettingFormItems
|
||||
...additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||
];
|
||||
}
|
||||
|
||||
// Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider
|
||||
List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = [];
|
||||
// If the source has been overridden, we expect the user to define one-time values as additional settings - don't use the stored values
|
||||
List<GeneratedFormItem> sourceConfigSettingFormItems = [];
|
||||
Future<Map<String, String>> getSourceConfigValues(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
SettingsProvider settingsProvider) async {
|
||||
Map<String, String> results = {};
|
||||
for (var e in sourceConfigSettingFormItems) {
|
||||
var val = hostChanged
|
||||
? additionalSettings[e.key]
|
||||
: settingsProvider.getSettingString(e.key);
|
||||
if (val != null) {
|
||||
results[e.key] = val;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
||||
Future<String?> getSourceNote() async {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
||||
Map<String, dynamic> additionalSettings) async {
|
||||
return apkUrl;
|
||||
}
|
||||
|
||||
bool canSearch = false;
|
||||
Future<Map<String, String>> search(String query) {
|
||||
bool excludeFromMassSearch = false;
|
||||
List<GeneratedFormItem> searchQuerySettingFormItems = [];
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) {
|
||||
throw NotImplementedError();
|
||||
}
|
||||
|
||||
String? tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ObtainiumError getObtainiumHttpError(Response res) {
|
||||
return ObtainiumError(res.reasonPhrase ??
|
||||
tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
|
||||
return ObtainiumError((res.reasonPhrase != null &&
|
||||
res.reasonPhrase != null &&
|
||||
res.reasonPhrase!.isNotEmpty)
|
||||
? res.reasonPhrase!
|
||||
: tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
|
||||
}
|
||||
|
||||
abstract class MassAppUrlSource {
|
||||
late String name;
|
||||
late List<String> requiredArgs;
|
||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
||||
Future<Map<String, List<String>>> getUrlsWithDescriptions(List<String> args);
|
||||
}
|
||||
|
||||
regExValidator(String? value) {
|
||||
@@ -387,43 +655,149 @@ regExValidator(String? value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
intValidator(String? value, {bool positive = false}) {
|
||||
if (value == null) {
|
||||
return tr('invalidInput');
|
||||
}
|
||||
var num = int.tryParse(value);
|
||||
if (num == null) {
|
||||
return tr('invalidInput');
|
||||
}
|
||||
if (positive && num <= 0) {
|
||||
return tr('invalidInput');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isTempId(App app) {
|
||||
// return app.id == generateTempID(app.url, app.additionalSettings);
|
||||
return RegExp('^[0-9]+\$').hasMatch(app.id);
|
||||
}
|
||||
|
||||
replaceMatchGroupsInString(RegExpMatch match, String matchGroupString) {
|
||||
if (RegExp('^\\d+\$').hasMatch(matchGroupString)) {
|
||||
matchGroupString = '\$$matchGroupString';
|
||||
}
|
||||
// Regular expression to match numbers in the input string
|
||||
final numberRegex = RegExp(r'\$\d+');
|
||||
// Extract all numbers from the input string
|
||||
final numbers = numberRegex.allMatches(matchGroupString);
|
||||
if (numbers.isEmpty) {
|
||||
// If no numbers found, return the original string
|
||||
return null;
|
||||
}
|
||||
// Replace numbers with corresponding match groups
|
||||
var outputString = matchGroupString;
|
||||
for (final numberMatch in numbers) {
|
||||
final number = numberMatch.group(0)!;
|
||||
final matchGroup = match.group(int.parse(number.substring(1))) ?? '';
|
||||
// Check if the number is preceded by a single backslash
|
||||
final isEscaped = outputString.contains('\\$number');
|
||||
// Replace the number with the corresponding match group
|
||||
if (!isEscaped) {
|
||||
outputString = outputString.replaceAll(number, matchGroup);
|
||||
} else {
|
||||
outputString = outputString.replaceAll('\\$number', number);
|
||||
}
|
||||
}
|
||||
return outputString;
|
||||
}
|
||||
|
||||
String? extractVersion(String? versionExtractionRegEx, String? matchGroupString,
|
||||
String stringToCheck) {
|
||||
if (versionExtractionRegEx?.isNotEmpty == true) {
|
||||
String? version = stringToCheck;
|
||||
var match = RegExp(versionExtractionRegEx!).allMatches(version);
|
||||
if (match.isEmpty) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
matchGroupString = matchGroupString?.trim() ?? '';
|
||||
if (matchGroupString.isEmpty) {
|
||||
matchGroupString = "0";
|
||||
}
|
||||
version = replaceMatchGroupsInString(match.last, matchGroupString);
|
||||
if (version?.isNotEmpty != true) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return version!;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<MapEntry<String, String>> filterApks(
|
||||
List<MapEntry<String, String>> apkUrls,
|
||||
String? apkFilterRegEx,
|
||||
bool? invert) {
|
||||
if (apkFilterRegEx?.isNotEmpty == true) {
|
||||
var reg = RegExp(apkFilterRegEx!);
|
||||
apkUrls = apkUrls.where((element) {
|
||||
var hasMatch = reg.hasMatch(element.key);
|
||||
return invert == true ? !hasMatch : hasMatch;
|
||||
}).toList();
|
||||
}
|
||||
return apkUrls;
|
||||
}
|
||||
|
||||
class SourceProvider {
|
||||
// Add more source classes here so they are available via the service
|
||||
List<AppSource> sources = [
|
||||
List<AppSource> get sources => [
|
||||
GitHub(),
|
||||
GitLab(),
|
||||
Codeberg(),
|
||||
FDroid(),
|
||||
IzzyOnDroid(),
|
||||
FDroidRepo(),
|
||||
SourceForge(),
|
||||
IzzyOnDroid(),
|
||||
SourceHut(),
|
||||
APKPure(),
|
||||
Aptoide(),
|
||||
Uptodown(),
|
||||
APKMirror(),
|
||||
Mullvad(),
|
||||
HuaweiAppGallery(),
|
||||
Jenkins(),
|
||||
Signal(),
|
||||
VLC(),
|
||||
// WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
|
||||
WhatsApp(),
|
||||
TelegramApp(),
|
||||
SteamMobile(),
|
||||
NeutronCode(),
|
||||
DirectAPKLink(),
|
||||
HTML() // This should ALWAYS be the last option as they are tried in order
|
||||
];
|
||||
|
||||
// Add more mass url source classes here so they are available via the service
|
||||
List<MassAppUrlSource> massUrlSources = [GitHubStars()];
|
||||
|
||||
AppSource getSource(String url) {
|
||||
AppSource getSource(String url, {String? overrideSource}) {
|
||||
url = preStandardizeUrl(url);
|
||||
if (overrideSource != null) {
|
||||
var srcs =
|
||||
sources.where((e) => e.runtimeType.toString() == overrideSource);
|
||||
if (srcs.isEmpty) {
|
||||
throw UnsupportedURLError();
|
||||
}
|
||||
var res = srcs.first;
|
||||
res.hosts = [Uri.parse(url).host];
|
||||
res.hostChanged = true;
|
||||
return srcs.first;
|
||||
}
|
||||
AppSource? source;
|
||||
for (var s in sources.where((element) => element.host != null)) {
|
||||
if (RegExp('://(.+\\.)?${s.host}').hasMatch(url)) {
|
||||
for (var s in sources.where((element) => element.hosts.isNotEmpty)) {
|
||||
try {
|
||||
if (RegExp(
|
||||
'^${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})\$')
|
||||
.hasMatch(Uri.parse(url).host)) {
|
||||
source = s;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
if (source == null) {
|
||||
for (var s in sources.where((element) => element.host == null)) {
|
||||
for (var s in sources.where(
|
||||
(element) => element.hosts.isEmpty && !element.neverAutoSelect)) {
|
||||
try {
|
||||
s.standardizeURL(url);
|
||||
s.sourceSpecificStandardizeURL(url);
|
||||
source = s;
|
||||
break;
|
||||
} catch (e) {
|
||||
@@ -452,30 +826,38 @@ class SourceProvider {
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) =>
|
||||
(standardUrl + additionalSettings.toString()).hashCode.toString();
|
||||
|
||||
bool isTempId(App app) {
|
||||
// return app.id == generateTempID(app.url, app.additionalSettings);
|
||||
return RegExp('^[0-9]+\$').hasMatch(app.id);
|
||||
}
|
||||
|
||||
Future<App> getApp(
|
||||
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
||||
{App? currentApp, bool trackOnlyOverride = false}) async {
|
||||
{App? currentApp,
|
||||
bool trackOnlyOverride = false,
|
||||
bool sourceIsOverriden = false,
|
||||
bool inferAppIdIfOptional = false}) async {
|
||||
if (trackOnlyOverride || source.enforceTrackOnly) {
|
||||
additionalSettings['trackOnly'] = true;
|
||||
}
|
||||
var trackOnly = additionalSettings['trackOnly'] == true;
|
||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||
String standardUrl = source.standardizeUrl(url);
|
||||
APKDetails apk =
|
||||
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
||||
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
|
||||
|
||||
if (source.runtimeType !=
|
||||
HTML().runtimeType && // Some sources do it separately
|
||||
source.runtimeType != SourceForge().runtimeType) {
|
||||
String? extractedVersion = extractVersion(
|
||||
additionalSettings['versionExtractionRegEx'] as String?,
|
||||
additionalSettings['matchGroupToUse'] as String?,
|
||||
apk.version);
|
||||
if (extractedVersion != null) {
|
||||
apk.version = extractedVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalSettings['releaseDateAsVersion'] == true &&
|
||||
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.key)).toList();
|
||||
}
|
||||
apk.apkUrls = filterApks(apk.apkUrls, additionalSettings['apkFilterRegEx'],
|
||||
additionalSettings['invertAPKFilter']);
|
||||
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
@@ -492,21 +874,22 @@ class SourceProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
String apkVersion = apk.version.replaceAll('/', '-');
|
||||
var name = currentApp != null ? currentApp.name.trim() : '';
|
||||
name = name.isNotEmpty
|
||||
? name
|
||||
: apk.names.name[0].toUpperCase() + apk.names.name.substring(1);
|
||||
return App(
|
||||
name = name.isNotEmpty ? name : apk.names.name;
|
||||
App finalApp = App(
|
||||
currentApp?.id ??
|
||||
source.tryInferringAppId(standardUrl,
|
||||
additionalSettings: additionalSettings) ??
|
||||
(!trackOnly &&
|
||||
(!source.appIdInferIsOptional ||
|
||||
(source.appIdInferIsOptional && inferAppIdIfOptional))
|
||||
? await source.tryInferringAppId(standardUrl,
|
||||
additionalSettings: additionalSettings)
|
||||
: null) ??
|
||||
generateTempID(standardUrl, additionalSettings),
|
||||
standardUrl,
|
||||
apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
|
||||
apk.names.author,
|
||||
name,
|
||||
currentApp?.installedVersion,
|
||||
apkVersion,
|
||||
apk.version,
|
||||
apk.apkUrls,
|
||||
apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0,
|
||||
additionalSettings,
|
||||
@@ -514,12 +897,24 @@ class SourceProvider {
|
||||
currentApp?.pinned ?? false,
|
||||
categories: currentApp?.categories ?? const [],
|
||||
releaseDate: apk.releaseDate,
|
||||
changeLog: apk.changeLog);
|
||||
changeLog: apk.changeLog,
|
||||
overrideSource: sourceIsOverriden
|
||||
? source.runtimeType.toString()
|
||||
: currentApp?.overrideSource,
|
||||
allowIdChange: currentApp?.allowIdChange ??
|
||||
trackOnly ||
|
||||
(source.appIdInferIsOptional &&
|
||||
inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install
|
||||
otherAssetUrls: apk.allAssetUrls
|
||||
.where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0)
|
||||
.toList());
|
||||
return source.endOfGetAppChanges(finalApp);
|
||||
}
|
||||
|
||||
// Returns errors in [results, errors] instead of throwing them
|
||||
Future<List<dynamic>> getAppsByURLNaive(List<String> urls,
|
||||
{List<String> alreadyAddedUrls = const []}) async {
|
||||
{List<String> alreadyAddedUrls = const [],
|
||||
AppSource? sourceOverride}) async {
|
||||
List<App> apps = [];
|
||||
Map<String, dynamic> errors = {};
|
||||
for (var url in urls) {
|
||||
@@ -527,10 +922,11 @@ class SourceProvider {
|
||||
if (alreadyAddedUrls.contains(url)) {
|
||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||
}
|
||||
var source = getSource(url);
|
||||
var source = sourceOverride ?? getSource(url);
|
||||
apps.add(await getApp(
|
||||
source,
|
||||
url,
|
||||
sourceIsOverriden: sourceOverride != null,
|
||||
getDefaultValuesFromFormItems(
|
||||
source.combinedAppSpecificSettingFormItems)));
|
||||
} catch (e) {
|
||||
|
||||
651
pubspec.lock
58
pubspec.yaml
@@ -1,5 +1,5 @@
|
||||
name: obtainium
|
||||
description: A new Flutter project.
|
||||
description: Get Android App Updates Directly From the Source.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
@@ -17,10 +17,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.11.33+155 # When changing this, update the tag in main() accordingly
|
||||
version: 1.1.6+2263
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.2 <3.0.0'
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@@ -37,41 +37,63 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.5
|
||||
path_provider: ^2.0.11
|
||||
flutter_fgbg: ^0.2.0 # Try removing reliance on this
|
||||
flutter_local_notifications: ^13.0.0
|
||||
flutter_fgbg: ^0.3.0 # Try removing reliance on this
|
||||
flutter_local_notifications: ^17.0.0
|
||||
provider: ^6.0.3
|
||||
http: ^0.13.5
|
||||
http: ^1.0.0
|
||||
webview_flutter: ^4.0.0
|
||||
dynamic_color: ^1.5.4
|
||||
html: ^0.15.0
|
||||
shared_preferences: ^2.0.15
|
||||
url_launcher: ^6.1.5
|
||||
permission_handler: ^10.0.0
|
||||
permission_handler: ^11.0.0
|
||||
fluttertoast: ^8.0.9
|
||||
device_info_plus: ^8.0.0
|
||||
file_picker: ^5.2.10
|
||||
device_info_plus: ^10.0.1
|
||||
file_picker: ^8.0.0+1
|
||||
animations: ^2.0.4
|
||||
install_plugin_v2: ^1.0.0
|
||||
share_plus: ^6.0.1
|
||||
installed_apps: ^1.3.1
|
||||
package_archive_info: ^0.1.0
|
||||
android_alarm_manager_plus: ^2.1.0
|
||||
android_package_installer:
|
||||
git:
|
||||
url: https://github.com/ImranR98/android_package_installer
|
||||
ref: main
|
||||
android_package_manager: ^0.7.0
|
||||
share_plus: ^9.0.0
|
||||
sqflite: ^2.2.0+3
|
||||
easy_localization: ^3.0.1
|
||||
android_intent_plus: ^3.1.5
|
||||
flutter_markdown: ^0.6.14
|
||||
|
||||
android_intent_plus: ^5.0.1
|
||||
flutter_markdown: ^0.7.1
|
||||
flutter_archive: ^6.0.0
|
||||
hsluv: ^1.1.3
|
||||
connectivity_plus: ^6.0.1
|
||||
shared_storage: ^0.8.0
|
||||
crypto: ^3.0.3
|
||||
app_links: ^4.0.0
|
||||
background_fetch: ^1.2.1
|
||||
equations: ^5.0.2
|
||||
flex_color_picker: ^3.4.1
|
||||
android_system_font:
|
||||
git:
|
||||
url: https://github.com/re7gog/android_system_font
|
||||
ref: master
|
||||
shizuku_apk_installer:
|
||||
git:
|
||||
url: https://github.com/re7gog/shizuku_apk_installer
|
||||
ref: master
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^2.0.1
|
||||
flutter_lints: ^3.0.0
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: "ic_launcher"
|
||||
image_path: "assets/graphics/icon.png"
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||