Compare commits
1029 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e609916df6 | ||
|
|
ac9d4f9e39 | ||
|
|
19fe680a86 | ||
|
|
b67b44a069 | ||
|
|
27b4212567 | ||
|
|
94735cd23d | ||
|
|
982fe468c8 | ||
|
|
681c43ec69 | ||
|
|
e9da8de67e | ||
|
|
112770024c | ||
|
|
f6f9bcaa8f | ||
|
|
5607ec70ff | ||
|
|
bca07c9c46 | ||
|
|
501a3da48f | ||
|
|
b9523c259f | ||
|
|
5d0c3fd977 | ||
|
|
508dcd4a42 | ||
|
|
4aaf5ab518 | ||
|
|
0a411e8421 | ||
|
|
3974fc7003 | ||
|
|
52e7101472 | ||
|
|
09b1d9b0b1 | ||
|
|
d707858ad7 | ||
|
|
95d6d1ac14 | ||
|
|
f5e0dc7cc7 | ||
|
|
1b6c62a204 | ||
|
|
ad323efe34 | ||
|
|
81393601e1 | ||
|
|
f03f4c0b8a | ||
|
|
0dac0566dd | ||
|
|
92343aee2b | ||
|
|
5d3939e61f | ||
|
|
0eef3dd918 | ||
|
|
4b4bb0ad7b | ||
|
|
9bafb790cf | ||
|
|
54c75da756 | ||
|
|
b64f44c341 | ||
|
|
9b29b40ff3 | ||
|
|
4ce60d957d | ||
|
|
44c5b59601 | ||
|
|
fc1cff477b | ||
|
|
e348ed243f | ||
|
|
55dfa9cbf4 | ||
|
|
22ba348959 | ||
|
|
78df4df73b | ||
|
|
868e2251fe | ||
|
|
82eec0688f | ||
|
|
cf5451599b | ||
|
|
14ebc82fc6 | ||
|
|
c0e94cbe5a | ||
|
|
9b610a3b16 | ||
|
|
b686c452cd | ||
|
|
4704f27e30 | ||
|
|
7957601814 | ||
|
|
baa42e643c | ||
|
|
1bce1e1c5d | ||
|
|
a9e1328cec | ||
|
|
e3fda69df3 | ||
|
|
cdcac3fab7 | ||
|
|
082a6b2b83 | ||
|
|
be325f84b2 | ||
|
|
0af3176166 | ||
|
|
6b9ca1b6c8 | ||
|
|
c75c576641 | ||
|
|
8420b373b4 | ||
|
|
0fdbd3e452 | ||
|
|
53fa2950e6 | ||
|
|
1771b5843b | ||
|
|
3ac882de8c | ||
|
|
e784b666c5 | ||
|
|
44195d0d87 | ||
|
|
e7044891c3 | ||
|
|
f0f41c378a | ||
|
|
04fc467cb4 | ||
|
|
7ffc876d9a | ||
|
|
718f5b4a75 | ||
|
|
5ef6316036 | ||
|
|
019989df4f | ||
|
|
a4899f1196 | ||
|
|
cc6a24d65c | ||
|
|
d03b151750 | ||
|
|
924b77a35d | ||
|
|
086ddf951b | ||
|
|
862286840f | ||
|
|
0ca54ea38d | ||
|
|
634bda6a94 | ||
|
|
a271858f77 | ||
|
|
ef4b18f622 | ||
|
|
583075abaa | ||
|
|
b47a888cc6 | ||
|
|
b216f55b0d | ||
|
|
951bdab694 | ||
|
|
0f8acca4df | ||
|
|
3d4c500a15 | ||
|
|
b5efb1ad19 | ||
|
|
51dcae1a54 | ||
|
|
ffeddb37e6 | ||
|
|
c42d53f8f5 | ||
|
|
63b1b20f5a | ||
|
|
2026e43630 | ||
|
|
b03ff287fe | ||
|
|
c48dbbe3cb | ||
|
|
473759085e | ||
|
|
02314a75e3 | ||
|
|
706a448698 | ||
|
|
525771927c | ||
|
|
375fe1806b | ||
|
|
35c4f35f5e | ||
|
|
c15ef04c80 | ||
|
|
b56ef52ae3 | ||
|
|
46db2f5751 | ||
|
|
02f8f165a1 | ||
|
|
2beaaf5575 | ||
|
|
208827ec78 | ||
|
|
853ed19507 | ||
|
|
c0742c6e20 | ||
|
|
b66ddb1679 | ||
|
|
af8b1c1773 | ||
|
|
bbba94625a | ||
|
|
632830278c | ||
|
|
8249d0a0f5 | ||
|
|
117639514e | ||
|
|
05c9722142 | ||
|
|
23ecb6f53a | ||
|
|
0bd5b37edc | ||
|
|
3e54e2d7d8 | ||
|
|
2fc8e1205e | ||
|
|
3901010309 | ||
|
|
0933bc448c | ||
|
|
626e5df595 | ||
|
|
c281290350 | ||
|
|
0bbb99fc18 | ||
|
|
eaa6a6d9e0 | ||
|
|
75c9d6bf07 | ||
|
|
efe05e9a04 | ||
|
|
175f011d01 | ||
|
|
1a4b78e64e | ||
|
|
e4e5fc520a | ||
|
|
7c5cec2285 | ||
|
|
cc4c75f88a | ||
|
|
440ec57d59 | ||
|
|
fd9e68513d | ||
|
|
f1f993f763 | ||
|
|
66353bc80c | ||
|
|
c6e704c04d | ||
|
|
f3ade3e7b2 | ||
|
|
0751a090d0 | ||
|
|
f90149baa2 | ||
|
|
31e7fdca3e | ||
|
|
51fe07e463 | ||
|
|
7b35100f4c | ||
|
|
8c09010a9a | ||
|
|
cbb30fd965 | ||
|
|
bc47afaf4b | ||
|
|
7313762775 | ||
|
|
5e872c0033 | ||
|
|
e63c27d649 | ||
|
|
ce266cf2a8 | ||
|
|
27dbdcbd55 | ||
|
|
cf4af358f5 | ||
|
|
d0a622c2b7 | ||
|
|
3478a3f11e | ||
|
|
8b6facd015 | ||
|
|
73773f3265 | ||
|
|
65f1fcf0d4 | ||
|
|
a8770a6abe | ||
|
|
fdc05a13ca | ||
|
|
785297cd2a | ||
|
|
291b2d8c66 | ||
|
|
8c4c275efd | ||
|
|
fa758159dc | ||
|
|
1692ac2c3d | ||
|
|
a348c9a63a | ||
|
|
193b16fc8c | ||
|
|
86a408afd9 | ||
|
|
b59420efb0 | ||
|
|
d09ae407b3 | ||
|
|
fd1f18ca9a | ||
|
|
a14d7db5ea | ||
|
|
6ea5afb0d8 | ||
|
|
eb70b83bb1 | ||
|
|
a66ac56246 | ||
|
|
aa886ba17c | ||
|
|
728b7a0d45 | ||
|
|
cd84d5f3fa | ||
|
|
8900230cb6 | ||
|
|
6b2473d382 | ||
|
|
26ace39177 | ||
|
|
c4e2d23387 | ||
|
|
7fc01c7b08 | ||
|
|
af764cf9e9 | ||
|
|
0fd90a7385 | ||
|
|
0747c86a50 | ||
|
|
1b20a7cdbb | ||
|
|
92585beaba | ||
|
|
ad2ade2f45 | ||
|
|
d94daaf177 | ||
|
|
22685e688f | ||
|
|
7d6e2e65d4 | ||
|
|
dbf2c2516b | ||
|
|
1540193a88 | ||
|
|
fde26dab5d | ||
|
|
76ac93bce1 | ||
|
|
40bf680058 | ||
|
|
210e98c5a1 | ||
|
|
ec9dbd86f1 | ||
|
|
1e909ca9eb | ||
|
|
a834e41570 | ||
|
|
6488c345c8 | ||
|
|
becf87591f | ||
|
|
5fc633921a | ||
|
|
d712e5267d | ||
|
|
16d863a22b | ||
|
|
4a91aa022d | ||
|
|
92200a674d | ||
|
|
2a4af77e6a | ||
|
|
aca5cc6906 | ||
|
|
ff2ce35654 | ||
|
|
8d1ae7b636 | ||
|
|
1111cf1a0b | ||
|
|
6b0f020e6d | ||
|
|
a6ba2db81f | ||
|
|
491716b1e2 | ||
|
|
26957de534 | ||
|
|
0daaf8027f | ||
|
|
f65eeeea41 | ||
|
|
f04638ccf3 | ||
|
|
46d95975fe | ||
|
|
479e83420e | ||
|
|
fa65ff7740 | ||
|
|
49f541755c | ||
|
|
31d02ff6fa | ||
|
|
f83d8251d1 | ||
|
|
eb07f50d3d | ||
|
|
42fb670abb | ||
|
|
491b7175b5 | ||
|
|
2705dc7485 | ||
|
|
2d63225008 | ||
|
|
9d7e2f1730 | ||
|
|
94bb886b4e | ||
|
|
a582bf7ab8 | ||
|
|
159c48b1ab | ||
|
|
cee4f59ea4 | ||
|
|
5d5365949e | ||
|
|
01d36394c6 | ||
|
|
243e6fe54d | ||
|
|
1d7415bbf1 | ||
|
|
fc452fe77b | ||
|
|
469d8d854e | ||
|
|
173b6b18f0 | ||
|
|
b5ef3fbb6d | ||
|
|
b60db521cd | ||
|
|
4ea16043da | ||
|
|
3fdb75e48c | ||
|
|
43d2dd2f01 | ||
|
|
0e1a1db526 | ||
|
|
d580f9dfd9 | ||
|
|
49039fc216 | ||
|
|
2b1f4807fd | ||
|
|
cd73becd03 | ||
|
|
2e7687c704 | ||
|
|
634cc8f51f | ||
|
|
7531dbd879 | ||
|
|
92bf2a0df8 | ||
|
|
4c534be61b | ||
|
|
26fce675aa | ||
|
|
50640acb19 | ||
|
|
7e9336d55b | ||
|
|
87a77d08f5 | ||
|
|
08e9991528 | ||
|
|
566be2af25 | ||
|
|
771e67e9d2 | ||
|
|
dd4bb5f437 | ||
|
|
3ebe078507 | ||
|
|
6d310eb6c8 | ||
|
|
3f6bf3aca4 | ||
|
|
daff0020cb | ||
|
|
f99d6cf34f | ||
|
|
96304b21c5 | ||
|
|
bc987e63eb | ||
|
|
7a380967ea | ||
|
|
03a6f5c79e | ||
|
|
7e1d10aa08 | ||
|
|
519744616e | ||
|
|
8e0563facc | ||
|
|
8075de46fb | ||
|
|
2a3c58adbd | ||
|
|
c59462f42d | ||
|
|
194172c26d | ||
|
|
369f83a26e | ||
|
|
c0771d8832 | ||
|
|
5cfb0acc92 | ||
|
|
c37879fa31 | ||
|
|
1bc76cf0d1 | ||
|
|
652bfe1766 | ||
|
|
832cd759d3 | ||
|
|
53ae9b2d23 | ||
|
|
a0c7bbf734 | ||
|
|
0857ab3024 | ||
|
|
41953477a5 | ||
|
|
6cad58ac33 | ||
|
|
f8cc12ff15 | ||
|
|
3a172bc878 | ||
|
|
ceb53f2c7a | ||
|
|
8cd5c55dd4 | ||
|
|
4be7382843 | ||
|
|
b696ef7b4a | ||
|
|
a81db75fd9 | ||
|
|
0962fd15d1 | ||
|
|
d0ac33afb9 | ||
|
|
4ffa34b9ab | ||
|
|
611802607e | ||
|
|
d260c81d58 | ||
|
|
d5259523c5 | ||
|
|
ff76642b0c | ||
|
|
ca9deebb18 | ||
|
|
19f7a7a04c | ||
|
|
67bb6bc3d9 | ||
|
|
271636ed64 | ||
|
|
f4f6707f5f | ||
|
|
ac0507e615 | ||
|
|
cbddeb6eb0 | ||
|
|
5ad17b7166 | ||
|
|
1e1baa0e88 | ||
|
|
7c289eee8c | ||
|
|
50bdc0c78a | ||
|
|
7513727ced | ||
|
|
5a56dadc59 | ||
|
|
a428b3311f | ||
|
|
9237324fcf | ||
|
|
4f0c705660 | ||
|
|
4d769237b6 | ||
|
|
a379c4278f | ||
|
|
8e0dd3d7b0 | ||
|
|
e05991a631 | ||
|
|
ec380f06d9 | ||
|
|
3cf264fab8 | ||
|
|
e0a8d46fdb | ||
|
|
14f87c071b | ||
|
|
e6b766ab94 | ||
|
|
a07b43a750 | ||
|
|
ebe38209bb | ||
|
|
c95c249843 | ||
|
|
c3e000da9d | ||
|
|
99a5c6303c | ||
|
|
62802fd30e | ||
|
|
fddf940b95 | ||
|
|
afe49abcfa | ||
|
|
b5d868dcfa | ||
|
|
1afd46f9a7 | ||
|
|
3f69385ffc | ||
|
|
e86a7b229a | ||
|
|
17c8a61416 | ||
|
|
1d10d542a0 | ||
|
|
8bb221cd36 | ||
|
|
ef9d911d9d | ||
|
|
ceea5c9206 | ||
|
|
991ff443c2 | ||
|
|
1ce01ea90a | ||
|
|
1d81caddf1 | ||
|
|
edc11d033e | ||
|
|
a8cd13782c | ||
|
|
544dac2cf6 | ||
|
|
a84dacded1 | ||
|
|
84215ac49b | ||
|
|
e83ec3343c | ||
|
|
88a5df88bf | ||
|
|
5567619e0f | ||
|
|
e14b00fc13 | ||
|
|
e9d9101526 | ||
|
|
8b2ee00c90 | ||
|
|
400079f0da | ||
|
|
fb6dc6219d | ||
|
|
b04bdb6d1f | ||
|
|
ced4c6a1b5 | ||
|
|
a4fcb80815 | ||
|
|
78ec26f0ee | ||
|
|
893ab70a53 | ||
|
|
73f940f818 | ||
|
|
59e1a47b52 | ||
|
|
b7ec052ab1 | ||
|
|
ac188deda3 | ||
|
|
5c941b5a34 | ||
|
|
21f7b79974 | ||
|
|
99d5e6f6cd | ||
|
|
abbbdc00ee | ||
|
|
1d5f09be0a | ||
|
|
d3c4a0bb98 | ||
|
|
bd37eb9db4 | ||
|
|
9feabeaa41 | ||
|
|
0160564378 | ||
|
|
eb9582f07a | ||
|
|
f3ef58db11 | ||
|
|
38df5c71e8 | ||
|
|
390b78062c | ||
|
|
76e2b3ed3b | ||
|
|
50cd96bee7 | ||
|
|
43719ba28b | ||
|
|
f1ae2b27d4 | ||
|
|
f845ce3d7f | ||
|
|
8c93ec7bed | ||
|
|
a020323bdb | ||
|
|
f2d0e55e57 | ||
|
|
967791878a | ||
|
|
95c1a29c18 | ||
|
|
0c8e0f412c | ||
|
|
e0c6e03c3f | ||
|
|
d26f066da0 | ||
|
|
30aa66bf2a | ||
|
|
18075308dd | ||
|
|
0a6b3c83c9 | ||
|
|
40e40bab12 | ||
|
|
d4bbf87703 | ||
|
|
fe097bfd14 | ||
|
|
171a283128 | ||
|
|
352fc604d7 | ||
|
|
9af22ecfb2 | ||
|
|
54203e3440 | ||
|
|
861cac921f | ||
|
|
a0fb8ab0e4 | ||
|
|
c7e7672bf3 | ||
|
|
0cd3be5401 | ||
|
|
0df613402e | ||
|
|
cb34e1a265 | ||
|
|
e32c1d39bc | ||
|
|
8a0a497d81 | ||
|
|
70b27ba55b | ||
|
|
d74a32c918 | ||
|
|
6ff7fe06e2 | ||
|
|
f7bdb4dc4d | ||
|
|
3f370584a4 | ||
|
|
34334f8e6e | ||
|
|
0f0a86934e | ||
|
|
653e714955 | ||
|
|
951a03fbac | ||
|
|
0d042cc3e6 | ||
|
|
f39ff3fcf3 | ||
|
|
6ed1801cbb | ||
|
|
53a9ff9f19 | ||
|
|
b95b909b0f | ||
|
|
92061e868a | ||
|
|
600d338bce | ||
|
|
a3e84bd865 | ||
|
|
dc6236f324 | ||
|
|
e030658947 | ||
|
|
21a08e4aa3 | ||
|
|
c85b6690da | ||
|
|
178c847c1c | ||
|
|
40f8332b4a | ||
|
|
5d34c4e946 | ||
|
|
64db1abf0f | ||
|
|
73256c5aab | ||
|
|
bd2b7ccb41 | ||
|
|
14a177ce97 | ||
|
|
2b9a9da558 | ||
|
|
b8345c2e7f | ||
|
|
9a0dbd41e9 | ||
|
|
00228a4012 | ||
|
|
935dc2792b | ||
|
|
15a9a36c00 | ||
|
|
5fc30927a7 | ||
|
|
fa89c472aa | ||
|
|
29f9538066 | ||
|
|
2b8d3b0cde | ||
|
|
cb76d39715 | ||
|
|
0926b5b9d2 | ||
|
|
8aaa7111bf | ||
|
|
b19a1bcada | ||
|
|
ac6e687107 | ||
|
|
dcd7c363c6 | ||
|
|
6996df091b | ||
|
|
76b43ba1a0 | ||
|
|
e875fead9b | ||
|
|
e1a671390c | ||
|
|
46962385b2 | ||
|
|
ef8f42de0f | ||
|
|
631b99ab01 | ||
|
|
1839f31f9a | ||
|
|
41efac21f2 | ||
|
|
aa769f8241 | ||
|
|
5c150c48f5 | ||
|
|
1a4078d2c2 | ||
|
|
e0943b84d2 | ||
|
|
640223ea7c | ||
|
|
9419f93a1d | ||
|
|
498d2bc0ea | ||
|
|
d2cd010151 | ||
|
|
df0d29724b | ||
|
|
33942dea90 | ||
|
|
fdfe614867 | ||
|
|
f71a353945 | ||
|
|
f63a1fed21 | ||
|
|
308e9f1861 | ||
|
|
e8dae6db6a | ||
|
|
298bd88fd9 | ||
|
|
5c97d757b3 | ||
|
|
ad6494fde9 | ||
|
|
4b4f76392d | ||
|
|
1ec726913f | ||
|
|
f11d8706d6 | ||
|
|
ea8306b13b | ||
|
|
791b033efc | ||
|
|
72fce2f14b | ||
|
|
a77b57e3d2 | ||
|
|
d57b803129 | ||
|
|
17428c45bf | ||
|
|
1be1eeaf31 | ||
|
|
a2c2c04d74 | ||
|
|
982f482d84 | ||
|
|
1fca6b712b | ||
|
|
d34099d49d | ||
|
|
7f4a60bf56 | ||
|
|
8f76989ae8 | ||
|
|
3ca0610544 | ||
|
|
f0d83a7cd5 | ||
|
|
713046ce64 | ||
|
|
ed6fa24f92 | ||
|
|
15c54b3d0c | ||
|
|
4f524964d0 | ||
|
|
37b330be45 | ||
|
|
4beed31d3e | ||
|
|
b5a7b43b52 | ||
|
|
448e3a0947 | ||
|
|
8dbc7e6591 | ||
|
|
05e23a1553 | ||
|
|
f77aab696c | ||
|
|
ec3b251d83 | ||
|
|
fd3591c666 | ||
|
|
1637383a19 | ||
|
|
82727700c9 | ||
|
|
7419a40b4e | ||
|
|
3635fe377f | ||
|
|
d52f811ac5 | ||
|
|
d51f8fd5ed | ||
|
|
63518cf90b | ||
|
|
2bdaa65a7d | ||
|
|
9b82caa1d0 | ||
|
|
9eed14606a | ||
|
|
2805e3b705 | ||
|
|
a0405e2b61 | ||
|
|
85dea2177d | ||
|
|
54f2ec5e2a | ||
|
|
0611d9b068 | ||
|
|
d847fc3a4f | ||
|
|
a822f05934 | ||
|
|
8c73798195 | ||
|
|
af2dc41f76 | ||
|
|
db4d74252d | ||
|
|
7dd3609078 | ||
|
|
8cbcf2ac74 | ||
|
|
4c99eb315d | ||
|
|
753d50879e | ||
|
|
e2f4e8bcab | ||
|
|
69a8da58e7 | ||
|
|
ef21614137 | ||
|
|
b83e7cdf30 | ||
|
|
521c247546 | ||
|
|
be82bf4fdd | ||
|
|
591b60db0b | ||
|
|
cbf7412d47 | ||
|
|
6ae8232e09 | ||
|
|
f6bc1326cf | ||
|
|
1511a0ed26 | ||
|
|
219088f3d1 | ||
|
|
cefd996115 | ||
|
|
11e48ed280 | ||
|
|
6209591ea5 | ||
|
|
0b3b04ab45 | ||
|
|
58c2d89fd6 | ||
|
|
b74136584a | ||
|
|
9c43dc30d2 | ||
|
|
47325e1e07 | ||
|
|
0cc8f9166d | ||
|
|
1f692b460b | ||
|
|
c2d6116fda | ||
|
|
5ea21da2cc | ||
|
|
304ddbf2fd | ||
|
|
d9752a8002 | ||
|
|
00586fcf3e | ||
|
|
ce7913c578 | ||
|
|
062e073eb0 | ||
|
|
2e6afa53f8 | ||
|
|
882afcdc1e | ||
|
|
745e295e0d | ||
|
|
0ad4659d2c | ||
|
|
83cf7b0e9f | ||
|
|
68e8196c70 | ||
|
|
163358b3fa | ||
|
|
d28a55a48f | ||
|
|
3a76110886 | ||
|
|
6cf88cb50d | ||
|
|
43892a3d43 | ||
|
|
501657c42f | ||
|
|
d8e35b7525 | ||
|
|
079fd395d7 | ||
|
|
d86ee0cd00 | ||
|
|
7922f8940e | ||
|
|
7ed5048b0b | ||
|
|
5ba84b26ab | ||
|
|
a1147fd832 | ||
|
|
e9c9340986 | ||
|
|
9c5eacf1ec | ||
|
|
b2eee5e09c | ||
|
|
edec2d6693 | ||
|
|
0415df2cd9 | ||
|
|
842fd5b01c | ||
|
|
c4d16f4ed9 | ||
|
|
e076be593d | ||
|
|
e6c854eb8b | ||
|
|
f512faba06 | ||
|
|
ad07457cf7 | ||
|
|
63c7b8229b | ||
|
|
78865f9330 | ||
|
|
c582d45fb5 | ||
|
|
dd95e66e16 | ||
|
|
82ff26f073 | ||
|
|
6640a8a4b7 | ||
|
|
0f85714268 | ||
|
|
ae2bf1fca1 | ||
|
|
88b63f96cf | ||
|
|
b9cac9c91b | ||
|
|
d5abd35beb | ||
|
|
9d45c12a6e | ||
|
|
427102ecd8 | ||
|
|
540e29e7fa | ||
|
|
6b819326b1 | ||
|
|
f4b16dfbca | ||
|
|
839970fe2b | ||
|
|
2a2fafd03f | ||
|
|
0dfaa4614f | ||
|
|
0c35749855 | ||
|
|
f99dc3e6f1 | ||
|
|
df3a9e8f74 | ||
|
|
6d05282916 | ||
|
|
f58b8b8733 | ||
|
|
c256388497 | ||
|
|
5dfacc8db4 | ||
|
|
7bace8936d | ||
|
|
75c9fb712b | ||
|
|
41dbd6892c | ||
|
|
8e5758d900 | ||
|
|
026ff73be4 | ||
|
|
316eba80fe | ||
|
|
6245c812af | ||
|
|
6bd3c41a86 | ||
|
|
5d93a0b07c | ||
|
|
e322076cb5 | ||
|
|
97b366d4c3 | ||
|
|
67433e1143 | ||
|
|
5fef2f15b8 | ||
|
|
89c8311809 | ||
|
|
aadea3f178 | ||
|
|
c002671281 | ||
|
|
cb6ce4c720 | ||
|
|
14affcdd01 | ||
|
|
f80f18cd1c | ||
|
|
ec3507a16b | ||
|
|
1f3ea9c267 | ||
|
|
7fb6d2d92e | ||
|
|
4d9f9ac28f | ||
|
|
c95a0f39e8 | ||
|
|
9ae8f5eb54 | ||
|
|
315d507485 | ||
|
|
d6aaf562c5 | ||
|
|
71acdefd0b | ||
|
|
d878b13528 | ||
|
|
c427c59797 | ||
|
|
648c4ee4ff | ||
|
|
6c43d792b2 | ||
|
|
2ead0dc6d4 | ||
|
|
0f3ed42340 | ||
|
|
441cb22abc | ||
|
|
85355336c9 | ||
|
|
78a5660bed | ||
|
|
2711cafc93 | ||
|
|
ce22ffa50e | ||
|
|
fc261e06ab | ||
|
|
b4ea1bc29b | ||
|
|
6de740ee6c | ||
|
|
6869f97689 | ||
|
|
22e01248ae | ||
|
|
47f45f63bb | ||
|
|
e0c2092563 | ||
|
|
84030eaeda | ||
|
|
6243d456ca | ||
|
|
ee81815afa | ||
|
|
1652a0d0a0 | ||
|
|
37808f6af9 | ||
|
|
fb42ad7f60 | ||
|
|
d72421806f | ||
|
|
2a08ae5f16 | ||
|
|
fe19ffda1f | ||
|
|
2eb07aebf1 | ||
|
|
6989fbfe87 | ||
|
|
07b4a0280e | ||
|
|
9bd4d6e74d | ||
|
|
4764e16ccd | ||
|
|
d29d340d84 | ||
|
|
8f3c855fe2 | ||
|
|
b1ce1e86dd | ||
|
|
ad96b5fdaa | ||
|
|
fe874f3692 | ||
|
|
4da986eaa3 | ||
|
|
284bc11ad7 | ||
|
|
fff3449174 | ||
|
|
11bcb663d2 | ||
|
|
ec6c8c94eb | ||
|
|
4ace715a8f | ||
|
|
37425a2e55 | ||
|
|
0b9dae6b98 | ||
|
|
e06751d458 | ||
|
|
43c0509b93 | ||
|
|
5826f89866 | ||
|
|
c12aa5b8b2 | ||
|
|
a5c71c6743 | ||
|
|
f030f65ce0 | ||
|
|
3661e601e5 | ||
|
|
59b5f84077 | ||
|
|
748cc4e999 | ||
|
|
63d093b47c | ||
|
|
6d08ce08ca | ||
|
|
607e8a9677 | ||
|
|
0a526b2fe9 | ||
|
|
a5f9877474 | ||
|
|
4f6b7fbb13 | ||
|
|
0b3df78cdd | ||
|
|
d2099d65af | ||
|
|
63e9658857 | ||
|
|
faa7dfbb72 | ||
|
|
d099267f6a | ||
|
|
baece1defd | ||
|
|
d5d13fd407 | ||
|
|
341c512ef9 | ||
|
|
9c7f13e12e | ||
|
|
d86cfe6d8a | ||
|
|
82f7870b07 | ||
|
|
1af56c719d | ||
|
|
09c43fa896 | ||
|
|
e175babc5e | ||
|
|
def3302bff | ||
|
|
8005e98af7 | ||
|
|
0a55202d4e | ||
|
|
77797d9d42 | ||
|
|
913940e2eb | ||
|
|
b0c02df8f5 | ||
|
|
d4c50910b3 | ||
|
|
b510f8a19b | ||
|
|
b6a9185fa0 | ||
|
|
418435957e | ||
|
|
abcb4c6a22 | ||
|
|
c54d4d799a | ||
|
|
9621fba368 | ||
|
|
ff57939a39 | ||
|
|
9b8a389def | ||
|
|
53e1b08956 | ||
|
|
bb65945ac1 | ||
|
|
37f5be5960 | ||
|
|
3082b9d626 | ||
|
|
706daafd5a | ||
|
|
23ff8a9ba5 | ||
|
|
91087854bb | ||
|
|
de08c14918 | ||
|
|
517ccb8983 | ||
|
|
243f85702d | ||
|
|
11d5f65b60 | ||
|
|
a1bbc51c11 | ||
|
|
a089097e90 | ||
|
|
6d6bc33dfe | ||
|
|
3623be1166 | ||
|
|
bbebf14837 | ||
|
|
4b05ac4258 | ||
|
|
4d3bf1f645 | ||
|
|
d09989cec2 | ||
|
|
19f390961a | ||
|
|
dc87e6a9d1 | ||
|
|
81d7f29deb | ||
|
|
c513b02483 | ||
|
|
ef50c317b8 | ||
|
|
1f6eda1d55 | ||
|
|
4db8539739 | ||
|
|
04cf2c00fa | ||
|
|
801390ca70 | ||
|
|
d491f4f585 | ||
|
|
416446b397 | ||
|
|
abfb633db5 | ||
|
|
6cb59b9c34 | ||
|
|
5415a1fc99 | ||
|
|
f4b313f3e0 | ||
|
|
000ca0ff04 | ||
|
|
508319452e | ||
|
|
ee3f21cc57 | ||
|
|
c03ac54a93 | ||
|
|
ae80a9aa06 | ||
|
|
ca35fc7b33 | ||
|
|
8cd40687ff | ||
|
|
570aa596c5 | ||
|
|
1b4da98bd3 | ||
|
|
4f30dfb588 | ||
|
|
be3a968f3e | ||
|
|
79bd797240 | ||
|
|
4c6cabca59 | ||
|
|
7e611ce77f | ||
|
|
020c753eb8 | ||
|
|
5664c0f242 | ||
|
|
b8d1567efb | ||
|
|
c7c9e5407c | ||
|
|
2f91209647 | ||
|
|
e66de7f5d4 | ||
|
|
86e18c14a6 | ||
|
|
6ea97f70f2 | ||
|
|
89489a0aa0 | ||
|
|
2153f8b675 | ||
|
|
7974417952 | ||
|
|
116e0295a7 | ||
|
|
e1cad2548e | ||
|
|
9af4984cc6 | ||
|
|
087d657ad9 | ||
|
|
eb65912998 | ||
|
|
e13333afe7 | ||
|
|
81db923989 | ||
|
|
c141300d2f | ||
|
|
912e8ed4ec | ||
|
|
9ef235169c | ||
|
|
c243dea3a2 | ||
|
|
b7260b41b1 | ||
|
|
da6106ab3b | ||
|
|
8f68b5ec3a | ||
|
|
ae27ecbe6a | ||
|
|
8705539a87 | ||
|
|
1299d365b7 | ||
|
|
36f19ba8df | ||
|
|
6397f120fc | ||
|
|
c47d226153 | ||
|
|
5472c62739 | ||
|
|
35f6822bb3 | ||
|
|
a3f319daec | ||
|
|
883e5cc179 | ||
|
|
60829dc2f8 | ||
|
|
54d2bbdaa8 | ||
|
|
ab016deb5b | ||
|
|
e7fa2d3eba | ||
|
|
80e82e60ae | ||
|
|
37a596fc8e | ||
|
|
901586a99e | ||
|
|
f8408aa959 | ||
|
|
ab80261b1c | ||
|
|
2f4e51ffc2 | ||
|
|
7c3b8e74d9 | ||
|
|
eec9b380dd | ||
|
|
63cd8e0b5a | ||
|
|
982d524c72 | ||
|
|
9bc58fd763 | ||
|
|
7c5e116b4c | ||
|
|
083946699d | ||
|
|
ce83066ced | ||
|
|
bdd82f3f57 | ||
|
|
972af358ca | ||
|
|
4bb2d77c74 | ||
|
|
152b4d585f | ||
|
|
94e2447f64 | ||
|
|
b9cf065cf8 | ||
|
|
be1a156c37 | ||
|
|
1f48612a67 | ||
|
|
9aa56fa8a3 | ||
|
|
07ad7b80e5 | ||
|
|
01f90882f9 | ||
|
|
30a194f5b8 | ||
|
|
0ef9fd47fb | ||
|
|
7dec41138f | ||
|
|
7dd09dabc8 | ||
|
|
16976d8e00 | ||
|
|
1d938505cf | ||
|
|
b088196133 | ||
|
|
cdf4f9a1a5 | ||
|
|
ae75fd80ce | ||
|
|
842df29bfe | ||
|
|
d85ca35a7d | ||
|
|
85bca0447a | ||
|
|
08619176ba | ||
|
|
701a1d6371 | ||
|
|
051a710c70 | ||
|
|
c7a35b11a5 | ||
|
|
76e1d8ddce | ||
|
|
fcd99e4018 | ||
|
|
7f000a0eca | ||
|
|
1cbcc2a107 | ||
|
|
2be67493f6 | ||
|
|
83583fb27f | ||
|
|
2bd37b50a4 | ||
|
|
c1e296ce80 | ||
|
|
f13a43f783 | ||
|
|
325726e716 | ||
|
|
bda1629210 | ||
|
|
6fc516a0d4 | ||
|
|
a69f65242f | ||
|
|
a0588fedfa | ||
|
|
63d72399e0 | ||
|
|
2d8fa4a706 | ||
|
|
56706ecc10 | ||
|
|
7b2da2ea74 | ||
|
|
2658f7e63f | ||
|
|
c06fe99fa0 | ||
|
|
78ccc08545 | ||
|
|
182244b708 | ||
|
|
ae692907bd | ||
|
|
c6f91f16f8 | ||
|
|
8c8cff3f73 | ||
|
|
230eaeede6 | ||
|
|
d16c009360 | ||
|
|
586e9731f8 | ||
|
|
f5de72c3f0 | ||
|
|
fe1d26ab96 | ||
|
|
8e35ab5eab | ||
|
|
af71cb6a2f | ||
|
|
71f5f9fe25 | ||
|
|
1a1ce973e8 | ||
|
|
6b876f8ee0 | ||
|
|
6c764a38bb | ||
|
|
69cd035ba1 | ||
|
|
ac500c58ac | ||
|
|
b6eda454bf | ||
|
|
b6790f9bbf | ||
|
|
6a09da92de | ||
|
|
e74b9d8c78 | ||
|
|
24793e1310 | ||
|
|
bcb1b6851b | ||
|
|
3345f8374e | ||
|
|
e2252e13d0 | ||
|
|
a75ca179ce | ||
|
|
75fb10a673 | ||
|
|
54a953a2ea | ||
|
|
19cb608934 | ||
|
|
b0142ba192 | ||
|
|
c0eafa1c74 | ||
|
|
0aa11c7529 | ||
|
|
745229cab6 | ||
|
|
3664cf9638 | ||
|
|
e2dd13e5bf | ||
|
|
d5aa75e2d2 | ||
|
|
65587f8e1d | ||
|
|
c94a35c6ea | ||
|
|
9827215298 | ||
|
|
2fbcbee45c | ||
|
|
58c3642dc1 | ||
|
|
ce29144951 | ||
|
|
2655d57269 | ||
|
|
db2eae3819 | ||
|
|
9a8ea5f94d | ||
|
|
9beede786a | ||
|
|
f14f39c60c | ||
|
|
65ee766579 | ||
|
|
17df9c4c0b | ||
|
|
eec8c3a115 | ||
|
|
292c5ca770 | ||
|
|
9eab39a3bc | ||
|
|
e5d35a3782 | ||
|
|
3cba82d95a | ||
|
|
a79a7738f4 | ||
|
|
ad6f2e3e46 | ||
|
|
a07eef9708 | ||
|
|
e9256832c1 | ||
|
|
73a0d82325 | ||
|
|
3c37f57e5e | ||
|
|
bb97f5f2c0 | ||
|
|
03855e4fae | ||
|
|
48a82704ae | ||
|
|
82ff7423b3 | ||
|
|
3cb9492791 | ||
|
|
69d85d4a69 | ||
|
|
74e03d9733 | ||
|
|
13d666fcf7 | ||
|
|
e224360140 | ||
|
|
5e3324b681 | ||
|
|
8c1a39cc71 | ||
|
|
ffc3821010 | ||
|
|
06d8ce30b1 | ||
|
|
bb9bb5da88 | ||
|
|
6bbe269469 | ||
|
|
76d23e2da0 | ||
|
|
166767a10c | ||
|
|
85d2499a06 | ||
|
|
d3fc29d8ed | ||
|
|
c643e02b8e | ||
|
|
9f1d8f8c4c | ||
|
|
020b792f48 | ||
|
|
06cf7b9733 | ||
|
|
4ae5517cb8 | ||
|
|
86d1ed8981 | ||
|
|
f9632021a8 | ||
|
|
ac66da899c | ||
|
|
3ebf986424 | ||
|
|
c28a47a2f4 | ||
|
|
0d9ff05bb6 | ||
|
|
fdbb3e1edc | ||
|
|
a84bdf6a58 | ||
|
|
253383516d | ||
|
|
0a14a550b9 | ||
|
|
250d1cec9d | ||
|
|
9ad3632532 | ||
|
|
2189faade6 | ||
|
|
836bc19148 | ||
|
|
89038d3186 | ||
|
|
90e571c613 | ||
|
|
d7e0e61dad | ||
|
|
e7faec0b1d | ||
|
|
ee75542b2d | ||
|
|
d519fc6110 | ||
|
|
dd3b97b0f2 | ||
|
|
e8f1a7d896 | ||
|
|
4dbe5e8c2b | ||
|
|
4f9d80c4c2 | ||
|
|
031a36cd75 | ||
|
|
c289601168 | ||
|
|
f10442fec4 | ||
|
|
30cb6f5838 | ||
|
|
ee6d371bde | ||
|
|
8081aec1bf | ||
|
|
a63a02e4de | ||
|
|
37429def51 | ||
|
|
8f4f92318e | ||
|
|
d9a5fd68ef | ||
|
|
08d67c00fa | ||
|
|
194be8ff2b | ||
|
|
9addf8325c | ||
|
|
816001b84e | ||
|
|
ecf81a8c0c | ||
|
|
0f8852dc98 | ||
|
|
74c9a21330 | ||
|
|
457cfe9480 |
BIN
.DS_Store
vendored
42
.github/workflows/build.yml
vendored
|
|
@ -1,11 +1,15 @@
|
|||
name: Build and Release IPA
|
||||
name: Build and Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build IPA and Mac Catalyst
|
||||
build-ios:
|
||||
name: Build IPA
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- name: Use Node.js 20
|
||||
|
|
@ -24,18 +28,34 @@ jobs:
|
|||
- name: Upload IPA artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sora-IPA
|
||||
path: build/Sora.ipa
|
||||
name: Sulfur-IPA
|
||||
path: build/Sulfur.ipa
|
||||
compression-level: 0
|
||||
|
||||
build-mac:
|
||||
name: Build Mac Catalyst
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run macbuild.sh
|
||||
run: |
|
||||
chmod +x macbuild.sh
|
||||
./macbuild.sh
|
||||
|
||||
- name: Upload Mac Catalyst artifact
|
||||
|
||||
- name: Create DMG
|
||||
run: |
|
||||
hdiutil create -volname "Sulfur" -srcfolder build/Sulfur.app -ov -format UDZO build/Sulfur.dmg
|
||||
|
||||
- name: Upload Mac artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sora-Catalyst
|
||||
path: build/Sora-catalyst.zip
|
||||
compression-level: 0
|
||||
name: Sulfur-Mac
|
||||
path: build/Sulfur.dmg
|
||||
compression-level: 0
|
||||
|
|
|
|||
134
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/macos,swift,xcode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,swift,xcode
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### Swift ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
# Pods/
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
||||
|
||||
### Xcode ###
|
||||
|
||||
## Xcode 8 and earlier
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcodeproj/project.xcworkspace/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,swift,xcode
|
||||
66
README.md
|
|
@ -1,18 +1,20 @@
|
|||
# Sora
|
||||
> Also known as Sulfur due to copyright considerations.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner.png" width="500px">
|
||||
<img src="https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/Sulfur.png" width="750px">
|
||||
|
||||
[](https://github.com/cranci1/Sora/actions/workflows/build.yml) [](https://img.shields.io/badge/Platform-iOS%20%7C%20iPadOS%2014.0%2B%20%26%20macOS%2012.0%2B-red?logo=apple&logoColor=white) [](https://discord.gg/XR3SrmUbpd)
|
||||
[](https://github.com/cranci1/Sora/actions/workflows/build.yml) [](https://discord.gg/XR3SrmUbpd) [](https://img.shields.io/badge/Platform-iOS%20%7C%20iPadOS%2015.0%2B%20%26%20macOS%2012.0%2B-red?logo=apple&logoColor=white)
|
||||
|
||||
A modular web scraping app, **Still in early builds** under the GPLv3.0 License
|
||||
**An iOS and macOS modular web scraping app, under the GPLv3.0 License.**
|
||||
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [License](#license)
|
||||
|
|
@ -20,37 +22,53 @@ A modular web scraping app, **Still in early builds** under the GPLv3.0 License
|
|||
|
||||
## Features
|
||||
|
||||
- [x] iOS/iPadOS 15+ support
|
||||
- [x] macOS 12+ support
|
||||
- [x] JSON module support
|
||||
- [ ] JavaScript module support
|
||||
- [x] macOS 12.0+ support
|
||||
- [x] iOS/iPadOS 15.0+ support
|
||||
- [x] JavaScript as main loader
|
||||
- [x] Download support (HLS & MP4)
|
||||
- [x] Tracking services (AniList, Trakt)
|
||||
- [x] Apple Keychain support for auth tokens
|
||||
- [x] Streams support (Jellyfin/Plex-like servers)
|
||||
- [x] External metadata providers (TMDB, AniList)
|
||||
- [x] Background playback and Picture-in-Picture (PiP) support
|
||||
- [x] External media player support (VLC, Infuse, Outplayer, nPlayer, SenPlayer, IINA, TracyPlayer)
|
||||
|
||||
## Installation
|
||||
|
||||
You can download Sora using Xcode or using the .ipa file, which you can find in the [Releases](https://github.com/cranci1/Sora/releases) tab or the [Nightly](https://nightly.link/cranci1/Sora/workflows/build/dev/Sulfur-IPA.zip) build page.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
1. **What is Sora?**
|
||||
- Sora is a module based web scraping app, made to work with modules only
|
||||
1. **What is Sora?**
|
||||
Sora is a modular web scraping application designed to work exclusively with custom modules.
|
||||
|
||||
2. **Is Sora safe?**
|
||||
- Yes, Sora is open-source and does not store user data on external servers.
|
||||
2. **Is Sora safe?**
|
||||
Yes, Sora is open-source and prioritizes user privacy. It does not store user data on external servers and does not collect crash logs.
|
||||
|
||||
3. **Will Sora ever be paid?**
|
||||
- No, Sora will always remain free without subscriptions, paid content, or any type of login.
|
||||
3. **Will Sora ever be paid?**
|
||||
No, Sora will always remain free without subscriptions, paid content, or any type of login.
|
||||
|
||||
4. **How can i get module?**
|
||||
- I do not provide any module for the app. Search for them on google or i dont know. I only created 1 module to watch videos of my school.
|
||||
4. **How can I get modules?**
|
||||
Sora does not include any modules by default. You will need to find and add the necessary modules yourself, or create your own.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
FrameWorks:
|
||||
- [SwiftSoup](https://github.com/scinfu/SwiftSoup) - MIT License
|
||||
- [KingFisher](https://github.com/onevcat/Kingfisher) - MIT License
|
||||
Frameworks:
|
||||
- [Drops](https://github.com/omaralbeik/Drops) - MIT License
|
||||
- [NukeUI](https://github.com/kean/NukeUI) - MIT License
|
||||
- [SoraCore](https://github.com/cranci1/SoraCore) - Custom License
|
||||
- [MarqueeLabel](https://github.com/cbpowell/MarqueeLabel) - MIT License
|
||||
|
||||
Misc:
|
||||
- [50/50](https://github.com/50n50) for the app icon
|
||||
- [Ciro](https://github.com/CiroHoodLove) for the episodes banners
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [GNU General Public License v3.0](LICENSE).
|
||||
This project is licensed under the [GNU General Public License v3.0](LICENSE) (GPLv3.0).
|
||||
|
||||
```
|
||||
Copyright © 2024 cranci. All rights reserved.
|
||||
Copyright © 2024-2025 cranci. All rights reserved.
|
||||
|
||||
Sora is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -68,16 +86,16 @@ along with Sora. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
## Legal
|
||||
|
||||
**_Sora is not made for Piracy! I don't promote piracy at all!_**
|
||||
**_Sora is not intended for piracy. The Sora project does not endorse or support any form of piracy._**
|
||||
|
||||
### No Liability
|
||||
|
||||
The developer(s) of this software assumes no liability for damages, legal claims, or other issues arising from the use or misuse of this software or any third-party modules. Users bear full responsibility for their actions. Use this software and modules at your own risk.
|
||||
The developer(s) of this software assume no liability for damages, legal claims, or other issues arising from the use or misuse of this software or any third-party modules. Users bear full responsibility for their actions. Use of this software and its modules is at your own risk.
|
||||
|
||||
### Third-Party Websites and Intellectual Property
|
||||
|
||||
This software is not affiliated with or endorsed by any third-party websites. Any references to third-party sites in user-generated modules do not imply endorsement. Users are responsible for verifying that their scraping activities comply with the terms of service and intellectual property rights of the sites they interact with.
|
||||
This software is not affiliated with or endorsed by any third-party entities. Any references to third-party sites in user-generated modules do not imply endorsement. Users are responsible for ensuring their scraping activities comply with the terms of service and intellectual property rights of the sites they interact with.
|
||||
|
||||
### DMCA
|
||||
|
||||
The developer(s) is not responsible for the misuse of any content inside or outside the app and shall not be responsible for the dissemination of any content within the app. Any violations should be send to the source website or module creator. The developer is not legally responsible for any module used inside the app.
|
||||
The developer(s) are not responsible for the misuse of any content inside or outside the app and shall not be held liable for the dissemination of any content within the app. Any violations should be reported to the source website or module creator. The developer bears no legal responsibility for any module used within the app.
|
||||
|
|
|
|||
|
|
@ -1,650 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 55;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBB2D19844A004CD38C /* Double+Extension.swift */; };
|
||||
1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */; };
|
||||
1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFC02D198466004CD38C /* CustomPlayer.swift */; };
|
||||
132417842D13198000B4F2D2 /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417832D13198000B4F2D2 /* SoraApp.swift */; };
|
||||
132417862D13198000B4F2D2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417852D13198000B4F2D2 /* ContentView.swift */; };
|
||||
132417882D13198200B4F2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 132417872D13198200B4F2D2 /* Assets.xcassets */; };
|
||||
1324178B2D13198200B4F2D2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */; };
|
||||
1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417932D1319E800B4F2D2 /* MiruDataStruct.swift */; };
|
||||
1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417952D1319E800B4F2D2 /* Notification.swift */; };
|
||||
132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417972D1319E800B4F2D2 /* HistoryManager.swift */; };
|
||||
132417A12D1319E800B4F2D2 /* ModuleStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417992D1319E800B4F2D2 /* ModuleStruct.swift */; };
|
||||
132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324179A2D1319E800B4F2D2 /* ModulesManager.swift */; };
|
||||
132417A32D1319E800B4F2D2 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */; };
|
||||
132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417A72D131A0600B4F2D2 /* SearchView.swift */; };
|
||||
132417B92D131A0600B4F2D2 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417A82D131A0600B4F2D2 /* SearchResultsView.swift */; };
|
||||
132417BA2D131A0600B4F2D2 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */; };
|
||||
132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */; };
|
||||
132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */; };
|
||||
132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */; };
|
||||
132417BE2D131A0600B4F2D2 /* SettingsPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */; };
|
||||
132417BF2D131A0600B4F2D2 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B02D131A0600B4F2D2 /* SettingView.swift */; };
|
||||
132417C02D131A0600B4F2D2 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B12D131A0600B4F2D2 /* HomeView.swift */; };
|
||||
132417C12D131A0600B4F2D2 /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B32D131A0600B4F2D2 /* LibraryManager.swift */; };
|
||||
132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B42D131A0600B4F2D2 /* LibraryView.swift */; };
|
||||
132417C32D131A0600B4F2D2 /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B62D131A0600B4F2D2 /* MediaView.swift */; };
|
||||
132417C42D131A0600B4F2D2 /* MediaExtraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B72D131A0600B4F2D2 /* MediaExtraction.swift */; };
|
||||
132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 132417CE2D131B7400B4F2D2 /* SwiftSoup */; };
|
||||
132417D22D131C5300B4F2D2 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132417D12D131C5300B4F2D2 /* Kingfisher */; };
|
||||
132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D42D13240200B4F2D2 /* EpisodeCell.swift */; };
|
||||
132417D72D13242400B4F2D2 /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D62D13242400B4F2D2 /* CircularProgressBar.swift */; };
|
||||
132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */; };
|
||||
1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1352BA702D1ABC30000A9AF9 /* URLSession.swift */; };
|
||||
13AEE7BA2D2451F200CA634A /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE7B92D2451F200CA634A /* GitHubAPI.swift */; };
|
||||
13AEE7BC2D24521200CA634A /* SettingsReleasesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */; };
|
||||
13B3A4B22D1477F100BCC0D5 /* SettingsStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */; };
|
||||
13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1308CFBB2D19844A004CD38C /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
1308CFC02D198466004CD38C /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||
132417802D13198000B4F2D2 /* Sora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sora.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
132417832D13198000B4F2D2 /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = "<group>"; };
|
||||
132417852D13198000B4F2D2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
132417872D13198200B4F2D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
132417932D1319E800B4F2D2 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = "<group>"; };
|
||||
132417952D1319E800B4F2D2 /* Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
|
||||
132417972D1319E800B4F2D2 /* HistoryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryManager.swift; sourceTree = "<group>"; };
|
||||
132417992D1319E800B4F2D2 /* ModuleStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModuleStruct.swift; sourceTree = "<group>"; };
|
||||
1324179A2D1319E800B4F2D2 /* ModulesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModulesManager.swift; sourceTree = "<group>"; };
|
||||
1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||
132417A72D131A0600B4F2D2 /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||
132417A82D131A0600B4F2D2 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||
132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = "<group>"; };
|
||||
132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsIUView.swift; sourceTree = "<group>"; };
|
||||
132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLogsView.swift; sourceTree = "<group>"; };
|
||||
132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsModuleView.swift; sourceTree = "<group>"; };
|
||||
132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsPlayerView.swift; sourceTree = "<group>"; };
|
||||
132417B02D131A0600B4F2D2 /* SettingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = "<group>"; };
|
||||
132417B12D131A0600B4F2D2 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
132417B32D131A0600B4F2D2 /* LibraryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = "<group>"; };
|
||||
132417B42D131A0600B4F2D2 /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||
132417B62D131A0600B4F2D2 /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
|
||||
132417B72D131A0600B4F2D2 /* MediaExtraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaExtraction.swift; sourceTree = "<group>"; };
|
||||
132417C52D131AA500B4F2D2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
132417D42D13240200B4F2D2 /* EpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = "<group>"; };
|
||||
132417D62D13242400B4F2D2 /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
|
||||
132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
1352BA6F2D1AB113000A9AF9 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
|
||||
1352BA702D1ABC30000A9AF9 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = "<group>"; };
|
||||
13AEE7B92D2451F200CA634A /* GitHubAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = "<group>"; };
|
||||
13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReleasesView.swift; sourceTree = "<group>"; };
|
||||
13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStorageView.swift; sourceTree = "<group>"; };
|
||||
13C9821E2D2152B1007A0132 /* GitHubRelease.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubRelease.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
1324177D2D13198000B4F2D2 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
132417D22D131C5300B4F2D2 /* Kingfisher in Frameworks */,
|
||||
132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1308CFBA2D19843E004CD38C /* CustomPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1308CFC02D198466004CD38C /* CustomPlayer.swift */,
|
||||
1308CFBF2D198450004CD38C /* Components */,
|
||||
);
|
||||
path = CustomPlayer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1308CFBF2D198450004CD38C /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */,
|
||||
1308CFBB2D19844A004CD38C /* Double+Extension.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417772D13198000B4F2D2 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417822D13198000B4F2D2 /* Sora */,
|
||||
132417812D13198000B4F2D2 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417812D13198000B4F2D2 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417802D13198000B4F2D2 /* Sora.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417822D13198000B4F2D2 /* Sora */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1352BA6F2D1AB113000A9AF9 /* Sora.entitlements */,
|
||||
132417C52D131AA500B4F2D2 /* Info.plist */,
|
||||
132417912D1319E800B4F2D2 /* Utils */,
|
||||
132417A52D131A0600B4F2D2 /* Views */,
|
||||
132417832D13198000B4F2D2 /* SoraApp.swift */,
|
||||
132417852D13198000B4F2D2 /* ContentView.swift */,
|
||||
132417872D13198200B4F2D2 /* Assets.xcassets */,
|
||||
132417892D13198200B4F2D2 /* Preview Content */,
|
||||
);
|
||||
path = Sora;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417892D13198200B4F2D2 /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417912D1319E800B4F2D2 /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13C9821D2D2152A0007A0132 /* GitHub */,
|
||||
1308CFBA2D19843E004CD38C /* CustomPlayer */,
|
||||
132417922D1319E800B4F2D2 /* Miru */,
|
||||
132417942D1319E800B4F2D2 /* Extensions */,
|
||||
132417962D1319E800B4F2D2 /* History */,
|
||||
132417982D1319E800B4F2D2 /* Modules */,
|
||||
1324179B2D1319E800B4F2D2 /* Player */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417922D1319E800B4F2D2 /* Miru */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417932D1319E800B4F2D2 /* MiruDataStruct.swift */,
|
||||
);
|
||||
path = Miru;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417942D1319E800B4F2D2 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417952D1319E800B4F2D2 /* Notification.swift */,
|
||||
1352BA702D1ABC30000A9AF9 /* URLSession.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417962D1319E800B4F2D2 /* History */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417972D1319E800B4F2D2 /* HistoryManager.swift */,
|
||||
);
|
||||
path = History;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417982D1319E800B4F2D2 /* Modules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417992D1319E800B4F2D2 /* ModuleStruct.swift */,
|
||||
1324179A2D1319E800B4F2D2 /* ModulesManager.swift */,
|
||||
);
|
||||
path = Modules;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1324179B2D1319E800B4F2D2 /* Player */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */,
|
||||
1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */,
|
||||
);
|
||||
path = Player;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417A52D131A0600B4F2D2 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417B12D131A0600B4F2D2 /* HomeView.swift */,
|
||||
132417B22D131A0600B4F2D2 /* LibraryViews */,
|
||||
132417A62D131A0600B4F2D2 /* SearchViews */,
|
||||
132417A92D131A0600B4F2D2 /* SettingsViews */,
|
||||
132417B52D131A0600B4F2D2 /* MediaViews */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417A62D131A0600B4F2D2 /* SearchViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417A72D131A0600B4F2D2 /* SearchView.swift */,
|
||||
132417A82D131A0600B4F2D2 /* SearchResultsView.swift */,
|
||||
);
|
||||
path = SearchViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417A92D131A0600B4F2D2 /* SettingsViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417AA2D131A0600B4F2D2 /* SubPages */,
|
||||
132417B02D131A0600B4F2D2 /* SettingView.swift */,
|
||||
);
|
||||
path = SettingsViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417AA2D131A0600B4F2D2 /* SubPages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */,
|
||||
132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */,
|
||||
132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */,
|
||||
132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */,
|
||||
132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */,
|
||||
13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */,
|
||||
13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */,
|
||||
);
|
||||
path = SubPages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417B22D131A0600B4F2D2 /* LibraryViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417B42D131A0600B4F2D2 /* LibraryView.swift */,
|
||||
132417B32D131A0600B4F2D2 /* LibraryManager.swift */,
|
||||
);
|
||||
path = LibraryViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417B52D131A0600B4F2D2 /* MediaViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417D32D1323F500B4F2D2 /* EpisodesCell */,
|
||||
132417B62D131A0600B4F2D2 /* MediaView.swift */,
|
||||
132417B72D131A0600B4F2D2 /* MediaExtraction.swift */,
|
||||
);
|
||||
path = MediaViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417D32D1323F500B4F2D2 /* EpisodesCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417D42D13240200B4F2D2 /* EpisodeCell.swift */,
|
||||
132417D62D13242400B4F2D2 /* CircularProgressBar.swift */,
|
||||
);
|
||||
path = EpisodesCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13C9821D2D2152A0007A0132 /* GitHub */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13C9821E2D2152B1007A0132 /* GitHubRelease.swift */,
|
||||
13AEE7B92D2451F200CA634A /* GitHubAPI.swift */,
|
||||
);
|
||||
path = GitHub;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
1324177F2D13198000B4F2D2 /* Sora */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 1324178E2D13198200B4F2D2 /* Build configuration list for PBXNativeTarget "Sora" */;
|
||||
buildPhases = (
|
||||
1324177C2D13198000B4F2D2 /* Sources */,
|
||||
1324177D2D13198000B4F2D2 /* Frameworks */,
|
||||
1324177E2D13198000B4F2D2 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Sora;
|
||||
packageProductDependencies = (
|
||||
132417CE2D131B7400B4F2D2 /* SwiftSoup */,
|
||||
132417D12D131C5300B4F2D2 /* Kingfisher */,
|
||||
);
|
||||
productName = Sora;
|
||||
productReference = 132417802D13198000B4F2D2 /* Sora.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
132417782D13198000B4F2D2 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1320;
|
||||
LastUpgradeCheck = 1320;
|
||||
TargetAttributes = {
|
||||
1324177F2D13198000B4F2D2 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 1324177B2D13198000B4F2D2 /* Build configuration list for PBXProject "Sora" */;
|
||||
compatibilityVersion = "Xcode 13.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 132417772D13198000B4F2D2;
|
||||
packageReferences = (
|
||||
132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||
132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
);
|
||||
productRefGroup = 132417812D13198000B4F2D2 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
1324177F2D13198000B4F2D2 /* Sora */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
1324177E2D13198000B4F2D2 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1324178B2D13198200B4F2D2 /* Preview Assets.xcassets in Resources */,
|
||||
132417882D13198200B4F2D2 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
1324177C2D13198000B4F2D2 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13B3A4B22D1477F100BCC0D5 /* SettingsStorageView.swift in Sources */,
|
||||
132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */,
|
||||
132417C42D131A0600B4F2D2 /* MediaExtraction.swift in Sources */,
|
||||
132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */,
|
||||
1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */,
|
||||
132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */,
|
||||
1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */,
|
||||
132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */,
|
||||
132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */,
|
||||
1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */,
|
||||
132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */,
|
||||
132417862D13198000B4F2D2 /* ContentView.swift in Sources */,
|
||||
13AEE7BA2D2451F200CA634A /* GitHubAPI.swift in Sources */,
|
||||
132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */,
|
||||
132417A32D1319E800B4F2D2 /* NormalPlayer.swift in Sources */,
|
||||
132417D72D13242400B4F2D2 /* CircularProgressBar.swift in Sources */,
|
||||
132417C02D131A0600B4F2D2 /* HomeView.swift in Sources */,
|
||||
132417BF2D131A0600B4F2D2 /* SettingView.swift in Sources */,
|
||||
132417C32D131A0600B4F2D2 /* MediaView.swift in Sources */,
|
||||
132417A12D1319E800B4F2D2 /* ModuleStruct.swift in Sources */,
|
||||
132417B92D131A0600B4F2D2 /* SearchResultsView.swift in Sources */,
|
||||
13AEE7BC2D24521200CA634A /* SettingsReleasesView.swift in Sources */,
|
||||
132417842D13198000B4F2D2 /* SoraApp.swift in Sources */,
|
||||
132417BE2D131A0600B4F2D2 /* SettingsPlayerView.swift in Sources */,
|
||||
132417C12D131A0600B4F2D2 /* LibraryManager.swift in Sources */,
|
||||
132417BA2D131A0600B4F2D2 /* SettingsAboutView.swift in Sources */,
|
||||
1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */,
|
||||
1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */,
|
||||
132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */,
|
||||
13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */,
|
||||
1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */,
|
||||
132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
1324178C2D13198200B4F2D2 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1324178D2D13198200B4F2D2 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
1324178F2D13198200B4F2D2 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Sora/Sora.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 399LMK6Q2Y;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sora/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
132417902D13198200B4F2D2 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Sora/Sora.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 399LMK6Q2Y;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sora/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
1324177B2D13198000B4F2D2 /* Build configuration list for PBXProject "Sora" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1324178C2D13198200B4F2D2 /* Debug */,
|
||||
1324178D2D13198200B4F2D2 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
1324178E2D13198200B4F2D2 /* Build configuration list for PBXNativeTarget "Sora" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1324178F2D13198200B4F2D2 /* Debug */,
|
||||
132417902D13198200B4F2D2 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 2.4.0;
|
||||
};
|
||||
};
|
||||
132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 7.9.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
132417CE2D131B7400B4F2D2 /* SwiftSoup */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
132417D12D131C5300B4F2D2 /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 132417782D13198000B4F2D2 /* Project object */;
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Kingfisher",
|
||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e",
|
||||
"version": "7.9.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftSoup",
|
||||
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5386dab25134eec11fc35fc5e43caf422fad0270",
|
||||
"version": "2.4.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>Sora.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Sora/Assets.xcassets/AccentColor.colorset/.DS_Store
vendored
|
|
@ -2,8 +2,31 @@
|
|||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"platform" : "universal",
|
||||
"reference" : "systemMintColor"
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
|
@ -1,111 +1,33 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40-2.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
"filename" : "lightmode.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "darkmode.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120-1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.jpg",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "tinting.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
BIN
Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/lightmode.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/tinting.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
21
Sora/Assets.xcassets/Discord Icon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Discord Icon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/Discord Icon.imageset/Discord Icon.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
21
Sora/Assets.xcassets/Github Icon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Github Icon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/Github Icon.imageset/Github Icon.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
21
Sora/Assets.xcassets/SplashScreenIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "SplashScreenIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/SplashScreenIcon.imageset/SplashScreenIcon.png
vendored
Normal file
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -2,117 +2,124 @@
|
|||
// ContentView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
// Created by Francesco on 06/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var modulesManager: ModulesManager
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
HomeView()
|
||||
.tabItem {
|
||||
Label("Home", systemImage: "house")
|
||||
}
|
||||
LibraryView()
|
||||
.tabItem {
|
||||
Label("Library", systemImage: "books.vertical")
|
||||
}
|
||||
SearchView()
|
||||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
}
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gearshape")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
checkForUpdate()
|
||||
Logger.shared.log("Started Sora")
|
||||
}
|
||||
}
|
||||
|
||||
func checkForUpdate() {
|
||||
fetchLatestRelease { release in
|
||||
guard let release = release else { return }
|
||||
|
||||
let latestVersion = release.tagName
|
||||
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||
|
||||
if latestVersion.compare(currentVersion, options: .numeric) == .orderedDescending {
|
||||
DispatchQueue.main.async {
|
||||
showUpdateAlert(release: release)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchLatestRelease(completion: @escaping (GitHubRelease?) -> Void) {
|
||||
let url = URL(string: "https://api.github.com/repos/cranci1/Sora/releases/latest")!
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, response, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let release = try? JSONDecoder().decode(GitHubRelease.self, from: data)
|
||||
completion(release)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func showUpdateAlert(release: GitHubRelease) {
|
||||
let alert = UIAlertController(title: "Update Available", message: "A new version (\(release.tagName)) is available. Would you like to update Sora?", preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Update", style: .default, handler: { _ in
|
||||
self.showInstallOptionsAlert(release: release)
|
||||
}))
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootViewController = windowScene.windows.first?.rootViewController {
|
||||
rootViewController.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func showInstallOptionsAlert(release: GitHubRelease) {
|
||||
let installAlert = UIAlertController(title: "Install Update", message: "Choose an installation method:", preferredStyle: .alert)
|
||||
|
||||
let downloadUrl = release.assets.first?.browserDownloadUrl ?? ""
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Install in AltStore", style: .default, handler: { _ in
|
||||
if let url = URL(string: "altstore://install?url=\(downloadUrl)") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}))
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Install in Sidestore", style: .default, handler: { _ in
|
||||
if let url = URL(string: "sidestore://install?url=\(downloadUrl)") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}))
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { _ in
|
||||
if let url = URL(string: downloadUrl) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}))
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootViewController = windowScene.windows.first?.rootViewController {
|
||||
rootViewController.present(installAlert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
.environmentObject(LibraryManager())
|
||||
.environmentObject(ModuleManager())
|
||||
.environmentObject(Settings())
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@AppStorage("useNativeTabBar") private var useNativeTabBar: Bool = false
|
||||
@State var selectedTab: Int = 0
|
||||
@State var lastTab: Int = 0
|
||||
@State private var searchQuery: String = ""
|
||||
@State private var shouldShowTabBar: Bool = true
|
||||
@State private var tabBarOffset: CGFloat = 0
|
||||
@State private var tabBarVisible: Bool = true
|
||||
@State private var lastHideTime: Date = Date()
|
||||
|
||||
let tabs: [TabItem] = [
|
||||
TabItem(icon: "square.stack", title: NSLocalizedString("LibraryTab", comment: "")),
|
||||
TabItem(icon: "arrow.down.circle", title: NSLocalizedString("DownloadsTab", comment: "")),
|
||||
TabItem(icon: "gearshape", title: NSLocalizedString("SettingsTab", comment: "")),
|
||||
TabItem(icon: "magnifyingglass", title: NSLocalizedString("SearchTab", comment: ""))
|
||||
]
|
||||
|
||||
private func tabView(for index: Int) -> some View {
|
||||
switch index {
|
||||
case 1: return AnyView(DownloadView())
|
||||
case 2: return AnyView(SettingsView())
|
||||
case 3: return AnyView(SearchView(searchQuery: $searchQuery))
|
||||
default: return AnyView(LibraryView())
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 26, *), useNativeTabBar == true {
|
||||
TabView {
|
||||
ForEach(Array(tabs.enumerated()), id: \.offset) { index, item in
|
||||
tabView(for: index)
|
||||
.tabItem {
|
||||
Label(item.title, systemImage: item.icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchQuery)
|
||||
} else {
|
||||
ZStack(alignment: .bottom) {
|
||||
ZStack {
|
||||
tabView(for: selectedTab)
|
||||
.id(selectedTab)
|
||||
.transition(.opacity)
|
||||
.animation(.easeInOut(duration: 0.3), value: selectedTab)
|
||||
}
|
||||
.onPreferenceChange(TabBarVisibilityKey.self) { shouldShowTabBar = $0 }
|
||||
|
||||
if shouldShowTabBar {
|
||||
TabBar(
|
||||
tabs: tabs,
|
||||
selectedTab: $selectedTab
|
||||
)
|
||||
.opacity(shouldShowTabBar && tabBarVisible ? 1 : 0)
|
||||
.offset(y: tabBarVisible ? 0 : 120)
|
||||
.animation(.spring(response: 0.15, dampingFraction: 0.7), value: tabBarVisible)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.padding(.bottom, -20)
|
||||
.onAppear {
|
||||
setupNotificationObservers()
|
||||
}
|
||||
.onDisappear {
|
||||
removeNotificationObservers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNotificationObservers() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .hideTabBar,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
lastHideTime = Date()
|
||||
tabBarVisible = false
|
||||
Logger.shared.log("Tab bar hidden", type: "Debug")
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .showTabBar,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
let timeSinceHide = Date().timeIntervalSince(lastHideTime)
|
||||
if timeSinceHide > 0.2 {
|
||||
tabBarVisible = true
|
||||
Logger.shared.log("Tab bar shown after \(timeSinceHide) seconds", type: "Debug")
|
||||
} else {
|
||||
Logger.shared.log("Tab bar show request ignored, only \(timeSinceHide) seconds since hide", type: "Debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeNotificationObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: .hideTabBar, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct TabBarVisibilityKey: PreferenceKey {
|
||||
static var defaultValue: Bool = true
|
||||
static func reduce(value: inout Bool, nextValue: () -> Bool) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,30 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>ar</string>
|
||||
<string>bos</string>
|
||||
<string>cs</string>
|
||||
<string>nl</string>
|
||||
<string>fr</string>
|
||||
<string>de</string>
|
||||
<string>it</string>
|
||||
<string>kk</string>
|
||||
<string>mn</string>
|
||||
<string>nn</string>
|
||||
<string>ru</string>
|
||||
<string>sk</string>
|
||||
<string>es</string>
|
||||
<string>sv</string>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
|
@ -11,13 +35,30 @@
|
|||
<string>me.cranci.scheme</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ryu</string>
|
||||
<string>sora</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>tracy</string>
|
||||
<string>iina</string>
|
||||
<string>outplayer</string>
|
||||
<string>infuse</string>
|
||||
<string>vlc</string>
|
||||
<string>nplayer-https</string>
|
||||
<string>senplayer</string>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
492
Sora/Localization/ar.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
/* General */
|
||||
"About" = "حول";
|
||||
"About Sora" = "حول Sora";
|
||||
"Active" = "نشط";
|
||||
"Active Downloads" = "التنزيلات النشطة";
|
||||
"Actively downloading media can be tracked from here." = "يمكن تتبع المحتوى الذي يتم تنزيله حاليًا من هنا.";
|
||||
"Add Module" = "إضافة وحدة";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "اضبط عدد عناصر المحتوى في كل صف في الوضعين الرأسي والأفقي.";
|
||||
"Advanced" = "متقدم";
|
||||
"AKA Sulfur" = "يعرف أيضًا باسم Sulfur";
|
||||
"All Bookmarks" = "العناصر المحفوظة";
|
||||
"All Watching" = "الكل قيد المشاهدة";
|
||||
"Also known as Sulfur" = "يعرف أيضًا باسم Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "معرّف AniList";
|
||||
"AniList Match" = "مطابقة AniList";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "يتم جمع بيانات مجهولة المصدر لتحسين التطبيق. لا يتم جمع أي معلومات شخصية. يمكن تعطيل هذا في أي وقت.";
|
||||
"App Info" = "معلومات التطبيق";
|
||||
"App Language" = "لغة التطبيق";
|
||||
"App Storage" = "تخزين التطبيق";
|
||||
"Appearance" = "المظهر";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "هل أنت متأكد من أنك تريد مسح جميع بيانات ذاكرة التخزين المؤقت؟ سيساعد هذا في تحرير مساحة التخزين.";
|
||||
"Are you sure you want to delete '%@'?" = "هل أنت متأكد أنك تريد حذف '%@'؟";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "هل أنت متأكد أنك تريد حذف جميع الحلقات البالغ عددها %1$d في '%2$@'؟";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "هل أنت متأكد من أنك تريد حذف جميع الأصول التي تم تنزيلها؟ يمكنك اختيار مسح المكتبة فقط مع الاحتفاظ بالملفات التي تم تنزيلها للاستخدام في المستقبل.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "هل أنت متأكد أنك تريد محو جميع بيانات التطبيق؟ هذا الإجراء لا يمكن التراجع عنه.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "التفعيل في الخلفية";
|
||||
"Bookmark items for an easier access later." = "احفظ العناصر للوصول إليها بسهولة لاحقًا.";
|
||||
"Bookmarks" = "العناصر المحفوظة";
|
||||
"Bottom Padding" = "الحشوة السفلية";
|
||||
"Cancel" = "إلغاء";
|
||||
"Cellular Quality" = "جودة بيانات الجوال";
|
||||
"Check out some community modules here!" = "اطلع على بعض وحدات المجتمع هنا!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "اختر دقة الفيديو المفضلة لاتصالات WiFi وبيانات الجوال. الدقة الأعلى تستهلك المزيد من البيانات ولكنها توفر جودة أفضل. إذا لم تكن الجودة الدقيقة متاحة، سيتم اختيار الخيار الأقرب تلقائيًا.\n\nملاحظة: ليست كل مصادر الفيديو والمشغلات تدعم اختيار الجودة. تعمل هذه الميزة بشكل أفضل مع تدفقات HLS باستخدام مشغل Sora.";
|
||||
"Clear" = "مسح";
|
||||
"Clear All Downloads" = "مسح كل التنزيلات";
|
||||
"Clear Cache" = "مسح ذاكرة التخزين المؤقت";
|
||||
"Clear Library Only" = "مسح المكتبة فقط";
|
||||
"Clear Logs" = "مسح السجلات";
|
||||
"Click the plus button to add a module!" = "انقر على زر الإضافة لإضافة وحدة!";
|
||||
"Continue Watching" = "متابعة المشاهدة";
|
||||
"Continue Watching Episode %d" = "متابعة مشاهدة الحلقة %d";
|
||||
"Contributors" = "المساهمون";
|
||||
"Copied to Clipboard" = "تم النسخ إلى الحافظة";
|
||||
"Copy to Clipboard" = "نسخ إلى الحافظة";
|
||||
"Copy URL" = "نسخ الرابط";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld حلقات";
|
||||
"%lld of %lld" = "%1$lld من %2$lld";
|
||||
"%lld-%lld" = "%1$lld-%2$lld";
|
||||
"%lld%% seen" = "تمت مشاهدة %lld%%";
|
||||
"Episode %lld" = "الحلقة %lld";
|
||||
"Episodes" = "الحلقات";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "قد لا تكون الحلقات متاحة بعد أو قد تكون هناك مشكلة في المصدر.";
|
||||
"Episodes Range" = "نطاق الحلقات";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "داكن";
|
||||
"DATA & LOGS" = "البيانات والسجلات";
|
||||
"Debug" = "تصحيح الأخطاء";
|
||||
"Debugging and troubleshooting." = "تصحيح الأخطاء وإصلاحها.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "حذف";
|
||||
"Delete All" = "حذف الكل";
|
||||
"Delete All Downloads" = "حذف كل التنزيلات";
|
||||
"Delete All Episodes" = "حذف كل الحلقات";
|
||||
"Delete Download" = "حذف التنزيل";
|
||||
"Delete Episode" = "حذف الحلقة";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "انقر نقرًا مزدوجًا للتقديم";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "النقر المزدوج على جانبي الشاشة سيقوم بالتقديم حسب إعداد النقر القصير.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "تنزيل";
|
||||
"Download Episode" = "تنزيل الحلقة";
|
||||
"Download Summary" = "ملخص التنزيل";
|
||||
"Download This Episode" = "تنزيل هذه الحلقة";
|
||||
"Downloaded" = "تم التنزيل";
|
||||
"Downloaded Shows" = "العروض المنزّلة";
|
||||
"Downloading" = "جاري التنزيل";
|
||||
"Downloads" = "التنزيلات";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "تفعيل التحليلات";
|
||||
"Enable Subtitles" = "تفعيل الترجمة";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "محو";
|
||||
"Erase all App Data" = "محو كل بيانات التطبيق";
|
||||
"Erase App Data" = "محو بيانات التطبيق";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "خطأ";
|
||||
"Error Fetching Results" = "خطأ في جلب النتائج";
|
||||
"Errors and critical issues." = "الأخطاء والمشاكل الحرجة.";
|
||||
"Failed to load contributors" = "فشل تحميل المساهمين";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "جلب بيانات الحلقة الوصفية";
|
||||
"Files Downloaded" = "الملفات المنزّلة";
|
||||
"Font Size" = "حجم الخط";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "فرض الوضع الأفقي";
|
||||
"General" = "عام";
|
||||
"General events and activities." = "الأحداث والأنشطة العامة.";
|
||||
"General Preferences" = "الإعدادات العامة";
|
||||
"Hide Splash Screen" = "إخفاء شاشة البداية";
|
||||
"HLS video downloading." = "تنزيل فيديو HLS.";
|
||||
"Hold Speed" = "سرعة الضغط المطول";
|
||||
|
||||
/* Info */
|
||||
"Info" = "معلومات";
|
||||
"INFOS" = "معلومات";
|
||||
"Installed Modules" = "الوحدات المثبتة";
|
||||
"Interface" = "الواجهة";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "انضم إلى ديسكورد";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "أعمدة الوضع الأفقي";
|
||||
"Language" = "اللغة";
|
||||
"LESS" = "أقل";
|
||||
|
||||
/* Library */
|
||||
"Library" = "المكتبة";
|
||||
"License (GPLv3.0)" = "الرخصة (GPLv3.0)";
|
||||
"Light" = "فاتح";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "جاري تحميل الحلقة %lld...";
|
||||
"Loading logs..." = "جاري تحميل السجلات...";
|
||||
"Loading module information..." = "جاري تحميل معلومات الوحدة...";
|
||||
"Loading Stream" = "جاري تحميل البث";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "تسجيل معلومات تصحيح الأخطاء";
|
||||
"Log Filters" = "مرشحات السجل";
|
||||
"Log In with AniList" = "تسجيل الدخول باستخدام AniList";
|
||||
"Log In with Trakt" = "تسجيل الدخول باستخدام Trakt";
|
||||
"Log Out from AniList" = "تسجيل الخروج من AniList";
|
||||
"Log Out from Trakt" = "تسجيل الخروج من Trakt";
|
||||
"Log Types" = "أنواع السجل";
|
||||
"Logged in as" = "تم تسجيل الدخول باسم";
|
||||
"Logged in as " = "تم تسجيل الدخول باسم ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "السجلات";
|
||||
"Long press Skip" = "ضغط مطول للتخطي";
|
||||
"MAIN" = "الرئيسية";
|
||||
"Main Developer" = "المطور الرئيسي";
|
||||
"MAIN SETTINGS" = "الإعدادات الرئيسية";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "تمييز كل ما سبق كمشاهد";
|
||||
"Mark as Watched" = "تمييز كمشاهد";
|
||||
"Mark Episode as Watched" = "تمييز الحلقة كمشاهدة";
|
||||
"Mark Previous Episodes as Watched" = "تمييز الحلقات السابقة كمشاهدة";
|
||||
"Mark watched" = "تمييز كمشاهد";
|
||||
"Match with AniList" = "مطابقة مع AniList";
|
||||
"Match with TMDB" = "مطابقة مع TMDB";
|
||||
"Matched ID: %lld" = "المعرّف المطابق: %lld";
|
||||
"Matched with: %@" = "تمت المطابقة مع: %@";
|
||||
"Max Concurrent Downloads" = "الحد الأقصى للتنزيلات المتزامنة";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "تخطيط شبكة المحتوى";
|
||||
"Media Player" = "مشغل المحتوى";
|
||||
"Media View" = "عرض المحتوى";
|
||||
"Metadata Provider" = "مزود البيانات الوصفية";
|
||||
"Metadata Providers Order" = "ترتيب مزودي البيانات الوصفية";
|
||||
"Module Removed" = "تمت إزالة الوحدة";
|
||||
"Modules" = "الوحدات";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "الوحدات";
|
||||
"MORE" = "المزيد";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "لا توجد تنزيلات نشطة";
|
||||
"No AniList matches found" = "لم يتم العثور على مطابقات في AniList";
|
||||
"No Data Available" = "لا توجد بيانات متاحة";
|
||||
"No Downloads" = "لا توجد تنزيلات";
|
||||
"No episodes available" = "لا توجد حلقات متاحة";
|
||||
"No Episodes Available" = "لا توجد حلقات متاحة";
|
||||
"No items to continue watching." = "لا توجد عناصر لمتابعة مشاهدتها.";
|
||||
"No matches found" = "لم يتم العثور على نتائج";
|
||||
"No Module Selected" = "لم يتم تحديد أي وحدة";
|
||||
"No Modules" = "لا توجد وحدات";
|
||||
"No Results Found" = "لم يتم العثور على نتائج";
|
||||
"No Search Results Found" = "لم يتم العثور على نتائج بحث";
|
||||
"Nothing to Continue Watching" = "لا شيء لمتابعة مشاهدته";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "ملاحظة: سيتم استبدال الوحدات فقط في حالة وجود سلسلة إصدار مختلفة داخل ملف JSON.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "موافق";
|
||||
"Open Community Library" = "فتح مكتبة المجتمع";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "فتح في AniList";
|
||||
"Original Poster" = "الملصق الأصلي";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "متوقف مؤقتًا";
|
||||
"Play" = "تشغيل";
|
||||
"Player" = "المشغل";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "يرجى إعادة تشغيل التطبيق لتطبيق تغيير اللغة.";
|
||||
"Please select a module from settings" = "يرجى تحديد وحدة من الإعدادات";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "أعمدة الوضع الرأسي";
|
||||
"Progress bar Marker Color" = "لون علامة شريط التقدم";
|
||||
"Provider: %@" = "المزود: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "قائمة الانتظار";
|
||||
"Queued" = "في قائمة الانتظار";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "سيظهر المحتوى الذي تمت مشاهدته مؤخرًا هنا.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "تحديث الوحدات عند التشغيل";
|
||||
"Refresh Storage Info" = "تحديث معلومات التخزين";
|
||||
"Remember Playback speed" = "تذكر سرعة التشغيل";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "إزالة";
|
||||
"Remove All Cache" = "إزالة كل ذاكرة التخزين المؤقت";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "إزالة كل المستندات";
|
||||
"Remove Documents" = "إزالة المستندات";
|
||||
"Remove Downloaded Media" = "إزالة المحتوى المنزل";
|
||||
"Remove Downloads" = "إزالة التنزيلات";
|
||||
"Remove from Bookmarks" = "إزالة من العناصر المحفوظة";
|
||||
"Remove Item" = "إزالة العنصر";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "الإبلاغ عن مشكلة";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "إعادة تعيين";
|
||||
"Reset AniList ID" = "إعادة تعيين معرّف AniList";
|
||||
"Reset Episode Progress" = "إعادة تعيين تقدم الحلقة";
|
||||
"Reset progress" = "إعادة تعيين التقدم";
|
||||
"Reset Progress" = "إعادة تعيين التقدم";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "إعادة التشغيل مطلوبة";
|
||||
"Running Sora %@ - cranci1" = "يعمل Sora %@ - بواسطة cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "حفظ";
|
||||
"Search" = "بحث";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "بحث في التنزيلات";
|
||||
"Search for something..." = "ابحث عن شيء ما...";
|
||||
"Search..." = "بحث...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "الموسم %d";
|
||||
"Season %lld" = "الموسم %lld";
|
||||
"Segments Color" = "لون الأجزاء";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "تحديد وحدة";
|
||||
"Set Custom AniList ID" = "تعيين معرّف AniList مخصص";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "الإعدادات";
|
||||
"Shadow" = "ظل";
|
||||
"Show More (%lld more characters)" = "عرض المزيد (%lld أحرف إضافية)";
|
||||
"Show PiP Button" = "إظهار زر صورة داخل صورة";
|
||||
"Show Skip 85s Button" = "إظهار زر تخطي 85 ثانية";
|
||||
"Show Skip Intro / Outro Buttons" = "إظهار أزرار تخطي المقدمة / الخاتمة";
|
||||
"Shows" = "العروض";
|
||||
"Size (%@)" = "الحجم (%@)";
|
||||
"Skip Settings" = "إعدادات التخطي";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "بعض الميزات تقتصر على مشغل Sora والمشغل الافتراضي، مثل فرض الوضع الأفقي، وسرعة الضغط المطول، وزيادات التخطي الزمني المخصصة.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ بواسطة cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora و cranci1 غير تابعين لـ AniList أو Trakt بأي شكل من الأشكال.\n\nيرجى ملاحظة أن تحديثات التقدم قد لا تكون دقيقة بنسبة 100%.";
|
||||
"Sora GitHub Repository" = "مستودع Sora على GitHub";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "سيظل Sora/Sulfur دائمًا مجانيًا وبدون إعلانات!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "فرز";
|
||||
"Speed Settings" = "إعدادات السرعة";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "ابدأ المشاهدة";
|
||||
"Start Watching Episode %d" = "ابدأ مشاهدة الحلقة %d";
|
||||
"Storage Used" = "المساحة المستخدمة";
|
||||
"Stream" = "بث";
|
||||
"Streaming and video playback." = "البث وتشغيل الفيديو.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "لون الترجمة";
|
||||
"Subtitle Settings" = "إعدادات الترجمة";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "مزامنة تقدم الأنمي";
|
||||
"Sync TV shows progress" = "مزامنة تقدم المسلسلات";
|
||||
|
||||
/* System */
|
||||
"System" = "النظام";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "انقر على عنوان لتجاوز المطابقة الحالية.";
|
||||
"Tap Skip" = "انقر للتخطي";
|
||||
"Tap to manage your modules" = "انقر لإدارة وحداتك";
|
||||
"Tap to select a module" = "انقر لتحديد وحدة";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "تساعد ذاكرة التخزين المؤقت للتطبيق في تحميل الصور بشكل أسرع.\n\nسيؤدي مسح مجلد المستندات إلى حذف جميع الوحدات التي تم تنزيلها.\n\nلا تقم بمحو بيانات التطبيق إلا إذا كنت تفهم العواقب — فقد يتسبب ذلك في تعطل التطبيق.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "يتحكم نطاق الحلقات في عدد الحلقات التي تظهر في كل صفحة. يتم تجميع الحلقات في مجموعات (مثل 1-25، 26-50، وهكذا)، مما يتيح لك التنقل بينها بسهولة أكبر.\n\nبالنسبة لبيانات الحلقة الوصفية، فإنها تشير إلى الصورة المصغرة للحلقة وعنوانها، حيث يمكن أن تحتوي أحيانًا على حرق للأحداث.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "قدمت الوحدة حلقة واحدة فقط، ومن المرجح أن يكون هذا فيلمًا، لذلك قررنا إنشاء شاشات منفصلة لهذه الحالات.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "عرض الصور المصغرة";
|
||||
"TMDB Match" = "مطابقة TMDB";
|
||||
"Trackers" = "منصات المتابعة";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "جرب كلمات مفتاحية مختلفة";
|
||||
"Try different search terms" = "جرب مصطلحات بحث مختلفة";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "اضغط بإصبعين للإيقاف المؤقت";
|
||||
"Unable to fetch matches. Please try again later." = "تعذر جلب المطابقات. يرجى المحاولة مرة أخرى لاحقًا.";
|
||||
"Use TMDB Poster Image" = "استخدام صورة ملصق TMDB";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "مشغل الفيديو";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "تفضيلات جودة الفيديو";
|
||||
"View All" = "عرض الكل";
|
||||
"Watched" = "تمت مشاهدته";
|
||||
"Why am I not seeing any episodes?" = "لماذا لا أرى أي حلقات؟";
|
||||
"WiFi Quality" = "جودة WiFi";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "أنت غير مسجل الدخول";
|
||||
"You have no items saved." = "ليس لديك عناصر محفوظة.";
|
||||
"Your downloaded episodes will appear here" = "ستظهر حلقاتك المنزّلة هنا";
|
||||
"Your recently watched content will appear here" = "سيظهر المحتوى الذي شاهدته مؤخرًا هنا";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "إعدادات التنزيل";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "يتحكم الحد الأقصى للتنزيلات المتزامنة في عدد الحلقات التي يمكن تنزيلها في وقت واحد. قد تستهلك القيم الأعلى المزيد من النطاق الترددي وموارد الجهاز.";
|
||||
"Quality" = "الجودة";
|
||||
"Max Concurrent Downloads" = "الحد الأقصى للتنزيلات المتزامنة";
|
||||
"Allow Cellular Downloads" = "السماح بالتنزيلات عبر بيانات الجوال";
|
||||
"Quality Information" = "معلومات الجودة";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "إدارة التخزين";
|
||||
"Storage Used" = "المساحة المستخدمة";
|
||||
"Library cleared successfully" = "تم مسح المكتبة بنجاح";
|
||||
"All downloads deleted successfully" = "تم حذف جميع التنزيلات بنجاح";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "عمليات البحث الأخيرة";
|
||||
"me frfr" = "me frfr";
|
||||
"Data" = "البيانات";
|
||||
"Maximum Quality Available" = "أعلى جودة متاحة";
|
||||
|
||||
/* New additions */
|
||||
"DownloadCountFormat" = "%d من %d";
|
||||
"Error loading chapter" = "حدث خطأ أثناء تحميل الفصل";
|
||||
"Font Size: %dpt" = "حجم الخط: %d نقطة";
|
||||
"Line Spacing: %.1f" = "تباعد الأسطر: %.1f";
|
||||
"Line Spacing" = "تباعد الأسطر";
|
||||
"Margin: %dpx" = "الهامش: %d بكسل";
|
||||
"Margin" = "الهامش";
|
||||
"Auto Scroll Speed" = "سرعة التمرير التلقائي";
|
||||
"Speed" = "السرعة";
|
||||
"Speed: %.1fx" = "السرعة: %.1fx";
|
||||
"Matched %@: %@" = "%@: %@ متطابق";
|
||||
"Enter the AniList ID for this series" = "أدخل معرف AniList لهذه السلسلة";
|
||||
|
||||
/* New additions */
|
||||
"Create Collection" = "إنشاء مجموعة";
|
||||
"Collection Name" = "اسم المجموعة";
|
||||
"Rename Collection" = "إعادة تسمية المجموعة";
|
||||
"Rename" = "إعادة تسمية";
|
||||
"All Reading" = "كل القراءة";
|
||||
"Recently Added" = "أضيفت مؤخراً";
|
||||
"Novel Title" = "عنوان الرواية";
|
||||
"Read Progress" = "تقدم القراءة";
|
||||
"Date Created" = "تاريخ الإنشاء";
|
||||
"Name" = "الاسم";
|
||||
"Item Count" = "عدد العناصر";
|
||||
"Date Added" = "تاريخ الإضافة";
|
||||
"Title" = "العنوان";
|
||||
"Source" = "المصدر";
|
||||
"Search reading..." = "ابحث في القراءة...";
|
||||
"Search collections..." = "ابحث في المجموعات...";
|
||||
"Search bookmarks..." = "ابحث في الإشارات المرجعية...";
|
||||
"%d items" = "%d عناصر";
|
||||
"Fetching Data" = "جاري جلب البيانات";
|
||||
"Please wait while fetching." = "يرجى الانتظار أثناء الجلب.";
|
||||
"Start Reading" = "ابدأ القراءة";
|
||||
"Chapters" = "الفصول";
|
||||
"Completed" = "مكتمل";
|
||||
"Drag to reorder" = "اسحب لإعادة الترتيب";
|
||||
"Drag to reorder sections" = "اسحب لإعادة ترتيب الأقسام";
|
||||
"Library View" = "عرض المكتبة";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "خصص الأقسام المعروضة في مكتبتك. يمكنك إعادة ترتيب الأقسام أو تعطيلها بالكامل.";
|
||||
"Library Sections Order" = "ترتيب أقسام المكتبة";
|
||||
"Completion Percentage" = "نسبة الإكمال";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "بعض الميزات محدودة في مشغل Sora والمشغل الافتراضي فقط، مثل الوضع الأفقي الإجباري، سرعة التثبيت، وزيادات تخطي الوقت المخصصة.\n\nإعداد نسبة الإكمال يحدد عند أي نقطة قبل نهاية الفيديو سيتم اعتبار العمل مكتمل في AniList وTrakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "ذاكرة التخزين المؤقت تساعد التطبيق على تحميل الصور بشكل أسرع.\n\nمسح مجلد المستندات سيحذف جميع الوحدات التي تم تنزيلها.\n\nمسح بيانات التطبيق سيحذف جميع إعداداتك وبياناتك.";
|
||||
"Translators" = "المترجمون";
|
||||
"Paste URL" = "الصق الرابط";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "عنوان السلسلة";
|
||||
"Content Source" = "مصدر المحتوى";
|
||||
"Watch Progress" = "تقدم المشاهدة";
|
||||
"Nothing to Continue Reading" = "لا شيء لمتابعة القراءة";
|
||||
"Your recently read novels will appear here" = "ستظهر الروايات التي قرأتها مؤخرًا هنا";
|
||||
"No Bookmarks" = "لا توجد إشارات مرجعية";
|
||||
"Add bookmarks to this collection" = "أضف إشارات مرجعية إلى هذه المجموعة";
|
||||
"items" = "عناصر";
|
||||
"All Watching" = "كل المشاهدة";
|
||||
"No Reading History" = "لا يوجد سجل قراءة";
|
||||
"Books you're reading will appear here" = "ستظهر الكتب التي تقرأها هنا";
|
||||
"Create Collection" = "إنشاء مجموعة";
|
||||
"Collection Name" = "اسم المجموعة";
|
||||
"Rename Collection" = "إعادة تسمية المجموعة";
|
||||
"Rename" = "إعادة تسمية";
|
||||
"Novel Title" = "عنوان الرواية";
|
||||
"Read Progress" = "تقدم القراءة";
|
||||
"Date Created" = "تاريخ الإنشاء";
|
||||
"Name" = "الاسم";
|
||||
"Item Count" = "عدد العناصر";
|
||||
"Date Added" = "تاريخ الإضافة";
|
||||
"Title" = "العنوان";
|
||||
"Source" = "المصدر";
|
||||
"Search reading..." = "ابحث في القراءة...";
|
||||
"Search collections..." = "ابحث في المجموعات...";
|
||||
"Search bookmarks..." = "ابحث في الإشارات المرجعية...";
|
||||
"%d items" = "%d عناصر";
|
||||
"Fetching Data" = "جاري جلب البيانات";
|
||||
"Please wait while fetching." = "يرجى الانتظار أثناء الجلب.";
|
||||
"Start Reading" = "ابدأ القراءة";
|
||||
"Chapters" = "الفصول";
|
||||
"Completed" = "مكتمل";
|
||||
"Drag to reorder" = "اسحب لإعادة الترتيب";
|
||||
"Drag to reorder sections" = "اسحب لإعادة ترتيب الأقسام";
|
||||
"Library View" = "عرض المكتبة";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "خصص الأقسام المعروضة في مكتبتك. يمكنك إعادة ترتيب الأقسام أو تعطيلها بالكامل.";
|
||||
"Library Sections Order" = "ترتيب أقسام المكتبة";
|
||||
"Completion Percentage" = "نسبة الإكمال";
|
||||
"Translators" = "المترجمون";
|
||||
"Paste URL" = "الصق الرابط";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "المجموعات";
|
||||
"Continue Reading" = "متابعة القراءة";
|
||||
|
||||
/* New additions */
|
||||
"Backup & Restore" = "النسخ الاحتياطي والاستعادة";
|
||||
"Export Backup" = "تصدير النسخة الاحتياطية";
|
||||
"Import Backup" = "استيراد النسخة الاحتياطية";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "تنبيه: هذه الميزة لا تزال تجريبية. يرجى التحقق من بياناتك بعد التصدير/الاستيراد.";
|
||||
"Backup" = "نسخة احتياطية";
|
||||
510
Sora/Localization/bos.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
/* General */
|
||||
"About" = "O aplikaciji";
|
||||
"About Sora" = "O Sora aplikaciji";
|
||||
"Active" = "Aktivno";
|
||||
"Active Downloads" = "Aktivna preuzimanja";
|
||||
"Actively downloading media can be tracked from here." = "Mediji koji se trenutno preuzimaju mogu se pratiti odavde.";
|
||||
"Add Module" = "Dodaj modul";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Podesi broj medijskih stavki po redu u portretnom i pejzažnom načinu.";
|
||||
"Advanced" = "Napredno";
|
||||
"AKA Sulfur" = "Također poznat kao Sulfur";
|
||||
"All Bookmarks" = "Sve zabilješke";
|
||||
"All Watching" = "Sve što gledam";
|
||||
"Also known as Sulfur" = "Također poznat kao Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ID";
|
||||
"AniList Match" = "AniList poklapanje";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonimni podaci se prikupljaju za poboljšanje aplikacije. Lične informacije se ne prikupljaju. Ovo se može onemogućiti u bilo kojem trenutku.";
|
||||
"App Info" = "Informacije o aplikaciji";
|
||||
"App Language" = "Jezik aplikacije";
|
||||
"App Storage" = "Spremište aplikacije";
|
||||
"Appearance" = "Izgled";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Jeste li sigurni da želite obrisati sve keširane podatke? Ovo će pomoći oslobađanju prostora za spremanje.";
|
||||
"Are you sure you want to delete '%@'?" = "Jeste li sigurni da želite obrisati '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Jeste li sigurni da želite obrisati sve %1$d epizode u '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Jeste li sigurni da želite obrisati sve preuzete sadržaje? Možete odabrati da obrišete samo biblioteku zadržavajući preuzete datoteke za buduću upotrebu.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Jeste li sigurni da želite obrisati sve podatke aplikacije? Ova radnja se ne može poništiti.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Pozadina omogućena";
|
||||
"Bookmark items for an easier access later." = "Zabilježite stavke za lakši pristup kasnije.";
|
||||
"Bookmarks" = "Zabilješke";
|
||||
"Bottom Padding" = "Donja margina";
|
||||
"Cancel" = "Otkaži";
|
||||
"Cellular Quality" = "Kvaliteta mobilne mreže";
|
||||
"Check out some community modules here!" = "Pogledajte neke module zajednice ovdje!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Odaberite željenu rezoluciju videa za WiFi i mobilne konekcije. Veće rezolucije koriste više podataka ali pružaju bolji kvalitet. Ako točna kvaliteta nije dostupna, najbliža opcija će biti automatski odabrana.\n\nNapomena: Nisu svi video izvori i playeri podržavaju odabir kvalitete. Ova funkcija najbolje radi s HLS streamovima koristeći Sora player.";
|
||||
"Clear" = "Obriši";
|
||||
"Clear All Downloads" = "Obriši sva preuzimanja";
|
||||
"Clear Cache" = "Obriši keš";
|
||||
"Clear Library Only" = "Obriši samo biblioteku";
|
||||
"Clear Logs" = "Obriši logove";
|
||||
"Click the plus button to add a module!" = "Kliknite plus dugme da dodate modul!";
|
||||
"Continue Watching" = "Nastavi gledanje";
|
||||
"Continue Watching Episode %d" = "Nastavi gledanje epizode %d";
|
||||
"Contributors" = "Saradnici";
|
||||
"Copied to Clipboard" = "Kopirano u međuspremnik";
|
||||
"Copy to Clipboard" = "Kopiraj u međuspremnik";
|
||||
"Copy URL" = "Kopiraj URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld epizoda";
|
||||
"%lld of %lld" = "%lld od %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% pogledano";
|
||||
"Episode %lld" = "Epizoda %lld";
|
||||
"Episodes" = "Epizode";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Epizode možda još nisu dostupne ili možda postoji problem s izvorom.";
|
||||
"Episodes Range" = "Raspon epizoda";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Tamno";
|
||||
"DATA & LOGS" = "PODACI I LOGOVI";
|
||||
"Debug" = "Otkrivanje grešaka";
|
||||
"Debugging and troubleshooting." = "Otkrivanje i rješavanje problema.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Obriši";
|
||||
"Delete All" = "Obriši sve";
|
||||
"Delete All Downloads" = "Obriši sva preuzimanja";
|
||||
"Delete All Episodes" = "Obriši sve epizode";
|
||||
"Delete Download" = "Obriši preuzimanje";
|
||||
"Delete Episode" = "Obriši epizodu";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Dvostruki dodir za traženje";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Dvostruki dodir ekrana sa strane će preskočiti s kratkim dodirom.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Preuzmi";
|
||||
"Download Episode" = "Preuzmi epizodu";
|
||||
"Download Summary" = "Sažetak preuzimanja";
|
||||
"Download This Episode" = "Preuzmi ovu epizodu";
|
||||
"Downloaded" = "Preuzeto";
|
||||
"Downloaded Shows" = "Preuzete serije";
|
||||
"Downloading" = "Preuzimanje";
|
||||
"Downloads" = "Preuzimanja";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Omogući analitiku";
|
||||
"Enable Subtitles" = "Omogući titlove";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Obriši";
|
||||
"Erase all App Data" = "Obriši sve podatke aplikacije";
|
||||
"Erase App Data" = "Obriši podatke aplikacije";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Greška";
|
||||
"Error Fetching Results" = "Greška pri dohvaćanju rezultata";
|
||||
"Errors and critical issues." = "Greške i kritični problemi.";
|
||||
"Failed to load contributors" = "Neuspješno učitavanje saradnika";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Dohvati metapodatke epizode";
|
||||
"Files Downloaded" = "Datoteke preuzete";
|
||||
"Font Size" = "Veličina fonta";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Forsiraj pejzažni način";
|
||||
"General" = "Općenito";
|
||||
"General events and activities." = "Općeniti događaji i aktivnosti.";
|
||||
"General Preferences" = "Općenite postavke";
|
||||
"Hide Splash Screen" = "Sakrij početni ekran";
|
||||
"HLS video downloading." = "HLS video preuzimanje.";
|
||||
"Hold Speed" = "Brzina držanja";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Informacije";
|
||||
"INFOS" = "INFORMACIJE";
|
||||
"Installed Modules" = "Instalirani moduli";
|
||||
"Interface" = "Sučelje";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Pridruži se Discordu";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Stupci u pejzažnom načinu";
|
||||
"Language" = "Jezik";
|
||||
"LESS" = "MANJE";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Biblioteka";
|
||||
"License (GPLv3.0)" = "Licenca (GPLv3.0)";
|
||||
"Light" = "Svijetlo";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Učitavam epizodu %lld...";
|
||||
"Loading logs..." = "Učitavam logove...";
|
||||
"Loading module information..." = "Učitavam informacije o modulu...";
|
||||
"Loading Stream" = "Učitavam stream";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Logiraj debug informacije";
|
||||
"Log Filters" = "Filtri logova";
|
||||
"Log In with AniList" = "Prijavite se s AniList";
|
||||
"Log In with Trakt" = "Prijavite se s Trakt";
|
||||
"Log Out from AniList" = "Odjavite se s AniList";
|
||||
"Log Out from Trakt" = "Odjavite se s Trakt";
|
||||
"Log Types" = "Tipovi logova";
|
||||
"Logged in as" = "Prijavljen kao";
|
||||
"Logged in as " = "Prijavljen kao ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logovi";
|
||||
"Long press Skip" = "Dugi pritisak za preskačanje";
|
||||
"MAIN" = "GLAVNO";
|
||||
"Main Developer" = "Glavni developer";
|
||||
"MAIN SETTINGS" = "GLAVNE POSTAVKE";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Označi sve prethodne kao pogledane";
|
||||
"Mark as Watched" = "Označi kao pogledano";
|
||||
"Mark Episode as Watched" = "Označi epizodu kao pogledanu";
|
||||
"Mark Previous Episodes as Watched" = "Označi prethodne epizode kao pogledane";
|
||||
"Mark watched" = "Označi pogledano";
|
||||
"Match with AniList" = "Poklopi s AniList";
|
||||
"Match with TMDB" = "Poklopi s TMDB";
|
||||
"Matched ID: %lld" = "Poklopljeni ID: %lld";
|
||||
"Matched with: %@" = "Poklopljeno s: %@";
|
||||
"Max Concurrent Downloads" = "Maksimalno istovremenih preuzimanja";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Mrežni raspored medija";
|
||||
"Media Player" = "Medijski player";
|
||||
"Media View" = "Pregled medija";
|
||||
"Metadata Provider" = "Pružatelj metapodataka";
|
||||
"Metadata Providers Order" = "Redoslijed pružatelja metapodataka";
|
||||
"Module Removed" = "Modul uklonjen";
|
||||
"Modules" = "Moduli";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULI";
|
||||
"MORE" = "VIŠE";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Nema aktivnih preuzimanja";
|
||||
"No AniList matches found" = "Nisu pronađena AniList poklapanja";
|
||||
"No Data Available" = "Nema dostupnih podataka";
|
||||
"No Downloads" = "Nema preuzimanja";
|
||||
"No episodes available" = "Nema dostupnih epizoda";
|
||||
"No Episodes Available" = "Nema dostupnih epizoda";
|
||||
"No items to continue watching." = "Nema stavki za nastavak gledanja.";
|
||||
"No matches found" = "Nisu pronađena poklapanja";
|
||||
"No Module Selected" = "Nije odabran modul";
|
||||
"No Modules" = "Nema modula";
|
||||
"No Results Found" = "Nisu pronađeni rezultati";
|
||||
"No Search Results Found" = "Nisu pronađeni rezultati pretrage";
|
||||
"Nothing to Continue Watching" = "Nema ništa za nastavak gledanja";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Imajte na umu da će moduli biti zamijenjeni samo ako postoji drugačiji string verzije u JSON datoteci.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "U redu";
|
||||
"Open Community Library" = "Otvori biblioteku zajednice";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Otvori u AniList";
|
||||
"Original Poster" = "Originalni poster";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pauzirano";
|
||||
"Play" = "Reproduciraj";
|
||||
"Player" = "Player";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Molimo restartajte aplikaciju da primijenite promjenu jezika.";
|
||||
"Please select a module from settings" = "Molimo odaberite modul iz postavki";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Stupci u portretnom načinu";
|
||||
"Progress bar Marker Color" = "Boja oznake progress bara";
|
||||
"Provider: %@" = "Pružatelj: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Red čekanja";
|
||||
"Queued" = "U redu čekanja";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Nedavno pogledani sadržaj će se pojaviti ovdje.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Osvježi module pri pokretanju";
|
||||
"Refresh Storage Info" = "Osvježi informacije o spremištu";
|
||||
"Remember Playback speed" = "Zapamti brzinu reprodukcije";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Ukloni";
|
||||
"Remove All Cache" = "Ukloni sav keš";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Ukloni sve dokumente";
|
||||
"Remove Documents" = "Ukloni dokumente";
|
||||
"Remove Downloaded Media" = "Ukloni preuzete medije";
|
||||
"Remove Downloads" = "Ukloni preuzimanja";
|
||||
"Remove from Bookmarks" = "Ukloni iz zabilježaka";
|
||||
"Remove Item" = "Ukloni stavku";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Prijavite problem";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Resetiraj";
|
||||
"Reset AniList ID" = "Resetiraj AniList ID";
|
||||
"Reset Episode Progress" = "Resetiraj napredak epizode";
|
||||
"Reset progress" = "Resetiraj napredak";
|
||||
"Reset Progress" = "Resetiraj napredak";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Potreban restart";
|
||||
"Running Sora %@ - cranci1" = "Pokrenut Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Spremi";
|
||||
"Search" = "Pretraži";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Pretraži preuzimanja";
|
||||
"Search for something..." = "Pretraži nešto...";
|
||||
"Search..." = "Pretraži...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Sezona %d";
|
||||
"Season %lld" = "Sezona %lld";
|
||||
"Segments Color" = "Boja segmenata";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Odaberi modul";
|
||||
"Set Custom AniList ID" = "Postavi prilagođeni AniList ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Postavke";
|
||||
"Shadow" = "Sjena";
|
||||
"Show More (%lld more characters)" = "Prikaži više (%lld više znakova)";
|
||||
"Show PiP Button" = "Prikaži PiP dugme";
|
||||
"Show Skip 85s Button" = "Prikaži dugme za preskakanje 85s";
|
||||
"Show Skip Intro / Outro Buttons" = "Prikaži dugmad za preskakanje intro / outro";
|
||||
"Shows" = "Serije";
|
||||
"Size (%@)" = "Veličina (%@)";
|
||||
"Skip Settings" = "Postavke preskakanja";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Neke funkcije su ograničene na Sora i zadani player, kao što su ForceLandscape, holdSpeed i prilagođeni vremenski intervali preskakanja.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ od cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "Sora i cranci1 nisu povezani s AniList ili Trakt na bilo koji način.
|
||||
|
||||
Također imajte na umu da ažuriranja napretka možda neće biti 100% točna.";
|
||||
"Sora GitHub Repository" = "Sora GitHub repozitorij";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur će uvijek ostati besplatan bez reklama!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Sortiraj";
|
||||
"Speed Settings" = "Postavke brzine";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Počni gledati";
|
||||
"Start Watching Episode %d" = "Počni gledati epizodu %d";
|
||||
"Storage Used" = "Iskorišten prostor za spremanje";
|
||||
"Stream" = "Stream";
|
||||
"Streaming and video playback." = "Streaming i reprodukcija videa.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Boja titlova";
|
||||
"Subtitle Settings" = "Postavke titlova";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Sinhroniziraj napredak animea";
|
||||
"Sync TV shows progress" = "Sinhroniziraj napredak TV serija";
|
||||
|
||||
/* System */
|
||||
"System" = "Sistem";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Dodirnite naslov da pregazite trenutno poklapanje.";
|
||||
"Tap Skip" = "Dodirnite za preskakanje";
|
||||
"Tap to manage your modules" = "Dodirnite za upravljanje modulima";
|
||||
"Tap to select a module" = "Dodirnite da odaberete modul";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.
|
||||
|
||||
Clearing the Documents folder will delete all downloaded modules.
|
||||
|
||||
Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Keš aplikacije pomaže aplikaciji da učita slike brže.
|
||||
|
||||
Brisanje mape Dokumenti će obrisati sve preuzete module.
|
||||
|
||||
Ne brišite podatke aplikacije osim ako ne razumijete posljedice — može dovesti do kvarova aplikacije.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.
|
||||
|
||||
For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Raspon epizoda kontroliše koliko epizoda se pojavljuje na svakoj stranici. Epizode su grupisane u setove (kao 1–25, 26–50, i tako dalje), omogućavajući vam lakše navigiranje kroz njih.
|
||||
|
||||
Za metapodatke epizode, odnosi se na sličicu i naslov epizode, jer ponekad mogu sadržavati spojlere.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modul je pružio samo jednu epizodu, ovo je najvjerojatniji film, pa smo odlučili napraviti zasebne ekrane za ove slučajeve.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Širina sličica";
|
||||
"TMDB Match" = "TMDB poklapanje";
|
||||
"Trackers" = "Pratitelji";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Pokušajte s drugačijim ključnim riječima";
|
||||
"Try different search terms" = "Pokušajte s drugačijim pojmovima za pretragu";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Držanje dva prsta za pauzu";
|
||||
"Unable to fetch matches. Please try again later." = "Nije moguće dohvatiti poklapanja. Molimo pokušajte ponovo kasnije.";
|
||||
"Use TMDB Poster Image" = "Koristi TMDB poster sliku";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Video player";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Postavke kvalitete videa";
|
||||
"View All" = "Prikaži sve";
|
||||
"Watched" = "Pogledano";
|
||||
"Why am I not seeing any episodes?" = "Zašto ne vidim epizode?";
|
||||
"WiFi Quality" = "WiFi kvaliteta";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Niste prijavljeni";
|
||||
"You have no items saved." = "Nemate spremljenih stavki.";
|
||||
"Your downloaded episodes will appear here" = "Vaše preuzete epizode će se pojaviti ovdje";
|
||||
"Your recently watched content will appear here" = "Vaš nedavno pogledani sadržaj će se pojaviti ovdje";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Postavke preuzimanja";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Maksimalno istovremenih preuzimanja kontroliše koliko epizoda se može preuzeti istovremeno. Veće vrijednosti mogu koristiti više propusnog opsega i resursa uređaja.";
|
||||
"Quality" = "Kvaliteta";
|
||||
"Max Concurrent Downloads" = "Maksimalno istovremenih preuzimanja";
|
||||
"Allow Cellular Downloads" = "Dozvoli preuzimanja preko mobilne mreže";
|
||||
"Quality Information" = "Informacije o kvaliteti";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Upravljanje spremištem";
|
||||
"Storage Used" = "Iskorišten prostor";
|
||||
"Library cleared successfully" = "Biblioteka uspješno obrisana";
|
||||
"All downloads deleted successfully" = "Sva preuzimanja uspješno obrisana";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Nedavne pretrage";
|
||||
"me frfr" = "ja stvarno";
|
||||
"Data" = "Podaci";
|
||||
"Maximum Quality Available" = "Maksimalna dostupna kvaliteta";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d od %d";
|
||||
"Error loading chapter" = "Greška pri učitavanju poglavlja";
|
||||
"Font Size: %dpt" = "Veličina fonta: %dpt";
|
||||
"Line Spacing: %.1f" = "Razmak između redova: %.1f";
|
||||
"Line Spacing" = "Razmak između redova";
|
||||
"Margin: %dpx" = "Margina: %dpx";
|
||||
"Margin" = "Margina";
|
||||
"Auto Scroll Speed" = "Brzina automatskog pomicanja";
|
||||
"Speed" = "Brzina";
|
||||
"Speed: %.1fx" = "Brzina: %.1fx";
|
||||
"Matched %@: %@" = "Poklapanje %@: %@";
|
||||
"Enter the AniList ID for this series" = "Unesite AniList ID za ovu seriju";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Kreiraj kolekciju";
|
||||
"Collection Name" = "Naziv kolekcije";
|
||||
"Rename Collection" = "Preimenuj kolekciju";
|
||||
"Rename" = "Preimenuj";
|
||||
"All Reading" = "Sva čitanja";
|
||||
"Recently Added" = "Nedavno dodano";
|
||||
"Novel Title" = "Naslov romana";
|
||||
"Read Progress" = "Napredak čitanja";
|
||||
"Date Created" = "Datum kreiranja";
|
||||
"Name" = "Naziv";
|
||||
"Item Count" = "Broj stavki";
|
||||
"Date Added" = "Datum dodavanja";
|
||||
"Title" = "Naslov";
|
||||
"Source" = "Izvor";
|
||||
"Search reading..." = "Pretraži čitanje...";
|
||||
"Search collections..." = "Pretraži kolekcije...";
|
||||
"Search bookmarks..." = "Pretraži oznake...";
|
||||
"%d items" = "%d stavki";
|
||||
"Fetching Data" = "Preuzimanje podataka";
|
||||
"Please wait while fetching." = "Molimo sačekajte dok se preuzima.";
|
||||
"Start Reading" = "Započni čitanje";
|
||||
"Chapters" = "Poglavlja";
|
||||
"Completed" = "Završeno";
|
||||
"Drag to reorder" = "Povuci za promjenu redoslijeda";
|
||||
"Drag to reorder sections" = "Povuci za promjenu redoslijeda sekcija";
|
||||
"Library View" = "Prikaz biblioteke";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prilagodite sekcije prikazane u vašoj biblioteci. Možete ih preurediti ili potpuno onemogućiti.";
|
||||
"Library Sections Order" = "Redoslijed sekcija biblioteke";
|
||||
"Completion Percentage" = "Procenat završetka";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Neke funkcije su ograničene na Sora i zadani player, kao što su prisilni pejzaž, držanje brzine i prilagođeni intervali preskakanja.\n\nPostavka procenta završetka određuje u kojoj tački prije kraja videa će aplikacija označiti kao završeno na AniList i Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Keš aplikacije pomaže bržem učitavanju slika.\n\nBrisanje Documents foldera će ukloniti sve preuzete module.\n\nBrisanje podataka aplikacije briše sve vaše postavke i podatke.";
|
||||
"Translators" = "Prevoditelji";
|
||||
"Paste URL" = "Zalijepi URL";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "Naslov serije";
|
||||
"Content Source" = "Izvor sadržaja";
|
||||
"Watch Progress" = "Napredak gledanja";
|
||||
"Recent searches" = "Nedavne pretrage";
|
||||
"All Reading" = "Sve što čitam";
|
||||
"Nothing to Continue Reading" = "Nema ništa za nastaviti čitanje";
|
||||
"Your recently read novels will appear here" = "Vaši nedavno pročitani romani će se pojaviti ovdje";
|
||||
"No Bookmarks" = "Nema zabilješki";
|
||||
"Add bookmarks to this collection" = "Dodajte zabilješke u ovu kolekciju";
|
||||
"items" = "stavke";
|
||||
"All Watching" = "Sve što gledam";
|
||||
"No Reading History" = "Nema historije čitanja";
|
||||
"Books you're reading will appear here" = "Knjige koje čitate će se pojaviti ovdje";
|
||||
"Create Collection" = "Kreiraj kolekciju";
|
||||
"Collection Name" = "Naziv kolekcije";
|
||||
"Rename Collection" = "Preimenuj kolekciju";
|
||||
"Rename" = "Preimenuj";
|
||||
"Novel Title" = "Naslov romana";
|
||||
"Read Progress" = "Napredak čitanja";
|
||||
"Date Created" = "Datum kreiranja";
|
||||
"Name" = "Ime";
|
||||
"Item Count" = "Broj stavki";
|
||||
"Date Added" = "Datum dodavanja";
|
||||
"Title" = "Naslov";
|
||||
"Source" = "Izvor";
|
||||
"Search reading..." = "Pretraži čitanje...";
|
||||
"Search collections..." = "Pretraži kolekcije...";
|
||||
"Search bookmarks..." = "Pretraži zabilješke...";
|
||||
"%d items" = "%d stavki";
|
||||
"Fetching Data" = "Dohvatanje podataka";
|
||||
"Please wait while fetching." = "Molimo sačekajte dok se podaci dohvaćaju.";
|
||||
"Start Reading" = "Započni čitanje";
|
||||
"Chapters" = "Poglavlja";
|
||||
"Completed" = "Završeno";
|
||||
"Drag to reorder" = "Povucite za promjenu redoslijeda";
|
||||
"Drag to reorder sections" = "Povucite za promjenu redoslijeda sekcija";
|
||||
"Library View" = "Prikaz biblioteke";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prilagodite sekcije prikazane u vašoj biblioteci. Možete promijeniti redoslijed ili ih potpuno onemogućiti.";
|
||||
"Library Sections Order" = "Redoslijed sekcija biblioteke";
|
||||
"Completion Percentage" = "Procenat završetka";
|
||||
"Translators" = "Prevodioci";
|
||||
"Paste URL" = "Zalijepi URL";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "Kolekcije";
|
||||
"Continue Reading" = "Nastavi čitanje";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Sigurnosna kopija i vraćanje";
|
||||
"Export Backup" = "Izvezi sigurnosnu kopiju";
|
||||
"Import Backup" = "Uvezi sigurnosnu kopiju";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Napomena: Ova funkcija je još uvijek eksperimentalna. Molimo provjerite svoje podatke nakon izvoza/uvoza.";
|
||||
"Backup" = "Sigurnosna kopija";
|
||||
511
Sora/Localization/cs.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
/* General */
|
||||
"About" = "O aplikaci";
|
||||
"About Sora" = "O Sora";
|
||||
"Active" = "Aktivní";
|
||||
"Active Downloads" = "Aktivní stahování";
|
||||
"Actively downloading media can be tracked from here." = "Aktivně stahovaná média lze sledovat zde.";
|
||||
"Add Module" = "Přidat modul";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Upravte počet položek médií na řádek v režimu na výšku a na šířku.";
|
||||
"Advanced" = "Pokročilé";
|
||||
"AKA Sulfur" = "Známý jako Sulfur";
|
||||
"All Bookmarks" = "Všechny záložky";
|
||||
"All Watching" = "Vše sledované";
|
||||
"Also known as Sulfur" = "Také známý jako Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ID";
|
||||
"AniList Match" = "Shoda AniList";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonymní data jsou shromažďována za účelem vylepšení aplikace. Žádné osobní údaje nejsou shromažďovány. Toto lze kdykoli vypnout.";
|
||||
"App Info" = "Informace o aplikaci";
|
||||
"App Language" = "Jazyk aplikace";
|
||||
"App Storage" = "Úložiště aplikace";
|
||||
"Appearance" = "Vzhled";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Opravdu chcete vymazat všechna data z cache? Toto pomůže uvolnit úložný prostor.";
|
||||
"Are you sure you want to delete '%@'?" = "Opravdu chcete smazat '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Opravdu chcete smazat všech %1$d epizod v '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Opravdu chcete smazat všechny stažené soubory? Můžete zvolit vymazání pouze knihovny při zachování stažených souborů pro budoucí použití.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Opravdu chcete vymazat všechna data aplikace? Tuto akci nelze vrátit zpět.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Pozadí povoleno";
|
||||
"Bookmark items for an easier access later." = "Uložte položky do záložek pro snadnější přístup později.";
|
||||
"Bookmarks" = "Záložky";
|
||||
"Bottom Padding" = "Spodní odsazení";
|
||||
"Cancel" = "Zrušit";
|
||||
"Cellular Quality" = "Kvalita na mobilních datech";
|
||||
"Check out some community modules here!" = "Podívejte se na některé komunitní moduly zde!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Vyberte preferované rozlišení videa pro WiFi a mobilní připojení. Vyšší rozlišení používají více dat, ale poskytují lepší kvalitu. Pokud není k dispozici přesná kvalita, bude automaticky vybrána nejbližší možnost.\n\nPoznámka: Ne všechny video zdroje a přehrávače podporují výběr kvality. Tato funkce funguje nejlépe s HLS streamy pomocí přehrávače Sora.";
|
||||
"Clear" = "Vymazat";
|
||||
"Clear All Downloads" = "Vymazat všechna stahování";
|
||||
"Clear Cache" = "Vymazat cache";
|
||||
"Clear Library Only" = "Vymazat pouze knihovnu";
|
||||
"Clear Logs" = "Vymazat logy";
|
||||
"Click the plus button to add a module!" = "Klikněte na tlačítko plus pro přidání modulu!";
|
||||
"Continue Watching" = "Pokračovat ve sledování";
|
||||
"Continue Watching Episode %d" = "Pokračovat ve sledování epizody %d";
|
||||
"Contributors" = "Přispěvatelé";
|
||||
"Copied to Clipboard" = "Zkopírováno do schránky";
|
||||
"Copy to Clipboard" = "Kopírovat do schránky";
|
||||
"Copy URL" = "Kopírovat URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld epizod";
|
||||
"%lld of %lld" = "%lld z %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% shlédnuto";
|
||||
"Episode %lld" = "Epizoda %lld";
|
||||
"Episodes" = "Epizody";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Epizody možná ještě nejsou dostupné nebo může být problém se zdrojem.";
|
||||
"Episodes Range" = "Rozsah epizod";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Tmavý";
|
||||
"DATA & LOGS" = "DATA & LOGY";
|
||||
"Debug" = "Ladění";
|
||||
"Debugging and troubleshooting." = "Ladění a řešení problémů.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Smazat";
|
||||
"Delete All" = "Smazat vše";
|
||||
"Delete All Downloads" = "Smazat všechna stahování";
|
||||
"Delete All Episodes" = "Smazat všechny epizody";
|
||||
"Delete Download" = "Smazat stahování";
|
||||
"Delete Episode" = "Smazat epizodu";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Dvojitý dotyk pro posun";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Dvojitý dotyk na strany obrazovky přeskočí s nastavením krátkého dotyku.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Stáhnout";
|
||||
"Download Episode" = "Stáhnout epizodu";
|
||||
"Download Summary" = "Přehled stahování";
|
||||
"Download This Episode" = "Stáhnout tuto epizodu";
|
||||
"Downloaded" = "Staženo";
|
||||
"Downloaded Shows" = "Stažené seriály";
|
||||
"Downloading" = "Stahování";
|
||||
"Downloads" = "Stahování";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Povolit analytiku";
|
||||
"Enable Subtitles" = "Povolit titulky";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Vymazat";
|
||||
"Erase all App Data" = "Vymazat všechna data aplikace";
|
||||
"Erase App Data" = "Vymazat data aplikace";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Chyba";
|
||||
"Error Fetching Results" = "Chyba při načítání výsledků";
|
||||
"Errors and critical issues." = "Chyby a kritické problémy.";
|
||||
"Failed to load contributors" = "Nepodařilo se načíst přispěvatele";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Načíst metadata epizody";
|
||||
"Files Downloaded" = "Stažené soubory";
|
||||
"Font Size" = "Velikost písma";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Vynutit na šířku";
|
||||
"General" = "Obecné";
|
||||
"General events and activities." = "Obecné události a aktivity.";
|
||||
"General Preferences" = "Obecné předvolby";
|
||||
"Hide Splash Screen" = "Skrýt úvodní obrazovku (Splash Screen)";
|
||||
"HLS video downloading." = "Stahování HLS videa.";
|
||||
"Hold Speed" = "Rychlost při podržení";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Informace";
|
||||
"INFOS" = "INFORMACE";
|
||||
"Installed Modules" = "Nainstalované moduly";
|
||||
"Interface" = "Rozhraní";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Připojit se na Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Sloupce na šířku";
|
||||
"Language" = "Jazyk";
|
||||
"LESS" = "MÉNĚ";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Knihovna";
|
||||
"License (GPLv3.0)" = "Licence (GPLv3.0)";
|
||||
"Light" = "Světlý";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Načítá se epizoda %lld...";
|
||||
"Loading logs..." = "Načítají se logy...";
|
||||
"Loading module information..." = "Načítají se informace o modulu...";
|
||||
"Loading Stream" = "Načítá se stream";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Logovat ladicí informace";
|
||||
"Log Filters" = "Logovat filtry";
|
||||
"Log In with AniList" = "Přihlásit se pomocí AniList";
|
||||
"Log In with Trakt" = "Přihlásit se pomocí Trakt";
|
||||
"Log Out from AniList" = "Odhlásit se z AniList";
|
||||
"Log Out from Trakt" = "Odhlásit se z Trakt";
|
||||
"Log Types" = "Logovat typy";
|
||||
"Logged in as" = "Přihlášen jako";
|
||||
"Logged in as " = "Přihlášen jako ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logy";
|
||||
"Long press Skip" = "Dlouhý stisk pro přeskočení";
|
||||
"MAIN" = "HLAVNÍ";
|
||||
"Main Developer" = "Hlavní vývojář";
|
||||
"MAIN SETTINGS" = "HLAVNÍ NASTAVENÍ";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Označit všechny předchozí jako shlédnuté";
|
||||
"Mark as Watched" = "Označit jako shlédnuté";
|
||||
"Mark Episode as Watched" = "Označit epizodu jako shlédnutou";
|
||||
"Mark Previous Episodes as Watched" = "Označit předchozí epizody jako shlédnuté";
|
||||
"Mark watched" = "Označit jako shlédnuté";
|
||||
"Match with AniList" = "Párovat s AniList";
|
||||
"Match with TMDB" = "Párovat s TMDB";
|
||||
"Matched ID: %lld" = "Spárované ID: %lld";
|
||||
"Matched with: %@" = "Spárováno s: %@";
|
||||
"Max Concurrent Downloads" = "Max současných stahování";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Rozložení mřížky médií";
|
||||
"Media Player" = "Přehrávač médií";
|
||||
"Media View" = "Zobrazení médií";
|
||||
"Metadata Provider" = "Poskytovatel metadat";
|
||||
"Metadata Providers Order" = "Pořadí poskytovatelů metadat";
|
||||
"Module Removed" = "Modul odstraněn";
|
||||
"Modules" = "Moduly";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULY";
|
||||
"MORE" = "VÍCE";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Žádná aktivní stahování";
|
||||
"No AniList matches found" = "Nenalezeny žádné shody z AniListu";
|
||||
"No Data Available" = "Žádná data k dispozici";
|
||||
"No Downloads" = "Žádná stahování";
|
||||
"No episodes available" = "Žádné epizody k dispozici";
|
||||
"No Episodes Available" = "Žádné epizody k dispozici";
|
||||
"No items to continue watching." = "Žádné položky k pokračování ve sledování.";
|
||||
"No matches found" = "Nenalezeny žádné shody";
|
||||
"No Module Selected" = "Žádný modul nevybrán";
|
||||
"No Modules" = "Žádné moduly";
|
||||
"No Results Found" = "Nenalezeny žádné výsledky";
|
||||
"No Search Results Found" = "Nenalezeny žádné výsledky vyhledávání";
|
||||
"Nothing to Continue Watching" = "Nic k pokračování ve sledování";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Poznámka: moduly budou nahrazeny pouze pokud je v JSON souboru jiný řetězec verze.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Otevřít komunitní knihovnu";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Otevřít v AniList";
|
||||
"Original Poster" = "Původní plakát";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pozastaveno";
|
||||
"Play" = "Přehrát";
|
||||
"Player" = "Přehrávač";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Prosím restartujte aplikaci pro použití změny jazyka.";
|
||||
"Please select a module from settings" = "Prosím vyberte modul v nastavení";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Sloupce na výšku";
|
||||
"Progress bar Marker Color" = "Barva značky v progres baru";
|
||||
"Provider: %@" = "Poskytovatel: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Fronta";
|
||||
"Queued" = "Ve frontě";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Nedávno sledovaný obsah se zobrazí zde.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Obnovit moduly při spuštění aplikace";
|
||||
"Refresh Storage Info" = "Obnovit informace o úložišti";
|
||||
"Remember Playback speed" = "Zapamatovat rychlost přehrávání";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Odebrat";
|
||||
"Remove All Cache" = "Odebrat veškerou cache paměť";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Odebrat všechny dokumenty";
|
||||
"Remove Documents" = "Odebrat dokumenty";
|
||||
"Remove Downloaded Media" = "Odebrat stažená média";
|
||||
"Remove Downloads" = "Odebrat stahování";
|
||||
"Remove from Bookmarks" = "Odebrat ze záložek";
|
||||
"Remove Item" = "Odebrat položku";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Hlásit problém";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Resetovat";
|
||||
"Reset AniList ID" = "Resetovat AniList ID";
|
||||
"Reset Episode Progress" = "Resetovat progres epizody";
|
||||
"Reset progress" = "Resetovat progres";
|
||||
"Reset Progress" = "Resetovat progres";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Vyžadován restart";
|
||||
"Running Sora %@ - cranci1" = "Spuštěna Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Uložit";
|
||||
"Search" = "Hledat";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Hledat ve stahováních";
|
||||
"Search for something..." = "Hledat něco...";
|
||||
"Search..." = "Hledat...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Sezóna %d";
|
||||
"Season %lld" = "Sezóna %lld";
|
||||
"Segments Color" = "Barva segmentů";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Vybrat modul";
|
||||
"Set Custom AniList ID" = "Nastavit vlastní AniList ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Nastavení";
|
||||
"Shadow" = "Stín";
|
||||
"Show More (%lld more characters)" = "Zobrazit více (%lld dalších znaků)";
|
||||
"Show PiP Button" = "Zobrazit PiP tlačítko";
|
||||
"Show Skip 85s Button" = "Zobrazit tlačítko přeskočení 85s";
|
||||
"Show Skip Intro / Outro Buttons" = "Zobrazit tlačítka přeskočení intro/outro";
|
||||
"Shows" = "Seriály";
|
||||
"Size (%@)" = "Velikost (%@)";
|
||||
"Skip Settings" = "Nastavení přeskakování";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Některé funkce jsou omezeny na přehrávač Sora a výchozí, jako je vynucení orientace na šířku (ForceLandscape), rychlost při podržení (holdSpeed) a vlastní časové skoky.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ od cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "Sora a cranci1 nejsou nijak spojeni s AniList nebo Trakt.
|
||||
|
||||
Také si všimněte, že aktualizace progresu nemusí být 100% přesné.";
|
||||
"Sora GitHub Repository" = "Sora GitHub repozitář";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur vždy zůstane zdarma bez reklam!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Seřadit";
|
||||
"Speed Settings" = "Nastavení rychlosti";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Začít sledovat";
|
||||
"Start Watching Episode %d" = "Začít sledovat epizodu %d";
|
||||
"Storage Used" = "Využité úložiště";
|
||||
"Stream" = "Stream";
|
||||
"Streaming and video playback." = "Streamování a přehrávání videa.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Barva titulků";
|
||||
"Subtitle Settings" = "Nastavení titulků";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Synchronizovat progres anime";
|
||||
"Sync TV shows progress" = "Synchronizovat progres TV seriálů";
|
||||
|
||||
/* System */
|
||||
"System" = "Systém";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Dotkněte se názvu pro přepsání aktuální shody.";
|
||||
"Tap Skip" = "Klepnutím přeskočit";
|
||||
"Tap to manage your modules" = "Dotkněte se pro správu vašich modulů";
|
||||
"Tap to select a module" = "Dotkněte se pro výběr modulu";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.
|
||||
|
||||
Clearing the Documents folder will delete all downloaded modules.
|
||||
|
||||
Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Cache aplikace pomáhá aplikaci načítat obrázky rychleji.
|
||||
|
||||
Vymazání složky Dokumenty smaže všechny stažené moduly.
|
||||
|
||||
Nemažte Data aplikace, pokud nerozumíte důsledkům — může to způsobit selhání aplikace.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.
|
||||
|
||||
For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Rozsah epizod určuje, kolik epizod se zobrazí na každé stránce. Epizody jsou seskupeny do bloků (jako 1–25, 26–50, atd.), což vám umožňuje procházet je snadněji.
|
||||
|
||||
Metadata epizody se týkají náhledu a názvu epizody, které mohou někdy obsahovat spoilery.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modul poskytl pouze jednu epizodu, toto je pravděpodobně film, takže jsme se rozhodli vytvořit samostatné obrazovky pro tyto případy.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Šířka náhledů";
|
||||
"TMDB Match" = "TMDB shoda";
|
||||
"Trackers" = "Trackery";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Zkuste jiná klíčová slova";
|
||||
"Try different search terms" = "Zkuste jiné vyhledávací termíny";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Držení dvěma prsty pro pozastavení";
|
||||
"Unable to fetch matches. Please try again later." = "Nelze načíst shody. Prosím zkuste to později.";
|
||||
"Use TMDB Poster Image" = "Použít plakát z TMDB";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Přehrávač videa";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Předvolby kvality videa";
|
||||
"View All" = "Zobrazit vše";
|
||||
"Watched" = "Shlédnuto";
|
||||
"Why am I not seeing any episodes?" = "Proč nevidím žádné epizody?";
|
||||
"WiFi Quality" = "WiFi kvalita";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Nejste přihlášeni";
|
||||
"You have no items saved." = "Nemáte uložené žádné položky.";
|
||||
"Your downloaded episodes will appear here" = "Vaše stažené epizody se zobrazí zde";
|
||||
"Your recently watched content will appear here" = "Váš nedávno sledovaný obsah se zobrazí zde";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Nastavení stahování";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Max současných stahování určuje, kolik epizod se může stahovat současně. Vyšší hodnoty mohou používat více šířky pásma a zdrojů zařízení.";
|
||||
"Quality" = "Kvalita";
|
||||
"Max Concurrent Downloads" = "Max současných stahování";
|
||||
"Allow Cellular Downloads" = "Povolit stahování přes mobilní síť";
|
||||
"Quality Information" = "Informace o kvalitě";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Správa úložiště";
|
||||
"Storage Used" = "Využité úložiště";
|
||||
"Library cleared successfully" = "Knihovna úspěšně vymazána";
|
||||
"All downloads deleted successfully" = "Všechna stahování úspěšně smazána";
|
||||
|
||||
/* Recent searches */
|
||||
"Recent searches" = "Nedávná hledání";
|
||||
"me frfr" = "já frfr";
|
||||
"Data" = "Data";
|
||||
|
||||
/* New string */
|
||||
"Maximum Quality Available" = "Maximální dostupná kvalita";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d z %d";
|
||||
"Error loading chapter" = "Chyba při načítání kapitoly";
|
||||
"Font Size: %dpt" = "Velikost písma: %dpt";
|
||||
"Line Spacing: %.1f" = "Řádkování: %.1f";
|
||||
"Line Spacing" = "Řádkování";
|
||||
"Margin: %dpx" = "Okraj: %dpx";
|
||||
"Margin" = "Okraj";
|
||||
"Auto Scroll Speed" = "Rychlost automatického posunu";
|
||||
"Speed" = "Rychlost";
|
||||
"Speed: %.1fx" = "Rychlost: %.1fx";
|
||||
"Matched %@: %@" = "Shoda %@: %@";
|
||||
"Enter the AniList ID for this series" = "Zadejte AniList ID pro tuto sérii";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Vytvořit kolekci";
|
||||
"Collection Name" = "Název kolekce";
|
||||
"Rename Collection" = "Přejmenovat kolekci";
|
||||
"Rename" = "Přejmenovat";
|
||||
"All Reading" = "Všechny knihy";
|
||||
"Recently Added" = "Nedávno přidáno";
|
||||
"Novel Title" = "Název románu";
|
||||
"Read Progress" = "Postup čtení";
|
||||
"Date Created" = "Datum vytvoření";
|
||||
"Name" = "Název";
|
||||
"Item Count" = "Počet položek";
|
||||
"Date Added" = "Datum přidání";
|
||||
"Title" = "Titul";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hledat v knihách...";
|
||||
"Search collections..." = "Hledat v kolekcích...";
|
||||
"Search bookmarks..." = "Hledat v záložkách...";
|
||||
"%d items" = "%d položek";
|
||||
"Fetching Data" = "Načítání dat";
|
||||
"Please wait while fetching." = "Počkejte prosím během načítání.";
|
||||
"Start Reading" = "Začít číst";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončeno";
|
||||
"Drag to reorder" = "Přetáhněte pro změnu pořadí";
|
||||
"Drag to reorder sections" = "Přetáhněte pro změnu pořadí sekcí";
|
||||
"Library View" = "Zobrazení knihovny";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Přizpůsobte sekce zobrazené ve vaší knihovně. Můžete je přeuspořádat nebo zcela vypnout.";
|
||||
"Library Sections Order" = "Pořadí sekcí knihovny";
|
||||
"Completion Percentage" = "Procento dokončení";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Některé funkce jsou omezeny na Sora a výchozí přehrávač, například vynucená krajina, podržení rychlosti a vlastní intervaly přeskočení.\n\nNastavení procenta dokončení určuje, v jakém bodě před koncem videa bude aplikace označovat jako dokončené na AniList a Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Mezipaměť aplikace pomáhá rychlejšímu načítání obrázků.\n\nVymazání složky Documents odstraní všechny stažené moduly.\n\nVymazání dat aplikace smaže všechna vaše nastavení a data.";
|
||||
"Translators" = "Překladatelé";
|
||||
"Paste URL" = "Vložit URL";
|
||||
|
||||
/* New localizations */
|
||||
"Series Title" = "Název série";
|
||||
"Content Source" = "Zdroj obsahu";
|
||||
"Watch Progress" = "Průběh sledování";
|
||||
"All Reading" = "Vše ke čtení";
|
||||
"Nothing to Continue Reading" = "Nic k pokračování ve čtení";
|
||||
"Your recently read novels will appear here" = "Vaše nedávno čtené romány se zobrazí zde";
|
||||
"No Bookmarks" = "Žádné záložky";
|
||||
"Add bookmarks to this collection" = "Přidejte záložky do této kolekce";
|
||||
"items" = "položky";
|
||||
"All Watching" = "Vše ke sledování";
|
||||
"No Reading History" = "Žádná historie čtení";
|
||||
"Books you're reading will appear here" = "Knihy, které čtete, se zobrazí zde";
|
||||
"Create Collection" = "Vytvořit kolekci";
|
||||
"Collection Name" = "Název kolekce";
|
||||
"Rename Collection" = "Přejmenovat kolekci";
|
||||
"Rename" = "Přejmenovat";
|
||||
"Novel Title" = "Název románu";
|
||||
"Read Progress" = "Průběh čtení";
|
||||
"Date Created" = "Datum vytvoření";
|
||||
"Name" = "Jméno";
|
||||
"Item Count" = "Počet položek";
|
||||
"Date Added" = "Datum přidání";
|
||||
"Title" = "Název";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hledat ve čtení...";
|
||||
"Search collections..." = "Hledat v kolekcích...";
|
||||
"Search bookmarks..." = "Hledat v záložkách...";
|
||||
"%d items" = "%d položek";
|
||||
"Fetching Data" = "Načítání dat";
|
||||
"Please wait while fetching." = "Počkejte prosím, načítají se data.";
|
||||
"Start Reading" = "Začít číst";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončeno";
|
||||
"Drag to reorder" = "Přetáhněte pro změnu pořadí";
|
||||
"Drag to reorder sections" = "Přetáhněte pro změnu pořadí sekcí";
|
||||
"Library View" = "Zobrazení knihovny";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Přizpůsobte sekce zobrazené ve vaší knihovně. Můžete změnit jejich pořadí nebo je úplně vypnout.";
|
||||
"Library Sections Order" = "Pořadí sekcí knihovny";
|
||||
"Completion Percentage" = "Procento dokončení";
|
||||
"Translators" = "Překladatelé";
|
||||
"Paste URL" = "Vložit URL";
|
||||
|
||||
/* New localizations */
|
||||
"Collections" = "Kolekce";
|
||||
"Continue Reading" = "Pokračovat ve čtení";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Zálohování a obnovení";
|
||||
"Export Backup" = "Exportovat zálohu";
|
||||
"Import Backup" = "Importovat zálohu";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Upozornění: Tato funkce je stále experimentální. Po exportu/importu si prosím zkontrolujte svá data.";
|
||||
"Backup" = "Záloha";
|
||||
499
Sora/Localization/de.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
/* General */
|
||||
"About" = "Über";
|
||||
"About Sora" = "Über Sora";
|
||||
"Active" = "Aktiv";
|
||||
"Active Downloads" = "Aktive Downloads";
|
||||
"Actively downloading media can be tracked from here." = "Hier kannst du deine laufenden Downloads verfolgen.";
|
||||
"Add Module" = "Modul hinzufügen";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Passe die Anzahl der Medienelemente pro Zeile im Hoch- und Querformat an.";
|
||||
"Advanced" = "Erweitert";
|
||||
"AKA Sulfur" = "Alias Sulfur";
|
||||
"All Bookmarks" = "Alle Lesezeichen";
|
||||
"All Watching" = "Alle ansehen";
|
||||
"Also known as Sulfur" = "Auch bekannt als Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList-ID";
|
||||
"AniList Match" = "AniList-Zuordnung";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonyme Daten werden gesammelt, um die App zu verbessern. Es werden keine persönlichen Informationen erfasst. Du kannst dies jederzeit deaktivieren.";
|
||||
"App Info" = "App-Infos";
|
||||
"App Language" = "App-Sprache";
|
||||
"App Storage" = "App-Speicher";
|
||||
"Appearance" = "Darstellung";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Bist du sicher, dass du den Cache leeren möchtest? Dies hilft, Speicherplatz freizugeben.";
|
||||
"Are you sure you want to delete '%@'?" = "Bist du sicher, dass du '%@' löschen möchtest?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Bist du sicher, dass du alle %1$d Folgen in '%2$@' löschen möchtest?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Bist du sicher, dass du alle Downloads löschen möchtest? Du kannst auch nur die Bibliothek leeren und die heruntergeladenen Dateien für später behalten.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Bist du sicher, dass du alle App-Daten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Hintergrund aktiviert";
|
||||
"Bookmark items for an easier access later." = "Setze Lesezeichen für späteren schnellen Zugriff.";
|
||||
"Bookmarks" = "Lesezeichen";
|
||||
"Bottom Padding" = "Abstand unten";
|
||||
"Cancel" = "Abbrechen";
|
||||
"Cellular Quality" = "Qualität (Mobilfunk)";
|
||||
"Check out some community modules here!" = "Entdecke Community-Module hier!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Wähle deine bevorzugte Videoqualität für WLAN und Mobilfunk. Höhere Qualität verbraucht mehr Daten. Falls nicht verfügbar, wird automatisch die nächstbeste Option gewählt.\n\nHinweis: Nicht alle Quellen unterstützen Qualitätsauswahl. Funktioniert am besten mit HLS-Streams im Sora-Player.";
|
||||
"Clear" = "Löschen";
|
||||
"Clear All Downloads" = "Alle Downloads löschen";
|
||||
"Clear Cache" = "Cache leeren";
|
||||
"Clear Library Only" = "Nur Bibliothek leeren";
|
||||
"Clear Logs" = "Logs löschen";
|
||||
"Click the plus button to add a module!" = "Tippe auf das Plus-Symbol, um ein Modul hinzuzufügen!";
|
||||
"Continue Watching" = "Weiterschauen";
|
||||
"Continue Watching Episode %d" = "Folge %d weiterschauen";
|
||||
"Contributors" = "Mitwirkende";
|
||||
"Copied to Clipboard" = "In die Zwischenablage kopiert";
|
||||
"Copy to Clipboard" = "In die Zwischenablage kopieren";
|
||||
"Copy URL" = "URL kopieren";
|
||||
"Collections" = "Sammlungen";
|
||||
"No Collections" = "Keine Sammlungen vorhanden";
|
||||
"Create a collection to organize your bookmarks" = "Erstelle eine Sammlung für mehr Organisation";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld Folgen";
|
||||
"%lld of %lld" = "%lld von %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% gesehen";
|
||||
"Episode %lld" = "Folge %lld";
|
||||
"Episodes" = "Folgen";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Die Folgen sind möglicherweise noch nicht verfügbar oder es gibt ein Problem mit der Quelle.";
|
||||
"Episodes Range" = "Folgenbereich";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Dunkel";
|
||||
"DATA & LOGS" = "DATEN & LOGS";
|
||||
"Debug" = "Debug";
|
||||
"Debugging and troubleshooting." = "Fehlersuche und Problembehebung.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Löschen";
|
||||
"Delete All" = "Alle löschen";
|
||||
"Delete All Downloads" = "Alle Downloads löschen";
|
||||
"Delete All Episodes" = "Alle Folgen löschen";
|
||||
"Delete Download" = "Download löschen";
|
||||
"Delete Episode" = "Folge löschen";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Tippe doppelt zum Spulen";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Doppeltippen auf die Bildschirmseiten überspringt entsprechend deiner Einstellung.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Herunterladen";
|
||||
"Download Episode" = "Folge herunterladen";
|
||||
"Download Summary" = "Download-Übersicht";
|
||||
"Download This Episode" = "Diese Folge herunterladen";
|
||||
"Downloaded" = "Geladen";
|
||||
"Downloaded Shows" = "Heruntergeladene Serien";
|
||||
"Downloading" = "Lädt herunter";
|
||||
"Downloads" = "Downloads";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Analytik aktivieren";
|
||||
"Enable Subtitles" = "Untertitel aktivieren";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Löschen";
|
||||
"Erase all App Data" = "Alle App-Daten löschen";
|
||||
"Erase App Data" = "App-Daten löschen";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Fehler";
|
||||
"Error Fetching Results" = "Fehler beim Laden";
|
||||
"Errors and critical issues." = "Fehler und kritische Probleme.";
|
||||
"Failed to load contributors" = "Mitwirkende konnten nicht geladen werden";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Folgen-Metadaten abrufen";
|
||||
"Files Downloaded" = "Dateien heruntergeladen";
|
||||
"Font Size" = "Schriftgröße";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Querformat erzwingen";
|
||||
"General" = "Allgemein";
|
||||
"General events and activities." = "Allgemeine Aktivitäten.";
|
||||
"General Preferences" = "Allgemeine Einstellungen";
|
||||
"Hide Splash Screen" = "Startbildschirm ausblenden";
|
||||
"Use Native Tab Bar" = "System Bar verwenden";
|
||||
"HLS video downloading." = "HLS Video-Downloads.";
|
||||
"Hold Speed" = "Geschwindigkeit halten";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Info";
|
||||
"INFOS" = "INFOS";
|
||||
"Installed Modules" = "Deine Module";
|
||||
"Interface" = "Oberfläche";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Tritt unserem Discord-Server bei";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Spalten (Querformat)";
|
||||
"Language" = "Sprache";
|
||||
"LESS" = "WENIGER";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Bibliothek";
|
||||
"License (GPLv3.0)" = "Lizenz (GPLv3.0)";
|
||||
"Light" = "Hell";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Lade Folge %lld...";
|
||||
"Loading logs..." = "Lade Logs...";
|
||||
"Loading module information..." = "Lade Modulinfos...";
|
||||
"Loading Stream" = "Stream wird geladen";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Debug-Infos protokollieren";
|
||||
"Log Filters" = "Log-Filter";
|
||||
"Log In with AniList" = "Mit AniList anmelden";
|
||||
"Log In with Trakt" = "Mit Trakt anmelden";
|
||||
"Log Out from AniList" = "Von AniList abmelden";
|
||||
"Log Out from Trakt" = "Von Trakt abmelden";
|
||||
"Log Types" = "Log-Typen";
|
||||
"Logged in as" = "Angemeldet als";
|
||||
"Logged in as " = "Angemeldet als ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logs";
|
||||
"Long press Skip" = "Lang drücken zum Überspringen";
|
||||
"MAIN" = "MAIN";
|
||||
"Main Developer" = "Hauptentwickler";
|
||||
"MAIN SETTINGS" = "HAUPTEINSTELLUNGEN";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Alle vorherigen als gesehen markieren";
|
||||
"Mark as Watched" = "Als gesehen markieren";
|
||||
"Mark Episode as Watched" = "Folge als gesehen markieren";
|
||||
"Mark Previous Episodes as Watched" = "Vorherige Folgen als gesehen markieren";
|
||||
"Mark watched" = "Als gesehen markieren";
|
||||
"Match with AniList" = "Mit AniList abgleichen";
|
||||
"Match with TMDB" = "Mit TMDB abgleichen";
|
||||
"Matched ID: %lld" = "Zugeordnete ID: %lld";
|
||||
"Matched with: %@" = "Abgeglichen mit: %@";
|
||||
"Max Concurrent Downloads" = "Max. gleichzeitige Downloads";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Medienraster-Layout";
|
||||
"Media Player" = "Media-Player";
|
||||
"Media View" = "Medienansicht";
|
||||
"Metadata Provider" = "Metadaten-Anbieter";
|
||||
"Metadata Providers Order" = "Reihenfolge der Metadaten-Anbieter";
|
||||
"Module Removed" = "Modul entfernt";
|
||||
"Modules" = "Module";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULE";
|
||||
"MORE" = "MEHR";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Keine aktiven Downloads";
|
||||
"No AniList matches found" = "Keine AniList-Übereinstimmungen";
|
||||
"No Data Available" = "Keine Daten";
|
||||
"No Downloads" = "Keine Downloads";
|
||||
"No episodes available" = "Keine Folgen verfügbar";
|
||||
"No Episodes Available" = "Keine Folgen verfügbar";
|
||||
"No items to continue watching." = "Keine Inhalte zum Weiterschauen verfügbar.";
|
||||
"No matches found" = "Keine Übereinstimmungen gefunden";
|
||||
"No Module Selected" = "Kein Modul ausgewählt";
|
||||
"No Modules" = "Keine Module";
|
||||
"No Results Found" = "Keine Ergebnisse";
|
||||
"No Search Results Found" = "Keine Suchergebnisse";
|
||||
"Nothing to Continue Watching" = "Keine Inhalte zum Weiterschauen";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Beachte, dass Module nur ersetzt werden, wenn in der JSON-Datei eine andere Versionskennung enthalten ist.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Community-Bibliothek öffnen";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "In AniList öffnen";
|
||||
"Original Poster" = "Original-Poster";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pausiert";
|
||||
"Play" = "Abspielen";
|
||||
"Player" = "Player";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Starte die App neu, um die Sprache zu ändern.";
|
||||
"Please select a module from settings" = "Wähle ein Modul in den Einstellungen";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Spalten (Hochformat)";
|
||||
"Progress bar Marker Color" = "Farbe der Fortschrittsanzeige";
|
||||
"Provider: %@" = "Anbieter: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Warteschlange";
|
||||
"Queued" = "In Warteschlange";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Deine kürzlich angesehenen Inhalte werden hier angezeigt.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Module beim Start aktualisieren";
|
||||
"Refresh Storage Info" = "Speicherinfo aktualisieren";
|
||||
"Remember Playback speed" = "Wiedergabegeschwindigkeit merken";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Entfernen";
|
||||
"Remove All Cache" = "Cache komplett löschen";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Alle Dokumente löschen";
|
||||
"Remove Documents" = "Dokumente löschen";
|
||||
"Remove Downloaded Media" = "Downloads entfernen";
|
||||
"Remove Downloads" = "Downloads entfernen";
|
||||
"Remove from Bookmarks" = "Aus Lesezeichen entfernen";
|
||||
"Remove Item" = "Element entfernen";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Problem melden";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Zurücksetzen";
|
||||
"Reset AniList ID" = "AniList-ID zurücksetzen";
|
||||
"Reset Episode Progress" = "Folgenfortschritt zurücksetzen";
|
||||
"Reset progress" = "Fortschritt zurücksetzen";
|
||||
"Reset Progress" = "Fortschritt zurücksetzen";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Neustart erforderlich";
|
||||
"Running Sora %@ - cranci1" = "Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Speichern";
|
||||
"Search" = "Suchen";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Downloads durchsuchen";
|
||||
"Search for something..." = "Suche nach etwas...";
|
||||
"Search..." = "Suchen...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Staffel %d";
|
||||
"Season %lld" = "Staffel %lld";
|
||||
"Segments Color" = "Segmentfarbe";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Modul auswählen";
|
||||
"Set Custom AniList ID" = "Eigene AniList-ID festlegen";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Einstellungen";
|
||||
"Shadow" = "Schatten";
|
||||
"Show More (%lld more characters)" = "Mehr anzeigen (%lld Zeichen mehr)";
|
||||
"Show PiP Button" = "PiP-Taste anzeigen";
|
||||
"Show Skip 85s Button" = "85s-Überspringen-Taste anzeigen";
|
||||
"Show Skip Intro / Outro Buttons" = "Intro/Outro-Tasten anzeigen";
|
||||
"Shows" = "Serien";
|
||||
"Size (%@)" = "Größe (%@)";
|
||||
"Skip Settings" = "Überspringen-Einstellungen";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Einige Funktionen sind auf den Sora- und Standard-Player beschränkt, wie z.B. Querformat erzwingen, Geschwindigkeit halten und individuelle Zeitsprünge.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ von cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora und cranci1 sind in keiner Weise mit AniList oder Trakt verbunden.\n\nBitte beachte, dass Fortschrittsaktualisierungen möglicherweise nicht zu 100% genau sind.";
|
||||
"Sora GitHub Repository" = "Sora GitHub-Repository";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur bleibt für immer kostenlos und ohne Werbung!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Sortieren";
|
||||
"Speed Settings" = "Geschwindigkeitseinstellungen";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Jetzt ansehen";
|
||||
"Start Watching Episode %d" = "Folge %d starten";
|
||||
"Storage Used" = "Belegter Speicher";
|
||||
"Stream" = "Streamen";
|
||||
"Streaming and video playback." = "Streaming und Videowiedergabe.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Untertitel-Farbe";
|
||||
"Subtitle Settings" = "Untertitel-Einstellungen";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Anime-Fortschritt syncen";
|
||||
"Sync TV shows progress" = "Serien-Fortschritt syncen";
|
||||
|
||||
/* System */
|
||||
"System" = "System";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Tippe einen Titel, um die Zuordnung zu ändern.";
|
||||
"Tap Skip" = "Tippe zum Überspringen";
|
||||
"Tap to manage your modules" = "Tippe hier, um deine Module zu verwalten";
|
||||
"Tap to select a module" = "Tippe hier, um ein Modul auszuwählen";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Der App-Cache hilft, Bilder schneller zu laden.\n\nDas Löschen des Dokumentenordners entfernt alle heruntergeladenen Module.\n\nLösche App-Daten nur, wenn du die Konsequenzen verstehst — dies könnte dazu führen, dass die App nicht mehr richtig funktioniert.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Der Folgenbereich legt fest, wie viele Folgen pro Seite angezeigt werden. Folgen werden in Gruppen zusammengefasst (z.B. 1-25, 26-50 usw.), um die Navigation zu erleichtern.\n\nFolgen-Metadaten beziehen sich auf Vorschaubilder und Titel, die manchmal Spoiler enthalten können.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Das Modul stellt nur eine einzelne Folge bereit, was wahrscheinlich ein Film ist, daher haben wir für diese Fälle separate Ansichten erstellt.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Thumbnail-Breite";
|
||||
"TMDB Match" = "TMDB-Zuordnung";
|
||||
"Trackers" = "Tracker";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Versuche es mit anderen Suchbegriffen";
|
||||
"Try different search terms" = "Verwende andere Suchbegriffe";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Halte mit zwei Fingern zum Pausieren";
|
||||
"Unable to fetch matches. Please try again later." = "Konnte keine Übereinstimmungen abrufen. Bitte versuche es später erneut.";
|
||||
"Use TMDB Poster Image" = "TMDB-Poster verwenden";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Video-Player";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Videoqualität";
|
||||
"View All" = "Alle anzeigen";
|
||||
"Watched" = "Gesehen";
|
||||
"Why am I not seeing any episodes?" = "Warum werden keine Folgen angezeigt?";
|
||||
"WiFi Quality" = "Qualität (WLAN)";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Du bist nicht angemeldet";
|
||||
"You have no items saved." = "Du hast keine gespeicherten Elemente.";
|
||||
"Your downloaded episodes will appear here" = "Deine heruntergeladenen Folgen werden hier angezeigt.";
|
||||
"Your recently watched content will appear here" = "Deine kürzlich angesehenen Inhalte werden hier angezeigt.";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Download-Einstellungen";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Die maximale Anzahl gleichzeitiger Downloads legt fest, wie viele Folgen gleichzeitig heruntergeladen werden können. Höhere Werte können mehr Bandbreite und Geräteleistung beanspruchen.";
|
||||
"Quality" = "Qualität";
|
||||
"Max Concurrent Downloads" = "Max. gleichzeitige Downloads";
|
||||
"Allow Cellular Downloads" = "Downloads über Mobilfunk erlauben";
|
||||
"Quality Information" = "Qualitäts-Info";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Speicherverwaltung";
|
||||
"Storage Used" = "Belegter Speicher";
|
||||
"Library cleared successfully" = "Bibliothek erfolgreich geleert";
|
||||
"All downloads deleted successfully" = "Alle Downloads erfolgreich gelöscht";
|
||||
|
||||
/* TabView */
|
||||
"LibraryTab" = "Bibliothek";
|
||||
"DownloadsTab" = "Downloads";
|
||||
"SettingsTab" = "Einstellungen";
|
||||
"SearchTab" = "Suchen";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Letzte Suchanfragen";
|
||||
"me frfr" = "Ich, ohne Witz";
|
||||
"Data" = "Daten";
|
||||
"Maximum Quality Available" = "Maximal verfügbare Qualität";
|
||||
|
||||
"DownloadCountFormat" = "%d von %d";
|
||||
"Error loading chapter" = "Fehler beim Laden des Kapitels";
|
||||
"Font Size: %dpt" = "Schriftgröße: %dpt";
|
||||
"Line Spacing: %.1f" = "Zeilenabstand: %.1f";
|
||||
"Line Spacing" = "Zeilenabstand";
|
||||
"Margin: %dpx" = "Rand: %dpx";
|
||||
"Margin" = "Rand";
|
||||
"Auto Scroll Speed" = "Automatische Scroll-Geschwindigkeit";
|
||||
"Speed" = "Geschwindigkeit";
|
||||
"Speed: %.1fx" = "Geschwindigkeit: %.1fx";
|
||||
"Matched %@: %@" = "Abgeglichen %@: %@";
|
||||
"Enter the AniList ID for this series" = "Geben Sie die AniList-ID für diese Serie ein";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Sammlung erstellen";
|
||||
"Collection Name" = "Sammlungsname";
|
||||
"Rename Collection" = "Sammlung umbenennen";
|
||||
"Rename" = "Umbenennen";
|
||||
"All Reading" = "Alles Lesen";
|
||||
"Recently Added" = "Kürzlich hinzugefügt";
|
||||
"Novel Title" = "Roman Titel";
|
||||
"Read Progress" = "Lesefortschritt";
|
||||
"Date Created" = "Erstellungsdatum";
|
||||
"Name" = "Name";
|
||||
"Item Count" = "Anzahl der Elemente";
|
||||
"Date Added" = "Hinzugefügt am";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Quelle";
|
||||
"Search reading..." = "Lesen durchsuchen...";
|
||||
"Search collections..." = "Sammlungen durchsuchen...";
|
||||
"Search bookmarks..." = "Lesezeichen durchsuchen...";
|
||||
"%d items" = "%d Elemente";
|
||||
"Fetching Data" = "Daten werden abgerufen";
|
||||
"Please wait while fetching." = "Bitte warten Sie während des Abrufs.";
|
||||
"Start Reading" = "Lesen starten";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Abgeschlossen";
|
||||
"Drag to reorder" = "Ziehen zum Neuordnen";
|
||||
"Drag to reorder sections" = "Ziehen zum Neuordnen der Abschnitte";
|
||||
"Library View" = "Bibliotheksansicht";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Passen Sie die in Ihrer Bibliothek angezeigten Abschnitte an. Sie können Abschnitte neu anordnen oder vollständig deaktivieren.";
|
||||
"Library Sections Order" = "Reihenfolge der Bibliotheksabschnitte";
|
||||
"Completion Percentage" = "Abschlussprozentsatz";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Einige Funktionen sind nur im Sora- und Standard-Player verfügbar, wie z.B. erzwungene Querformatansicht, Haltegeschwindigkeit und benutzerdefinierte Zeitsprünge.\n\nDie Einstellung des Abschlussprozentsatzes bestimmt, ab welchem Punkt vor dem Ende eines Videos die App es als abgeschlossen auf AniList und Trakt markiert.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Der App-Cache hilft, Bilder schneller zu laden.\n\nDas Löschen des Dokumente-Ordners entfernt alle heruntergeladenen Module.\n\nDas Löschen der App-Daten entfernt alle Ihre Einstellungen und Daten.";
|
||||
"Translators" = "Übersetzer";
|
||||
"Paste URL" = "URL einfügen";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serientitel";
|
||||
"Content Source" = "Inhaltsquelle";
|
||||
"Watch Progress" = "Fortschritt ansehen";
|
||||
"All Reading" = "Alles Lesen";
|
||||
"Nothing to Continue Reading" = "Nichts zum Weiterlesen";
|
||||
"Your recently read novels will appear here" = "Ihre zuletzt gelesenen Romane erscheinen hier";
|
||||
"No Bookmarks" = "Keine Lesezeichen";
|
||||
"Add bookmarks to this collection" = "Fügen Sie dieser Sammlung Lesezeichen hinzu";
|
||||
"items" = "Elemente";
|
||||
"All Watching" = "Alles Ansehen";
|
||||
"No Reading History" = "Kein Leseverlauf";
|
||||
"Books you're reading will appear here" = "Bücher, die Sie lesen, erscheinen hier";
|
||||
"Create Collection" = "Sammlung erstellen";
|
||||
"Collection Name" = "Sammlungsname";
|
||||
"Rename Collection" = "Sammlung umbenennen";
|
||||
"Rename" = "Umbenennen";
|
||||
"Novel Title" = "Roman Titel";
|
||||
"Read Progress" = "Lesefortschritt";
|
||||
"Date Created" = "Erstellungsdatum";
|
||||
"Name" = "Name";
|
||||
"Item Count" = "Anzahl der Elemente";
|
||||
"Date Added" = "Hinzugefügt am";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Quelle";
|
||||
"Search reading..." = "Lesen durchsuchen...";
|
||||
"Search collections..." = "Sammlungen durchsuchen...";
|
||||
"Search bookmarks..." = "Lesezeichen durchsuchen...";
|
||||
"%d items" = "%d Elemente";
|
||||
"Fetching Data" = "Daten werden abgerufen";
|
||||
"Please wait while fetching." = "Bitte warten Sie während des Abrufs.";
|
||||
"Start Reading" = "Lesen starten";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Abgeschlossen";
|
||||
"Drag to reorder" = "Ziehen zum Neuordnen";
|
||||
"Drag to reorder sections" = "Ziehen zum Neuordnen der Abschnitte";
|
||||
"Library View" = "Bibliotheksansicht";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Passen Sie die in Ihrer Bibliothek angezeigten Abschnitte an. Sie können Abschnitte neu anordnen oder vollständig deaktivieren.";
|
||||
"Library Sections Order" = "Reihenfolge der Bibliotheksabschnitte";
|
||||
"Completion Percentage" = "Abschlussprozentsatz";
|
||||
"Translators" = "Übersetzer";
|
||||
"Paste URL" = "URL einfügen";
|
||||
|
||||
"Continue Reading" = "Weiterlesen";
|
||||
|
||||
"Backup & Restore" = "Sichern & Wiederherstellen";
|
||||
"Export Backup" = "Backup exportieren";
|
||||
"Import Backup" = "Backup importieren";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Hinweis: Diese Funktion ist noch experimentell. Bitte überprüfe deine Daten nach dem Export/Import.";
|
||||
"Backup" = "Backup";
|
||||
461
Sora/Localization/en.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
/* General */
|
||||
"About" = "About";
|
||||
"About Sora" = "About Sora";
|
||||
"Active" = "Active";
|
||||
"Active Downloads" = "Active Downloads";
|
||||
"Actively downloading media can be tracked from here." = "Actively downloading media can be tracked from here.";
|
||||
"Add Module" = "Add Module";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Adjust the number of media items per row in portrait and landscape modes.";
|
||||
"Advanced" = "Advanced";
|
||||
"AKA Sulfur" = "AKA Sulfur";
|
||||
"All Bookmarks" = "All Bookmarks";
|
||||
"All Watching" = "All Watching";
|
||||
"Also known as Sulfur" = "Also known as Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ID";
|
||||
"AniList Match" = "AniList Match";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time.";
|
||||
"App Info" = "App Info";
|
||||
"App Language" = "App Language";
|
||||
"App Storage" = "App Storage";
|
||||
"Appearance" = "Appearance";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Are you sure you want to clear all cached data? This will help free up storage space.";
|
||||
"Are you sure you want to delete '%@'?" = "Are you sure you want to delete '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Are you sure you want to delete all %1$d episodes in '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Are you sure you want to erase all app data? This action cannot be undone.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Background Enabled";
|
||||
"Bookmark items for an easier access later." = "Bookmark items for an easier access later.";
|
||||
"Bookmarks" = "Bookmarks";
|
||||
"Bottom Padding" = "Bottom Padding";
|
||||
"Cancel" = "Cancel";
|
||||
"Cellular Quality" = "Cellular Quality";
|
||||
"Check out some community modules here!" = "Check out some community modules here!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player.";
|
||||
"Clear" = "Clear";
|
||||
"Clear All Downloads" = "Clear All Downloads";
|
||||
"Clear Cache" = "Clear Cache";
|
||||
"Clear Library Only" = "Clear Library Only";
|
||||
"Clear Logs" = "Clear Logs";
|
||||
"Click the plus button to add a module!" = "Click the plus button to add a module!";
|
||||
"Continue Watching" = "Continue Watching";
|
||||
"Continue Watching Episode %d" = "Continue Watching Episode %d";
|
||||
"Contributors" = "Contributors";
|
||||
"Copied to Clipboard" = "Copied to Clipboard";
|
||||
"Copy to Clipboard" = "Copy to Clipboard";
|
||||
"Copy URL" = "Copy URL";
|
||||
"Collections" = "Collections";
|
||||
"No Collections" = "No Collections";
|
||||
"Create a collection to organize your bookmarks" = "Create a collection to organize your bookmarks";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld Episodes";
|
||||
"%lld of %lld" = "%1$lld of %2$lld";
|
||||
"%lld-%lld" = "%1$lld-%2$lld";
|
||||
"%lld%% seen" = "%lld%% seen";
|
||||
"Episode %lld" = "Episode %lld";
|
||||
"Episodes" = "Episodes";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Episodes might not be available yet or there could be an issue with the source.";
|
||||
"Episodes Range" = "Episodes Range";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Dark";
|
||||
"DATA & LOGS" = "DATA & LOGS";
|
||||
"Debug" = "Debug";
|
||||
"Debugging and troubleshooting." = "Debugging and troubleshooting.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Delete";
|
||||
"Delete All" = "Delete All";
|
||||
"Delete All Downloads" = "Delete All Downloads";
|
||||
"Delete All Episodes" = "Delete All Episodes";
|
||||
"Delete Download" = "Delete Download";
|
||||
"Delete Episode" = "Delete Episode";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Double Tap to Seek";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Double tapping the screen on it's sides will skip with the short tap setting.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Download";
|
||||
"Download Episode" = "Download Episode";
|
||||
"Download Summary" = "Download Summary";
|
||||
"Download This Episode" = "Download This Episode";
|
||||
"Downloaded" = "Downloaded";
|
||||
"Downloaded Shows" = "Downloaded Shows";
|
||||
"Downloading" = "Downloading";
|
||||
"Downloads" = "Downloads";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Enable Analytics";
|
||||
"Enable Subtitles" = "Enable Subtitles";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Erase";
|
||||
"Erase all App Data" = "Erase all App Data";
|
||||
"Erase App Data" = "Erase App Data";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Error";
|
||||
"Error Fetching Results" = "Error Fetching Results";
|
||||
"Errors and critical issues." = "Errors and critical issues.";
|
||||
"Failed to load contributors" = "Failed to load contributors";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Fetch Episode metadata";
|
||||
"Files Downloaded" = "Files Downloaded";
|
||||
"Font Size" = "Font Size";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Force Landscape";
|
||||
"General" = "General";
|
||||
"General events and activities." = "General events and activities.";
|
||||
"General Preferences" = "General Preferences";
|
||||
"Hide Splash Screen" = "Hide Splash Screen";
|
||||
"Use Native Tab Bar" = "Use Native Tabs";
|
||||
"HLS video downloading." = "HLS video downloading.";
|
||||
"Hold Speed" = "Hold Speed";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Info";
|
||||
"INFOS" = "INFOS";
|
||||
"Installed Modules" = "Installed Modules";
|
||||
"Interface" = "Interface";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Join the Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Landscape Columns";
|
||||
"Language" = "Language";
|
||||
"LESS" = "LESS";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Library";
|
||||
"License (GPLv3.0)" = "License (GPLv3.0)";
|
||||
"Light" = "Light";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Loading Episode %lld...";
|
||||
"Loading logs..." = "Loading logs...";
|
||||
"Loading module information..." = "Loading module information...";
|
||||
"Loading Stream" = "Loading Stream";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Log Debug Info";
|
||||
"Log Filters" = "Log Filters";
|
||||
"Log In with AniList" = "Log In with AniList";
|
||||
"Log In with Trakt" = "Log In with Trakt";
|
||||
"Log Out from AniList" = "Log Out from AniList";
|
||||
"Log Out from Trakt" = "Log Out from Trakt";
|
||||
"Log Types" = "Log Types";
|
||||
"Logged in as" = "Logged in as";
|
||||
"Logged in as " = "Logged in as ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logs";
|
||||
"Long press Skip" = "Long press Skip";
|
||||
"MAIN" = "Main Settings";
|
||||
"Main Developer" = "Main Developer";
|
||||
"MAIN SETTINGS" = "MAIN SETTINGS";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Mark All Previous Watched";
|
||||
"Mark as Watched" = "Mark as Watched";
|
||||
"Mark Episode as Watched" = "Mark Episode as Watched";
|
||||
"Mark Previous Episodes as Watched" = "Mark Previous Episodes as Watched";
|
||||
"Mark watched" = "Mark watched";
|
||||
"Match with AniList" = "Match with AniList";
|
||||
"Match with TMDB" = "Match with TMDB";
|
||||
"Matched ID: %lld" = "Matched ID: %lld";
|
||||
"Matched with: %@" = "Matched with: %@";
|
||||
"Max Concurrent Downloads" = "Max Concurrent Downloads";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Media Grid Layout";
|
||||
"Media Player" = "Media Player";
|
||||
"Media View" = "Media View";
|
||||
"Metadata Provider" = "Metadata Provider";
|
||||
"Metadata Providers Order" = "Metadata Providers Order";
|
||||
"Module Removed" = "Module Removed";
|
||||
"Modules" = "Modules";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULES";
|
||||
"MORE" = "MORE";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "No Active Downloads";
|
||||
"No AniList matches found" = "No AniList matches found";
|
||||
"No Data Available" = "No Data Available";
|
||||
"No Downloads" = "No Downloads";
|
||||
"No episodes available" = "No episodes available";
|
||||
"No Episodes Available" = "No Episodes Available";
|
||||
"No items to continue watching." = "No items to continue watching.";
|
||||
"No matches found" = "No matches found";
|
||||
"No Module Selected" = "No Module Selected";
|
||||
"No Modules" = "No Modules";
|
||||
"No Results Found" = "No Results Found";
|
||||
"No Search Results Found" = "No Search Results Found";
|
||||
"Nothing to Continue Watching" = "Nothing to Continue Watching";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Note that the modules will be replaced only if there is a different version string inside the JSON file.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Open Community Library";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Open in AniList";
|
||||
"Original Poster" = "Original Poster";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Paused";
|
||||
"Play" = "Play";
|
||||
"Player" = "Player";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Please restart the app to apply the language change.";
|
||||
"Please select a module from settings" = "Please select a module from settings";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Portrait Columns";
|
||||
"Progress bar Marker Color" = "Progress bar Marker Color";
|
||||
"Provider: %@" = "Provider: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Queue";
|
||||
"Queued" = "Queued";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Recently watched content will appear here.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Refresh Modules on Launch";
|
||||
"Refresh Storage Info" = "Refresh Storage Info";
|
||||
"Remember Playback speed" = "Remember Playback speed";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Remove";
|
||||
"Remove All Cache" = "Remove All Cache";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Remove All Documents";
|
||||
"Remove Documents" = "Remove Documents";
|
||||
"Remove Downloaded Media" = "Remove Downloaded Media";
|
||||
"Remove Downloads" = "Remove Downloads";
|
||||
"Remove from Bookmarks" = "Remove from Bookmarks";
|
||||
"Remove Item" = "Remove Item";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Report an Issue";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Reset";
|
||||
"Reset AniList ID" = "Reset AniList ID";
|
||||
"Reset Episode Progress" = "Reset Episode Progress";
|
||||
"Reset progress" = "Reset progress";
|
||||
"Reset Progress" = "Reset Progress";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Restart Required";
|
||||
"Running Sora %@ - cranci1" = "Running Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Save";
|
||||
"Search" = "Search";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Search downloads";
|
||||
"Search for something..." = "Search for something...";
|
||||
"Search..." = "Search...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Season %d";
|
||||
"Season %lld" = "Season %lld";
|
||||
"Segments Color" = "Segments Color";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Select Module";
|
||||
"Set Custom AniList ID" = "Set Custom AniList ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Settings";
|
||||
"Shadow" = "Shadow";
|
||||
"Show More (%lld more characters)" = "Show More (%lld more characters)";
|
||||
"Show PiP Button" = "Show PiP Button";
|
||||
"Show Skip 85s Button" = "Show Skip 85s Button";
|
||||
"Show Skip Intro / Outro Buttons" = "Show Skip Intro / Outro Buttons";
|
||||
"Shows" = "Shows";
|
||||
"Size (%@)" = "Size (%@)";
|
||||
"Skip Settings" = "Skip Settings";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ by cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate.";
|
||||
"Sora GitHub Repository" = "Sora GitHub Repository";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur will always remain free with no ADs!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Sort";
|
||||
"Speed Settings" = "Speed Settings";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Start Watching";
|
||||
"Start Watching Episode %d" = "Start Watching Episode %d";
|
||||
"Storage Used" = "Storage Used";
|
||||
"Stream" = "Stream";
|
||||
"Streaming and video playback." = "Streaming and video playback.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Subtitle Color";
|
||||
"Subtitle Settings" = "Subtitle Settings";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Sync anime progress";
|
||||
"Sync TV shows progress" = "Sync TV shows progress";
|
||||
|
||||
/* System */
|
||||
"System" = "System";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Tap a title to override the current match.";
|
||||
"Tap Skip" = "Tap Skip";
|
||||
"Tap to manage your modules" = "Tap to manage your modules";
|
||||
"Tap to select a module" = "Tap to select a module";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases.";
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Thumbnails Width";
|
||||
"TMDB Match" = "TMDB Match";
|
||||
"Trackers" = "Trackers";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Try different keywords";
|
||||
"Try different search terms" = "Try different search terms";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Two Finger Hold for Pause";
|
||||
"Unable to fetch matches. Please try again later." = "Unable to fetch matches. Please try again later.";
|
||||
"Use TMDB Poster Image" = "Use TMDB Poster Image";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Video Player";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Video Quality Preferences";
|
||||
"View All" = "View All";
|
||||
"Watched" = "Watched";
|
||||
"Why am I not seeing any episodes?" = "Why am I not seeing any episodes?";
|
||||
"WiFi Quality" = "WiFi Quality";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "You are not logged in";
|
||||
"You have no items saved." = "You have no items saved.";
|
||||
"Your downloaded episodes will appear here" = "Your downloaded episodes will appear here";
|
||||
"Your recently watched content will appear here" = "Your recently watched content will appear here";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Download Settings";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources.";
|
||||
"Quality" = "Quality";
|
||||
"Max Concurrent Downloads" = "Max Concurrent Downloads";
|
||||
"Allow Cellular Downloads" = "Allow Cellular Downloads";
|
||||
"Quality Information" = "Quality Information";
|
||||
"Maximum Quality Available" = "Maximum Quality Available";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Storage Management";
|
||||
"Storage Used" = "Storage Used";
|
||||
"Library cleared successfully" = "Library cleared successfully";
|
||||
"All downloads deleted successfully" = "All downloads deleted successfully";
|
||||
|
||||
/* TabView */
|
||||
"LibraryTab" = "Library";
|
||||
"DownloadsTab" = "Downloads";
|
||||
"SettingsTab" = "Settings";
|
||||
"SearchTab" = "Search";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Recent searches";
|
||||
"me frfr" = "me frfr";
|
||||
"Data" = "Data";
|
||||
"All Reading" = "All Reading";
|
||||
"No Reading History" = "No Reading History";
|
||||
"Books you're reading will appear here" = "Books you're reading will appear here";
|
||||
"All Watching" = "All Watching";
|
||||
"Continue Reading" = "Continue Reading";
|
||||
"Nothing to Continue Reading" = "Nothing to Continue Reading";
|
||||
"Your recently read novels will appear here" = "Your recently read novels will appear here";
|
||||
"No Bookmarks" = "No Bookmarks";
|
||||
"Add bookmarks to this collection" = "Add bookmarks to this collection";
|
||||
"items" = "items";
|
||||
"Chapter %d" = "Chapter %d";
|
||||
"Episode %d" = "Episode %d";
|
||||
"%d%%" = "%d%%";
|
||||
"%d%% seen" = "%d%% seen";
|
||||
"DownloadCountFormat" = "%d of %d";
|
||||
"Error loading chapter" = "Error loading chapter";
|
||||
"Font Size: %dpt" = "Font Size: %dpt";
|
||||
"Line Spacing: %.1f" = "Line Spacing: %.1f";
|
||||
"Line Spacing" = "Line Spacing";
|
||||
"Margin: %dpx" = "Margin: %dpx";
|
||||
"Margin" = "Margin";
|
||||
"Auto Scroll Speed" = "Auto Scroll Speed";
|
||||
"Speed" = "Speed";
|
||||
"Speed: %.1fx" = "Speed: %.1fx";
|
||||
"Matched %@: %@" = "Matched %@: %@";
|
||||
"Enter the AniList ID for this series" = "Enter the AniList ID for this series";
|
||||
|
||||
/* New additions */
|
||||
"Create Collection" = "Create Collection";
|
||||
"Collection Name" = "Collection Name";
|
||||
"Rename Collection" = "Rename Collection";
|
||||
"Rename" = "Rename";
|
||||
"All Reading" = "All Reading";
|
||||
"Recently Added" = "Recently Added";
|
||||
"Novel Title" = "Novel Title";
|
||||
"Read Progress" = "Read Progress";
|
||||
"Date Created" = "Date Created";
|
||||
"Name" = "Name";
|
||||
"Item Count" = "Item Count";
|
||||
"Date Added" = "Date Added";
|
||||
"Title" = "Title";
|
||||
"Source" = "Source";
|
||||
"Search reading..." = "Search reading...";
|
||||
"Search collections..." = "Search collections...";
|
||||
"Search bookmarks..." = "Search bookmarks...";
|
||||
"%d items" = "%d items";
|
||||
"Fetching Data" = "Fetching Data";
|
||||
"Please wait while fetching." = "Please wait while fetching.";
|
||||
"Start Reading" = "Start Reading";
|
||||
"Chapters" = "Chapters";
|
||||
"Completed" = "Completed";
|
||||
"Drag to reorder" = "Drag to reorder";
|
||||
"Drag to reorder sections" = "Drag to reorder sections";
|
||||
"Library View" = "Library View";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Customize the sections shown in your library. You can reorder sections or disable them completely.";
|
||||
"Library Sections Order" = "Library Sections Order";
|
||||
"Completion Percentage" = "Completion Percentage";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app.";
|
||||
"Translators" = "Translators";
|
||||
"Paste URL" = "Paste URL";
|
||||
|
||||
506
Sora/Localization/es.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
/* General */
|
||||
"About" = "Acerca de";
|
||||
"About Sora" = "Acerca de Sora";
|
||||
"Active" = "Activo";
|
||||
"Active Downloads" = "Descargas activas";
|
||||
"Actively downloading media can be tracked from here." = "El contenido multimedia que se está descargando activamente se puede seguir desde aquí.";
|
||||
"Add Module" = "Añadir módulo";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Ajusta el número de elementos multimedia por fila en modo vertical y horizontal.";
|
||||
"Advanced" = "Avanzado";
|
||||
"AKA Sulfur" = "También conocido como Sulfur";
|
||||
"All Bookmarks" = "Todos los marcadores";
|
||||
"All Watching" = "Todo lo que estás viendo";
|
||||
"Also known as Sulfur" = "También conocido como Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "ID de AniList";
|
||||
"AniList Match" = "Coincidencia de AniList";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Se recopilan datos anónimos para mejorar la aplicación. No se recopila información personal. Esto puede deshabilitarse en cualquier momento.";
|
||||
"App Info" = "Información de la aplicación";
|
||||
"App Language" = "Idioma de la aplicación";
|
||||
"App Storage" = "Almacenamiento de la aplicación";
|
||||
"Appearance" = "Apariencia";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "¿Estás seguro/a de que quieres borrar todos los datos en caché? Esto ayudará a liberar espacio de almacenamiento.";
|
||||
"Are you sure you want to delete '%@'?" = "¿Estás seguro/a de que quieres eliminar '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "¿Estás seguro/a de que quieres eliminar todos los %1$d episodios en '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "¿Estás seguro/a de que quieres eliminar todos los archivos descargados? Puedes optar por borrar solo la biblioteca y conservar los archivos descargados para uso futuro.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "¿Estás seguro/a de que quieres borrar todos los datos de la aplicación? Esta acción no se puede deshacer.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Fondo habilitado";
|
||||
"Bookmark items for an easier access later." = "Guarda los elementos en marcadores para facilitar el acceso más adelante.";
|
||||
"Bookmarks" = "Marcadores";
|
||||
"Bottom Padding" = "Espacio inferior";
|
||||
"Cancel" = "Cancelar";
|
||||
"Cellular Quality" = "Calidad con datos móviles";
|
||||
"Check out some community modules here!" = "¡Echa un vistazo a algunos módulos de la comunidad aquí!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Elige la resolución de video preferida para conexiones WiFi y de datos móviles. Las resoluciones más altas usan más datos pero brindan mejor calidad. Si la calidad exacta no está disponible, se seleccionará automáticamente la opción más cercana.\n\nNota: No todas las fuentes de video y reproductores admiten la selección de calidad. Esta función funciona mejor con transmisiones HLS usando el reproductor Sora.";
|
||||
"Clear" = "Borrar";
|
||||
"Clear All Downloads" = "Borrar todas las descargas";
|
||||
"Clear Cache" = "Borrar caché";
|
||||
"Clear Library Only" = "Borrar solo la biblioteca";
|
||||
"Clear Logs" = "Borrar registros";
|
||||
"Click the plus button to add a module!" = "¡Haz clic en el botón de más para añadir un módulo!";
|
||||
"Continue Watching" = "Continuar viendo";
|
||||
"Continue Watching Episode %d" = "Continuar viendo el episodio %d";
|
||||
"Contributors" = "Colaboradores";
|
||||
"Copied to Clipboard" = "Copiado al portapapeles";
|
||||
"Copy to Clipboard" = "Copiar al portapapeles";
|
||||
"Copy URL" = "Copiar enlace";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld Episodios";
|
||||
"%lld of %lld" = "%lld de %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% visto(s)";
|
||||
"Episode %lld" = "Episodio %lld";
|
||||
"Episodes" = "Episodios";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Es posible que los episodios aún no estén disponibles o que haya un problema con la fuente.";
|
||||
"Episodes Range" = "Rango de episodios";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Oscuro";
|
||||
"DATA & LOGS" = "DATOS Y REGISTROS";
|
||||
"Debug" = "Depurar";
|
||||
"Debugging and troubleshooting." = "Depuración y resolución de problemas.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Eliminar";
|
||||
"Delete All" = "Eliminar todo";
|
||||
"Delete All Downloads" = "Eliminar todas las descargas";
|
||||
"Delete All Episodes" = "Eliminar todos los episodios";
|
||||
"Delete Download" = "Eliminar descarga";
|
||||
"Delete Episode" = "Eliminar episodio";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Toca dos veces para buscar";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Al tocar dos veces la pantalla en sus lados, se saltará con la configuración de toque corto.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Descargar";
|
||||
"Download Episode" = "Descargar episodio";
|
||||
"Download Summary" = "Resumen de descarga";
|
||||
"Download This Episode" = "Descargar este episodio";
|
||||
"Downloaded" = "Descargado";
|
||||
"Downloaded Shows" = "Programas descargados";
|
||||
"Downloading" = "Descargando";
|
||||
"Downloads" = "Descargas";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Habilitar análisis";
|
||||
"Enable Subtitles" = "Habilitar subtítulos";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Borrar";
|
||||
"Erase all App Data" = "Borrar todos los datos de la aplicación";
|
||||
"Erase App Data" = "Borrar datos de la aplicación";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Error";
|
||||
"Error Fetching Results" = "Error al obtener resultados";
|
||||
"Errors and critical issues." = "Errores y problemas críticos.";
|
||||
"Failed to load contributors" = "No se pudieron cargar los colaboradores";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Obtener metadatos del episodio";
|
||||
"Files Downloaded" = "Archivos descargados";
|
||||
"Font Size" = "Tamaño de fuente";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Forzar horizontal";
|
||||
"General" = "General";
|
||||
"General events and activities." = "Eventos y actividades generales.";
|
||||
"General Preferences" = "Preferencias generales";
|
||||
"Hide Splash Screen" = "Ocultar pantalla de inicio";
|
||||
"HLS video downloading." = "Descarga de video HLS.";
|
||||
"Hold Speed" = "Velocidad de retención";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Información";
|
||||
"INFOS" = "INFORMACIÓN";
|
||||
"Installed Modules" = "Módulos instalados";
|
||||
"Interface" = "Interfaz";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Unirse a Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Columnas horizontales";
|
||||
"Language" = "Idioma";
|
||||
"LESS" = "MENOS";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Biblioteca";
|
||||
"License (GPLv3.0)" = "Licencia (GPLv3.0)";
|
||||
"Light" = "Claro";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Cargando episodio %lld...";
|
||||
"Loading logs..." = "Cargando registros...";
|
||||
"Loading module information..." = "Cargando información del módulo...";
|
||||
"Loading Stream" = "Cargando transmisión";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Registrar información de depuración";
|
||||
"Log Filters" = "Filtros de registro";
|
||||
"Log In with AniList" = "Iniciar sesión con AniList";
|
||||
"Log In with Trakt" = "Iniciar sesión con Trakt";
|
||||
"Log Out from AniList" = "Cerrar sesión de AniList";
|
||||
"Log Out from Trakt" = "Cerrar sesión de Trakt";
|
||||
"Log Types" = "Tipos de registro";
|
||||
"Logged in as" = "Sesión iniciada como";
|
||||
"Logged in as " = "Sesión iniciada como ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Registros";
|
||||
"Long press Skip" = "Mantener presionado para saltar";
|
||||
"MAIN" = "PRINCIPAL";
|
||||
"Main Developer" = "Desarrollador principal";
|
||||
"MAIN SETTINGS" = "AJUSTES PRINCIPALES";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Marcar todos los anteriores como vistos";
|
||||
"Mark as Watched" = "Marcar como visto";
|
||||
"Mark Episode as Watched" = "Marcar episodio como visto";
|
||||
"Mark Previous Episodes as Watched" = "Marcar episodios anteriores como vistos";
|
||||
"Mark watched" = "Marcar como visto";
|
||||
"Match with AniList" = "Coincidir con AniList";
|
||||
"Match with TMDB" = "Coincidir con TMDB";
|
||||
"Matched ID: %lld" = "ID coincidente: %lld";
|
||||
"Matched with: %@" = "Coincidido con: %@";
|
||||
"Max Concurrent Downloads" = "Máx. descargas simultáneas";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Diseño de cuadrícula multimedia";
|
||||
"Media Player" = "Reproductor multimedia";
|
||||
"Media View" = "Vista multimedia";
|
||||
"Metadata Provider" = "Proveedor de metadatos";
|
||||
"Metadata Providers Order" = "Orden de proveedores de metadatos";
|
||||
"Module Removed" = "Módulo eliminado";
|
||||
"Modules" = "Módulos";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MÓDULOS";
|
||||
"MORE" = "MÁS";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "No hay descargas activas";
|
||||
"No AniList matches found" = "No se encontraron coincidencias en AniList";
|
||||
"No Data Available" = "No hay datos disponibles";
|
||||
"No Downloads" = "No hay descargas";
|
||||
"No episodes available" = "No hay episodios disponibles";
|
||||
"No Episodes Available" = "No hay episodios disponibles";
|
||||
"No items to continue watching." = "No hay elementos para seguir viendo.";
|
||||
"No matches found" = "No se encontraron coincidencias";
|
||||
"No Module Selected" = "Ningún módulo seleccionado";
|
||||
"No Modules" = "No hay módulos";
|
||||
"No Results Found" = "No se encontraron resultados";
|
||||
"No Search Results Found" = "No se encontraron resultados de búsqueda";
|
||||
"Nothing to Continue Watching" = "Nada para continuar viendo";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Ten en cuenta que los módulos se reemplazarán solo si hay una cadena de versión diferente dentro del archivo JSON.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Abrir biblioteca de la comunidad";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Abrir en AniList";
|
||||
"Original Poster" = "Póster original";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pausado";
|
||||
"Play" = "Reproducir";
|
||||
"Player" = "Reproductor";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Por favor, reinicia la aplicación para aplicar el cambio de idioma.";
|
||||
"Please select a module from settings" = "Por favor, selecciona un módulo desde la configuración";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Columnas verticales";
|
||||
"Progress bar Marker Color" = "Color del marcador de la barra de progreso";
|
||||
"Provider: %@" = "Proveedor: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Cola";
|
||||
"Queued" = "En cola";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "El contenido visto recientemente aparecerá aquí.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Actualizar módulos al iniciar";
|
||||
"Refresh Storage Info" = "Actualizar información de almacenamiento";
|
||||
"Remember Playback speed" = "Recordar velocidad de reproducción";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Eliminar";
|
||||
"Remove All Cache" = "Eliminar toda la caché";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Eliminar todos los documentos";
|
||||
"Remove Documents" = "Eliminar documentos";
|
||||
"Remove Downloaded Media" = "Eliminar contenido multimedia descargado";
|
||||
"Remove Downloads" = "Eliminar descargas";
|
||||
"Remove from Bookmarks" = "Eliminar de marcadores";
|
||||
"Remove Item" = "Eliminar elemento";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Reportar un problema";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Restablecer";
|
||||
"Reset AniList ID" = "Restablecer ID de AniList";
|
||||
"Reset Episode Progress" = "Restablecer progreso del episodio";
|
||||
"Reset progress" = "Restablecer progreso";
|
||||
"Reset Progress" = "Restablecer progreso";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Reinicio requerido";
|
||||
"Running Sora %@ - cranci1" = "Ejecutando Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Guardar";
|
||||
"Search" = "Buscar";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Buscar descargas";
|
||||
"Search for something..." = "Buscar algo...";
|
||||
"Search..." = "Buscar...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Temporada %d";
|
||||
"Season %lld" = "Temporada %lld";
|
||||
"Segments Color" = "Color de segmentos";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Seleccionar módulo";
|
||||
"Set Custom AniList ID" = "Establecer ID de AniList personalizado";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Ajustes";
|
||||
"Shadow" = "Sombra";
|
||||
"Show More (%lld more characters)" = "Mostrar más (%lld caracteres más)";
|
||||
"Show PiP Button" = "Mostrar botón PiP";
|
||||
"Show Skip 85s Button" = "Mostrar botón Saltar 85s";
|
||||
"Show Skip Intro / Outro Buttons" = "Mostrar botones Saltar introducción/final";
|
||||
"Shows" = "Programas";
|
||||
"Size (%@)" = "Tamaño (%@)";
|
||||
"Skip Settings" = "Configuración de saltar";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Algunas funciones están limitadas al reproductor Sora y Default, como Forzar horizontal, velocidad de retención e incrementos de tiempo de salto personalizados.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ por cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "Sora y cranci1 no están afiliados a AniList ni a Trakt de ninguna manera.
|
||||
|
||||
Ten en cuenta también que las actualizaciones de progreso pueden no ser 100% precisas.";
|
||||
"Sora GitHub Repository" = "Repositorio de Sora en GitHub";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "¡Sora/Sulfur siempre será gratis y sin anuncios!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Ordenar";
|
||||
"Speed Settings" = "Configuración de velocidad";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Empezar a ver";
|
||||
"Start Watching Episode %d" = "Empezar a ver episodio %d";
|
||||
"Storage Used" = "Almacenamiento usado";
|
||||
"Stream" = "Transmisión";
|
||||
"Streaming and video playback." = "Transmisión y reproducción de video.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Color de subtítulos";
|
||||
"Subtitle Settings" = "Configuración de subtítulos";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Sincronizar progreso de anime";
|
||||
"Sync TV shows progress" = "Sincronizar progreso de programas de TV";
|
||||
|
||||
/* System */
|
||||
"System" = "Sistema";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Toca un título para anular la coincidencia actual.";
|
||||
"Tap Skip" = "Tocar para saltar";
|
||||
"Tap to manage your modules" = "Toca para administrar tus módulos";
|
||||
"Tap to select a module" = "Toca para seleccionar un módulo";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.
|
||||
|
||||
Clearing the Documents folder will delete all downloaded modules.
|
||||
|
||||
Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "La caché de la aplicación ayuda a que la aplicación cargue las imágenes más rápido.
|
||||
|
||||
Al borrar la carpeta Documentos, se eliminarán todos los módulos descargados.
|
||||
|
||||
No borres los datos de la aplicación a menos que comprendas las consecuencias; podría hacer que la aplicación funcione mal.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.
|
||||
|
||||
For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "El rango de episodios controla cuántos episodios aparecen en cada página. Los episodios se agrupan en conjuntos (como 1-25, 26-50, etc.), lo que te permite navegar por ellos más fácilmente.
|
||||
|
||||
Para los metadatos del episodio, se refiere a la miniatura y el título del episodio, ya que a veces puede contener spoilers.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "El módulo proporcionó solo un episodio, esto es muy probable que sea una película, por lo que decidimos hacer pantallas separadas para estos casos.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Ancho de miniaturas";
|
||||
"TMDB Match" = "Coincidencia de TMDB";
|
||||
"Trackers" = "Rastreadores";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Prueba con diferentes palabras clave";
|
||||
"Try different search terms" = "Prueba con diferentes términos de búsqueda";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Mantén presionado con dos dedos para pausar";
|
||||
"Unable to fetch matches. Please try again later." = "No se pudieron obtener las coincidencias. Por favor, inténtalo de nuevo más tarde.";
|
||||
"Use TMDB Poster Image" = "Usar imagen de póster de TMDB";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Reproductor de video";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Preferencias de calidad de video";
|
||||
"View All" = "Ver todo";
|
||||
"Watched" = "Visto";
|
||||
"Why am I not seeing any episodes?" = "¿Por qué no veo ningún episodio?";
|
||||
"WiFi Quality" = "Calidad de WiFi";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "No has iniciado sesión";
|
||||
"You have no items saved." = "No tienes elementos guardados.";
|
||||
"Your downloaded episodes will appear here" = "Tus episodios descargados aparecerán aquí";
|
||||
"Your recently watched content will appear here" = "Tu contenido visto recientemente aparecerá aquí";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Configuración de descarga";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "El número máximo de descargas simultáneas controla cuántos episodios se pueden descargar al mismo tiempo. Valores más altos pueden usar más ancho de banda y recursos del dispositivo.";
|
||||
"Quality" = "Calidad";
|
||||
"Max Concurrent Downloads" = "Máx. descargas simultáneas";
|
||||
"Allow Cellular Downloads" = "Permitir descargas con datos móviles";
|
||||
"Quality Information" = "Información de calidad";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Gestión de almacenamiento";
|
||||
"Storage Used" = "Almacenamiento usado";
|
||||
"Library cleared successfully" = "Biblioteca borrada con éxito";
|
||||
"All downloads deleted successfully" = "Todas las descargas eliminadas con éxito";
|
||||
|
||||
/* New localizations */
|
||||
"Recent searches" = "Búsquedas recientes";
|
||||
"me frfr" = "yo frfr";
|
||||
"Data" = "Datos";
|
||||
"Maximum Quality Available" = "Calidad máxima disponible";
|
||||
"DownloadCountFormat" = "%d de %d";
|
||||
"Error loading chapter" = "Error al cargar el capítulo";
|
||||
"Font Size: %dpt" = "Tamaño de fuente: %dpt";
|
||||
"Line Spacing: %.1f" = "Espaciado de línea: %.1f";
|
||||
"Line Spacing" = "Espaciado de línea";
|
||||
"Margin: %dpx" = "Margen: %dpx";
|
||||
"Margin" = "Margen";
|
||||
"Auto Scroll Speed" = "Velocidad de desplazamiento automático";
|
||||
"Speed" = "Velocidad";
|
||||
"Speed: %.1fx" = "Velocidad: %.1fx";
|
||||
"Matched %@: %@" = "Coincidencia %@: %@";
|
||||
"Enter the AniList ID for this series" = "Introduce el ID de AniList para esta serie";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Crear colección";
|
||||
"Collection Name" = "Nombre de la colección";
|
||||
"Rename Collection" = "Renombrar colección";
|
||||
"Rename" = "Renombrar";
|
||||
"All Reading" = "Todas las lecturas";
|
||||
"Recently Added" = "Añadido recientemente";
|
||||
"Novel Title" = "Título de la novela";
|
||||
"Read Progress" = "Progreso de lectura";
|
||||
"Date Created" = "Fecha de creación";
|
||||
"Name" = "Nombre";
|
||||
"Item Count" = "Cantidad de elementos";
|
||||
"Date Added" = "Fecha de añadido";
|
||||
"Title" = "Título";
|
||||
"Source" = "Fuente";
|
||||
"Search reading..." = "Buscar en lecturas...";
|
||||
"Search collections..." = "Buscar en colecciones...";
|
||||
"Search bookmarks..." = "Buscar en marcadores...";
|
||||
"%d items" = "%d elementos";
|
||||
"Fetching Data" = "Obteniendo datos";
|
||||
"Please wait while fetching." = "Por favor, espere mientras se obtienen los datos.";
|
||||
"Start Reading" = "Comenzar a leer";
|
||||
"Chapters" = "Capítulos";
|
||||
"Completed" = "Completado";
|
||||
"Drag to reorder" = "Arrastrar para reordenar";
|
||||
"Drag to reorder sections" = "Arrastrar para reordenar secciones";
|
||||
"Library View" = "Vista de biblioteca";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personaliza las secciones que se muestran en tu biblioteca. Puedes reordenarlas o desactivarlas completamente.";
|
||||
"Library Sections Order" = "Orden de secciones de la biblioteca";
|
||||
"Completion Percentage" = "Porcentaje de finalización";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Algunas funciones están limitadas al reproductor Sora y al predeterminado, como el modo horizontal forzado, la velocidad de retención y los saltos de tiempo personalizados.\n\nEl ajuste del porcentaje de finalización determina en qué punto antes del final de un vídeo la app lo marcará como completado en AniList y Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "La caché de la app ayuda a cargar imágenes más rápido.\n\nBorrar la carpeta Documentos eliminará todos los módulos descargados.\n\nBorrar los datos de la app eliminará todos tus ajustes y datos.";
|
||||
"Translators" = "Traductores";
|
||||
"Paste URL" = "Pegar URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Título de la serie";
|
||||
"Content Source" = "Fuente de contenido";
|
||||
"Watch Progress" = "Progreso de visualización";
|
||||
"All Reading" = "Todo lo que lees";
|
||||
"Nothing to Continue Reading" = "Nada para continuar leyendo";
|
||||
"Your recently read novels will appear here" = "Tus novelas leídas recientemente aparecerán aquí";
|
||||
"No Bookmarks" = "Sin marcadores";
|
||||
"Add bookmarks to this collection" = "Agrega marcadores a esta colección";
|
||||
"items" = "elementos";
|
||||
"All Watching" = "Todo lo que ves";
|
||||
"No Reading History" = "Sin historial de lectura";
|
||||
"Books you're reading will appear here" = "Los libros que estás leyendo aparecerán aquí";
|
||||
"Create Collection" = "Crear colección";
|
||||
"Collection Name" = "Nombre de la colección";
|
||||
"Rename Collection" = "Renombrar colección";
|
||||
"Rename" = "Renombrar";
|
||||
"Novel Title" = "Título de la novela";
|
||||
"Read Progress" = "Progreso de lectura";
|
||||
"Date Created" = "Fecha de creación";
|
||||
"Name" = "Nombre";
|
||||
"Item Count" = "Cantidad de elementos";
|
||||
"Date Added" = "Fecha de agregado";
|
||||
"Title" = "Título";
|
||||
"Source" = "Fuente";
|
||||
"Search reading..." = "Buscar en lecturas...";
|
||||
"Search collections..." = "Buscar en colecciones...";
|
||||
"Search bookmarks..." = "Buscar en marcadores...";
|
||||
"%d items" = "%d elementos";
|
||||
"Fetching Data" = "Obteniendo datos";
|
||||
"Please wait while fetching." = "Por favor, espera mientras se obtienen los datos.";
|
||||
"Start Reading" = "Comenzar a leer";
|
||||
"Chapters" = "Capítulos";
|
||||
"Completed" = "Completado";
|
||||
"Drag to reorder" = "Arrastra para reordenar";
|
||||
"Drag to reorder sections" = "Arrastra para reordenar secciones";
|
||||
"Library View" = "Vista de biblioteca";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personaliza las secciones que se muestran en tu biblioteca. Puedes reordenar secciones o deshabilitarlas completamente.";
|
||||
"Library Sections Order" = "Orden de secciones de la biblioteca";
|
||||
"Completion Percentage" = "Porcentaje de finalización";
|
||||
"Translators" = "Traductores";
|
||||
"Paste URL" = "Pegar URL";
|
||||
|
||||
"Collections" = "Colecciones";
|
||||
"Continue Reading" = "Continuar leyendo";
|
||||
|
||||
"Backup & Restore" = "Copia de seguridad y restaurar";
|
||||
"Export Backup" = "Exportar copia de seguridad";
|
||||
"Import Backup" = "Importar copia de seguridad";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Aviso: Esta función aún es experimental. Por favor, verifica tus datos después de exportar/importar.";
|
||||
"Backup" = "Copia de seguridad";
|
||||
|
||||
494
Sora/Localization/fr.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
/* General */
|
||||
"About" = "À propos";
|
||||
"About Sora" = "À propos de Sora";
|
||||
"Active" = "Actif";
|
||||
"Active Downloads" = "Téléchargements actifs";
|
||||
"Actively downloading media can be tracked from here." = "Les médias en cours de téléchargement peuvent être suivis ici.";
|
||||
"Add Module" = "Ajouter un module";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Ajustez le nombre d'éléments multimédias par ligne en mode portrait et paysage.";
|
||||
"Advanced" = "Avancé";
|
||||
"AKA Sulfur" = "Aussi connu sous le nom de Sulfur";
|
||||
"All Bookmarks" = "Tous les favoris";
|
||||
"All Watching" = "Tout ce que je regarde";
|
||||
"Also known as Sulfur" = "Aussi connu sous le nom de Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "ID AniList";
|
||||
"AniList Match" = "Correspondance AniList";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Des données anonymes sont collectées pour améliorer l'application. Aucune information personnelle n'est collectée. Ceci peut être désactivé à tout moment.";
|
||||
"App Info" = "Infos sur l'application";
|
||||
"App Language" = "Langue de l'application";
|
||||
"App Storage" = "Stockage de l'application";
|
||||
"Appearance" = "Apparence";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Voulez-vous vraiment vider toutes les données en cache ? Cela aidera à libérer de l'espace de stockage.";
|
||||
"Are you sure you want to delete '%@'?" = "Voulez-vous vraiment supprimer '%@' ?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Voulez-vous vraiment supprimer les %1$d épisodes de '%2$@' ?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Voulez-vous vraiment supprimer tous les fichiers téléchargés ? Vous pouvez choisir de vider uniquement la bibliothèque tout en conservant les fichiers téléchargés pour une utilisation future.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Voulez-vous vraiment effacer toutes les données de l'application ? Cette action est irréversible.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Activé en arrière-plan";
|
||||
"Bookmark items for an easier access later." = "Mettez des éléments en favoris pour un accès plus facile plus tard.";
|
||||
"Bookmarks" = "Favoris";
|
||||
"Bottom Padding" = "Marge intérieure inférieure";
|
||||
"Cancel" = "Annuler";
|
||||
"Cellular Quality" = "Qualité cellulaire";
|
||||
"Check out some community modules here!" = "Découvrez quelques modules communautaires ici !";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Choisissez la résolution vidéo préférée pour les connexions WiFi et cellulaires. Les résolutions plus élevées utilisent plus de données mais offrent une meilleure qualité. Si la qualité exacte n'est pas disponible, l'option la plus proche sera sélectionnée automatiquement.\n\nRemarque : toutes les sources vidéo et tous les lecteurs ne prennent pas en charge la sélection de la qualité. Cette fonctionnalité fonctionne mieux avec les flux HLS utilisant le lecteur Sora.";
|
||||
"Clear" = "Vider";
|
||||
"Clear All Downloads" = "Vider tous les téléchargements";
|
||||
"Clear Cache" = "Vider le cache";
|
||||
"Clear Library Only" = "Vider la bibliothèque uniquement";
|
||||
"Clear Logs" = "Vider les journaux";
|
||||
"Click the plus button to add a module!" = "Cliquez sur le bouton plus pour ajouter un module !";
|
||||
"Continue Watching" = "Reprendre la lecture";
|
||||
"Continue Watching Episode %d" = "Reprendre l'épisode %d";
|
||||
"Contributors" = "Contributeurs";
|
||||
"Copied to Clipboard" = "Copié dans le presse-papiers";
|
||||
"Copy to Clipboard" = "Copier dans le presse-papiers";
|
||||
"Copy URL" = "Copier l'URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld épisodes";
|
||||
"%lld of %lld" = "%1$lld sur %2$lld";
|
||||
"%lld-%lld" = "%1$lld-%2$lld";
|
||||
"%lld%% seen" = "%lld%% vus";
|
||||
"Episode %lld" = "Épisode %lld";
|
||||
"Episodes" = "Épisodes";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Les épisodes ne sont peut-être pas encore disponibles ou il y a un problème avec la source.";
|
||||
"Episodes Range" = "Plage d'épisodes";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Sombre";
|
||||
"DATA & LOGS" = "DONNÉES & JOURNEAUX";
|
||||
"Debug" = "Débogage";
|
||||
"Debugging and troubleshooting." = "Débogage et résolution de problèmes.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Supprimer";
|
||||
"Delete All" = "Tout supprimer";
|
||||
"Delete All Downloads" = "Supprimer tous les téléchargements";
|
||||
"Delete All Episodes" = "Supprimer tous les épisodes";
|
||||
"Delete Download" = "Supprimer le téléchargement";
|
||||
"Delete Episode" = "Supprimer l'épisode";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Touchez deux fois pour avancer";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Appuyer deux fois sur les côtés de l'écran permet de sauter avec le réglage de pression courte.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Télécharger";
|
||||
"Download Episode" = "Télécharger l'épisode";
|
||||
"Download Summary" = "Résumé du téléchargement";
|
||||
"Download This Episode" = "Télécharger cet épisode";
|
||||
"Downloaded" = "Téléchargé";
|
||||
"Downloaded Shows" = "Séries téléchargées";
|
||||
"Downloading" = "Téléchargement en cours";
|
||||
"Downloads" = "Téléchargements";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Activer les analyses";
|
||||
"Enable Subtitles" = "Activer les sous-titres";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Effacer";
|
||||
"Erase all App Data" = "Effacer toutes les données de l'app";
|
||||
"Erase App Data" = "Effacer les données de l'app";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Erreur";
|
||||
"Error Fetching Results" = "Erreur lors de la récupération des résultats";
|
||||
"Errors and critical issues." = "Erreurs et problèmes critiques.";
|
||||
"Failed to load contributors" = "Échec du chargement des contributeurs";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Récupérer les métadonnées de l'épisode";
|
||||
"Files Downloaded" = "Fichiers téléchargés";
|
||||
"Font Size" = "Taille de la police";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Forcer le mode paysage";
|
||||
"General" = "Général";
|
||||
"General events and activities." = "Événements et activités générales.";
|
||||
"General Preferences" = "Préférences générales";
|
||||
"Hide Splash Screen" = "Masquer l'écran de démarrage";
|
||||
"HLS video downloading." = "Téléchargement de vidéos HLS.";
|
||||
"Hold Speed" = "Vitesse de maintien";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Infos";
|
||||
"INFOS" = "INFOS";
|
||||
"Installed Modules" = "Modules installés";
|
||||
"Interface" = "Interface";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Rejoindre le Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Colonnes en paysage";
|
||||
"Language" = "Langue";
|
||||
"LESS" = "MOINS";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Bibliothèque";
|
||||
"License (GPLv3.0)" = "Licence (GPLv3.0)";
|
||||
"Light" = "Clair";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Chargement de l'épisode %lld...";
|
||||
"Loading logs..." = "Chargement des journaux...";
|
||||
"Loading module information..." = "Chargement des informations du module...";
|
||||
"Loading Stream" = "Chargement du flux";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Journaliser les infos de débogage";
|
||||
"Log Filters" = "Filtres de journal";
|
||||
"Log In with AniList" = "Se connecter avec AniList";
|
||||
"Log In with Trakt" = "Se connecter avec Trakt";
|
||||
"Log Out from AniList" = "Se déconnecter d'AniList";
|
||||
"Log Out from Trakt" = "Se déconnecter de Trakt";
|
||||
"Log Types" = "Types de journaux";
|
||||
"Logged in as" = "Connecté en tant que";
|
||||
"Logged in as " = "Connecté en tant que ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Journaux";
|
||||
"Long press Skip" = "Pression longue pour sauter";
|
||||
"MAIN" = "PRINCIPAL";
|
||||
"Main Developer" = "Développeur principal";
|
||||
"MAIN SETTINGS" = "RÉGLAGES PRINCIPAUX";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Marquer tout comme vu";
|
||||
"Mark as Watched" = "Marquer comme vu";
|
||||
"Mark Episode as Watched" = "Marquer l'épisode comme vu";
|
||||
"Mark Previous Episodes as Watched" = "Marquer les épisodes précédents comme vus";
|
||||
"Mark watched" = "Marquer comme vu";
|
||||
"Match with AniList" = "Associer avec AniList";
|
||||
"Match with TMDB" = "Associer avec TMDB";
|
||||
"Matched ID: %lld" = "ID associé : %lld";
|
||||
"Matched with: %@" = "Associé avec : %@";
|
||||
"Max Concurrent Downloads" = "Téléchargements simultanés max";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Mise en page de la grille média";
|
||||
"Media Player" = "Lecteur multimédia";
|
||||
"Media View" = "Vue multimédia";
|
||||
"Metadata Provider" = "Fournisseur de métadonnées";
|
||||
"Metadata Providers Order" = "Ordre des fournisseurs de métadonnées";
|
||||
"Module Removed" = "Module supprimé";
|
||||
"Modules" = "Modules";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULES";
|
||||
"MORE" = "PLUS";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Aucun téléchargement actif";
|
||||
"No AniList matches found" = "Aucune correspondance AniList trouvée";
|
||||
"No Data Available" = "Aucune donnée disponible";
|
||||
"No Downloads" = "Aucun téléchargement";
|
||||
"No episodes available" = "Aucun épisode disponible";
|
||||
"No Episodes Available" = "Aucun épisode disponible";
|
||||
"No items to continue watching." = "Aucun élément à reprendre.";
|
||||
"No matches found" = "Aucune correspondance trouvée";
|
||||
"No Module Selected" = "Aucun module sélectionné";
|
||||
"No Modules" = "Aucun module";
|
||||
"No Results Found" = "Aucun résultat trouvé";
|
||||
"No Search Results Found" = "Aucun résultat de recherche trouvé";
|
||||
"Nothing to Continue Watching" = "Rien à reprendre";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Notez que les modules ne seront remplacés que s'il existe une chaîne de version différente dans le fichier JSON.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Ouvrir la bibliothèque communautaire";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Ouvrir dans AniList";
|
||||
"Original Poster" = "Affiche originale";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "En pause";
|
||||
"Play" = "Lecture";
|
||||
"Player" = "Lecteur";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Veuillez redémarrer l'application pour appliquer le changement de langue.";
|
||||
"Please select a module from settings" = "Veuillez sélectionner un module dans les paramètres";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Colonnes en portrait";
|
||||
"Progress bar Marker Color" = "Couleur du marqueur de la barre de progression";
|
||||
"Provider: %@" = "Fournisseur : %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "File d'attente";
|
||||
"Queued" = "En attente";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Le contenu récemment visionné apparaîtra ici.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Actualiser les modules au lancement";
|
||||
"Refresh Storage Info" = "Actualiser les infos de stockage";
|
||||
"Remember Playback speed" = "Mémoriser la vitesse de lecture";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Supprimer";
|
||||
"Remove All Cache" = "Supprimer tout le cache";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Supprimer tous les documents";
|
||||
"Remove Documents" = "Supprimer les documents";
|
||||
"Remove Downloaded Media" = "Supprimer les médias téléchargés";
|
||||
"Remove Downloads" = "Supprimer les téléchargements";
|
||||
"Remove from Bookmarks" = "Supprimer des favoris";
|
||||
"Remove Item" = "Supprimer l'élément";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Signaler un problème";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Réinitialiser";
|
||||
"Reset AniList ID" = "Réinitialiser l'ID AniList";
|
||||
"Reset Episode Progress" = "Réinitialiser la progression de l'épisode";
|
||||
"Reset progress" = "Réinitialiser la progression";
|
||||
"Reset Progress" = "Réinitialiser la progression";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Redémarrage requis";
|
||||
"Running Sora %@ - cranci1" = "Sora %@ en cours d'exécution - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Enregistrer";
|
||||
"Search" = "Rechercher";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Rechercher dans les téléchargements";
|
||||
"Search for something..." = "Rechercher quelque chose...";
|
||||
"Search..." = "Rechercher...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Saison %d";
|
||||
"Season %lld" = "Saison %lld";
|
||||
"Segments Color" = "Couleur des segments";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Sélectionner un module";
|
||||
"Set Custom AniList ID" = "Définir un ID AniList personnalisé";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Paramètres";
|
||||
"Shadow" = "Ombre";
|
||||
"Show More (%lld more characters)" = "Afficher plus (%lld caractères de plus)";
|
||||
"Show PiP Button" = "Afficher le bouton PiP";
|
||||
"Show Skip 85s Button" = "Afficher le bouton Sauter 85s";
|
||||
"Show Skip Intro / Outro Buttons" = "Afficher les boutons Sauter l'intro / l'outro";
|
||||
"Shows" = "Séries";
|
||||
"Size (%@)" = "Taille (%@)";
|
||||
"Skip Settings" = "Réglages de saut";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Certaines fonctionnalités sont limitées au lecteur Sora et par défaut, telles que Forcer Paysage, Vitesse par pression longue et les incréments de saut de temps personnalisés.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ par cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora et cranci1 ne sont en aucun cas affiliés à AniList ou Trakt.\n\nNotez également que les mises à jour de progression peuvent ne pas être précises à 100%.";
|
||||
"Sora GitHub Repository" = "Dépôt GitHub de Sora";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur restera toujours gratuit et sans publicités !";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Trier";
|
||||
"Speed Settings" = "Réglages de vitesse";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Commencer à regarder";
|
||||
"Start Watching Episode %d" = "Commencer l'épisode %d";
|
||||
"Storage Used" = "Stockage utilisé";
|
||||
"Stream" = "Flux";
|
||||
"Streaming and video playback." = "Streaming et lecture vidéo.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Couleur des sous-titres";
|
||||
"Subtitle Settings" = "Réglages des sous-titres";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Synchroniser la progression des animes";
|
||||
"Sync TV shows progress" = "Synchroniser la progression des séries TV";
|
||||
|
||||
/* System */
|
||||
"System" = "Système";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Appuyez sur un titre pour remplacer la correspondance actuelle.";
|
||||
"Tap Skip" = "Appuyer pour sauter";
|
||||
"Tap to manage your modules" = "Appuyez pour gérer vos modules";
|
||||
"Tap to select a module" = "Appuyez pour sélectionner un module";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Le cache de l'application permet de charger les images plus rapidement.\n\nLa suppression du dossier Documents effacera tous les modules téléchargés.\n\nN'effacez pas les données de l'application à moins de comprendre les conséquences — cela pourrait entraîner un dysfonctionnement de l'application.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "La plage d'épisodes contrôle le nombre d'épisodes qui apparaissent sur chaque page. Les épisodes sont regroupés en séries (comme 1–25, 26–50, etc.), ce qui vous permet de naviguer plus facilement.\n\nPour les métadonnées d'épisode, cela fait référence à la miniature et au titre de l'épisode, car ils peuvent parfois contenir des spoilers.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Le module n'a fourni qu'un seul épisode, il s'agit très probablement d'un film, nous avons donc décidé de créer des écrans séparés pour ces cas.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Largeur des miniatures";
|
||||
"TMDB Match" = "Correspondance TMDB";
|
||||
"Trackers" = "Trackers";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Essayez différents mots-clés";
|
||||
"Try different search terms" = "Essayez différents termes de recherche";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Maintenir avec deux doigts pour mettre en pause";
|
||||
"Unable to fetch matches. Please try again later." = "Impossible de récupérer les correspondances. Veuillez réessayer plus tard.";
|
||||
"Use TMDB Poster Image" = "Utiliser l'image de l'affiche TMDB";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Lecteur vidéo";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Préférences de qualité vidéo";
|
||||
"View All" = "Voir tout";
|
||||
"Watched" = "Vu";
|
||||
"Why am I not seeing any episodes?" = "Pourquoi ne vois-je aucun épisode ?";
|
||||
"WiFi Quality" = "Qualité WiFi";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Vous n'êtes pas connecté";
|
||||
"You have no items saved." = "Vous n'avez aucun élément enregistré.";
|
||||
"Your downloaded episodes will appear here" = "Vos épisodes téléchargés apparaîtront ici";
|
||||
"Your recently watched content will appear here" = "Votre contenu récemment visionné apparaîtra ici";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Paramètres de téléchargement";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Le nombre maximum de téléchargements simultanés contrôle le nombre d'épisodes pouvant être téléchargés en même temps. Des valeurs plus élevées peuvent utiliser plus de bande passante et de ressources de l'appareil.";
|
||||
"Quality" = "Qualité";
|
||||
"Max Concurrent Downloads" = "Téléchargements simultanés max";
|
||||
"Allow Cellular Downloads" = "Autoriser les téléchargements cellulaires";
|
||||
"Quality Information" = "Informations sur la qualité";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Gestion du stockage";
|
||||
"Storage Used" = "Stockage utilisé";
|
||||
"Library cleared successfully" = "Bibliothèque vidée avec succès";
|
||||
"All downloads deleted successfully" = "Tous les téléchargements ont été supprimés avec succès";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Recherches récentes";
|
||||
"me frfr" = "moi frfr";
|
||||
"Data" = "Données";
|
||||
"Maximum Quality Available" = "Qualité maximale disponible";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d sur %d";
|
||||
"Error loading chapter" = "Erreur lors du chargement du chapitre";
|
||||
"Font Size: %dpt" = "Taille de police : %dpt";
|
||||
"Line Spacing: %.1f" = "Interligne : %.1f";
|
||||
"Line Spacing" = "Interligne";
|
||||
"Margin: %dpx" = "Marge : %dpx";
|
||||
"Margin" = "Marge";
|
||||
"Auto Scroll Speed" = "Vitesse de défilement automatique";
|
||||
"Speed" = "Vitesse";
|
||||
"Speed: %.1fx" = "Vitesse : %.1fx";
|
||||
"Matched %@: %@" = "Correspondance %@ : %@";
|
||||
"Enter the AniList ID for this series" = "Entrez l'ID AniList pour cette série";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Créer une collection";
|
||||
"Collection Name" = "Nom de la collection";
|
||||
"Rename Collection" = "Renommer la collection";
|
||||
"Rename" = "Renommer";
|
||||
"All Reading" = "Toutes les lectures";
|
||||
"Recently Added" = "Ajouté récemment";
|
||||
"Novel Title" = "Titre du roman";
|
||||
"Read Progress" = "Progression de la lecture";
|
||||
"Date Created" = "Date de création";
|
||||
"Name" = "Nom";
|
||||
"Item Count" = "Nombre d'éléments";
|
||||
"Date Added" = "Date d'ajout";
|
||||
"Title" = "Titre";
|
||||
"Source" = "Source";
|
||||
"Search reading..." = "Rechercher dans les lectures...";
|
||||
"Search collections..." = "Rechercher dans les collections...";
|
||||
"Search bookmarks..." = "Rechercher dans les favoris...";
|
||||
"%d items" = "%d éléments";
|
||||
"Fetching Data" = "Récupération des données";
|
||||
"Please wait while fetching." = "Veuillez patienter pendant la récupération.";
|
||||
"Start Reading" = "Commencer la lecture";
|
||||
"Chapters" = "Chapitres";
|
||||
"Completed" = "Terminé";
|
||||
"Drag to reorder" = "Glisser pour réorganiser";
|
||||
"Drag to reorder sections" = "Glisser pour réorganiser les sections";
|
||||
"Library View" = "Vue de la bibliothèque";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personnalisez les sections affichées dans votre bibliothèque. Vous pouvez réorganiser ou désactiver complètement les sections.";
|
||||
"Library Sections Order" = "Ordre des sections de la bibliothèque";
|
||||
"Completion Percentage" = "Pourcentage d'achèvement";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Certaines fonctionnalités sont limitées au lecteur Sora et au lecteur par défaut, comme le mode paysage forcé, la vitesse de maintien et les sauts de temps personnalisés.\n\nLe réglage du pourcentage d'achèvement détermine à quel moment avant la fin d'une vidéo l'application la marquera comme terminée sur AniList et Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Le cache de l'application aide à charger les images plus rapidement.\n\nVider le dossier Documents supprimera tous les modules téléchargés.\n\nEffacer les données de l'application supprimera tous vos paramètres et données.";
|
||||
"Translators" = "Traducteurs";
|
||||
"Paste URL" = "Coller l'URL";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "Titre de la série";
|
||||
"Content Source" = "Source du contenu";
|
||||
"Watch Progress" = "Progression de visionnage";
|
||||
"Recent searches" = "Recherches récentes";
|
||||
"All Reading" = "Tout ce que vous lisez";
|
||||
"Nothing to Continue Reading" = "Rien à continuer à lire";
|
||||
"Your recently read novels will appear here" = "Vos romans récemment lus apparaîtront ici";
|
||||
"No Bookmarks" = "Aucun favori";
|
||||
"Add bookmarks to this collection" = "Ajoutez des favoris à cette collection";
|
||||
"items" = "éléments";
|
||||
"All Watching" = "Tout ce que vous regardez";
|
||||
"No Reading History" = "Aucun historique de lecture";
|
||||
"Books you're reading will appear here" = "Les livres que vous lisez apparaîtront ici";
|
||||
"Create Collection" = "Créer une collection";
|
||||
"Collection Name" = "Nom de la collection";
|
||||
"Rename Collection" = "Renommer la collection";
|
||||
"Rename" = "Renommer";
|
||||
"Novel Title" = "Titre du roman";
|
||||
"Read Progress" = "Progression de lecture";
|
||||
"Date Created" = "Date de création";
|
||||
"Name" = "Nom";
|
||||
"Item Count" = "Nombre d'éléments";
|
||||
"Date Added" = "Date d'ajout";
|
||||
"Title" = "Titre";
|
||||
"Source" = "Source";
|
||||
"Search reading..." = "Rechercher dans les lectures...";
|
||||
"Search collections..." = "Rechercher dans les collections...";
|
||||
"Search bookmarks..." = "Rechercher dans les favoris...";
|
||||
"%d items" = "%d éléments";
|
||||
"Fetching Data" = "Récupération des données";
|
||||
"Please wait while fetching." = "Veuillez patienter pendant la récupération.";
|
||||
"Start Reading" = "Commencer la lecture";
|
||||
"Chapters" = "Chapitres";
|
||||
"Completed" = "Terminé";
|
||||
"Drag to reorder" = "Faites glisser pour réorganiser";
|
||||
"Drag to reorder sections" = "Faites glisser pour réorganiser les sections";
|
||||
"Library View" = "Vue de la bibliothèque";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personnalisez les sections affichées dans votre bibliothèque. Vous pouvez réorganiser les sections ou les désactiver complètement.";
|
||||
"Library Sections Order" = "Ordre des sections de la bibliothèque";
|
||||
"Completion Percentage" = "Pourcentage d'achèvement";
|
||||
"Translators" = "Traducteurs";
|
||||
"Paste URL" = "Coller l'URL";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "Collections";
|
||||
"Continue Reading" = "Continuer la lecture";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Sauvegarde & Restauration";
|
||||
"Export Backup" = "Exporter la sauvegarde";
|
||||
"Import Backup" = "Importer la sauvegarde";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Remarque : Cette fonctionnalité est encore expérimentale. Veuillez vérifier vos données après l'export/import.";
|
||||
"Backup" = "Sauvegarde";
|
||||
425
Sora/Localization/it.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
/* General */
|
||||
"About" = "informazioni";
|
||||
"About Sora" = "Informazioni su Sora";
|
||||
"Active" = "Attivi";
|
||||
"Active Downloads" = "Download Attivi";
|
||||
"Actively downloading media can be tracked from here." = "I Download Attivi si possono trovare qui";
|
||||
"Add Module" = "Aggiungi Modulo";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Imposta il numero dei media per riga in vista orizzontale e in vista verticale";
|
||||
"Advanced" = "Avanzate";
|
||||
"AKA Sulfur" = "Aka Sulfur";
|
||||
"All Bookmarks" = "Tutti i Preferiti";
|
||||
"All Watching" = "Tutti i Guardati";
|
||||
"Also known as Sulfur" = "Conosciuta anche come Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ID";
|
||||
"AniList Match" = "AniList Match";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Vengono raccolti dati anonimi per il miglioramento dell'App, Nessun Dato Personale viene Registrato, Questa opzione può essere disabilitata in ogni momento";
|
||||
"App Info" = "Informazioni App";
|
||||
"App Language" = "Lingua App";
|
||||
"App Storage" = "Memoria Utilizzata dall'app";
|
||||
"Appearance" = "Aspetto";
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Sei sicuro di voler rimuovere tutti i dati presenti nella cache? Questo aiuterà a liberare spazio";
|
||||
"Are you sure you want to delete '%@'?" = "Sei sicuro di volere rimuovere '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Sei sicuro di voler eliminare tutti %1$d episodi in '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Siete sicuri di voler eliminare tutte le risorse scaricate? È possibile scegliere di cancellare solo la libreria, conservando i file scaricati per un uso futuro.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Sei sicuro di voler eliminare tutti i dati dell'app, questa azione non può essere cancellata";
|
||||
/* Features */
|
||||
"Background Enabled" = "Background Abilitato";
|
||||
"Bookmark items for an easier access later." = "Imposta Preferiti per un accesso Più veloce dopo";
|
||||
"Bookmarks" = "Preferiti";
|
||||
"Bottom Padding" = "Distanza Sottotitoli";
|
||||
"Cancel" = "Cancella";
|
||||
"Cellular Quality" = "Qualità Dati Cellulare";
|
||||
"Check out some community modules here!" = "Cerca i moduli della community qui!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Seleziona la Risoluzione video preferita per WiFi e Dati Mobili. Una Risoluzione più alta consuma più dati ma fornisce una qualità migliore. Se la stessa qualità non é disponibile la soluzione più vicina verrà scelta automaticamente \n\nNota:Non tutte le fonti e player supportano la scelta della qualità video.Questa Feature funziona meglio usando il Player integrato di Sora";
|
||||
"Clear" = "Pulisci";
|
||||
"Clear All Downloads" = "Pulisci Tutti i Download";
|
||||
"Clear Cache" = "Pulisci Cache";
|
||||
"Clear Library Only" = "Pulisci Solo La Libreria";
|
||||
"Clear Logs" = "Pulisci Log";
|
||||
"Click the plus button to add a module!" = "Premi il più per aggiungere un modulo";
|
||||
"Continue Watching" = "Continua a Guardare";
|
||||
"Continue Watching Episode %d" = "Continua a guardare %d";
|
||||
"Contributors" = "Collaboratori";
|
||||
"Copied to Clipboard" = "Copiato Negli Appunti";
|
||||
"Copy to Clipboard" = "Copia negli Appunti";
|
||||
"Copy URL" = "Copia URL";
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld Episodi";
|
||||
"%lld of %lld" = "%lld di %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% visti";
|
||||
"Episode %lld" = "Episodio %lld";
|
||||
"Episodes" = "Episodi";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Gli episodi potrebbero non essere ancora disponibili o potrebbe esserci un errore con la fonte";
|
||||
"Episodes Range" = "Range Episodi";
|
||||
/* System */
|
||||
"cranci1" = "cranc1";
|
||||
"Dark" = "Dark";
|
||||
"DATA & LOGS" = "DATA & LOGS";
|
||||
"Debug" = "Debug";
|
||||
"Debugging and troubleshooting." = "Debugging e Risoluzione Problemi";
|
||||
/* Actions */
|
||||
"Delete" = "Elimina";
|
||||
"Delete All" = "Elimina Tutto";
|
||||
"Delete All Downloads" = "Elimina Tutti I Download";
|
||||
"Delete All Episodes" = "Elimina Tutti Gli Episodi";
|
||||
"Delete Download" = "Elimina Download";
|
||||
"Delete Episode" = "Elimina Episodio";
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Doppio Tap Per (Seek)";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Se l'opzione per saltare é abilitata, con questo premendo nei lati due volte é possibile skippare";
|
||||
/* Downloads */
|
||||
"Download" = "Scarica";
|
||||
"Download Episode" = "Scarica Episodio";
|
||||
"Download Summary" = "Scarica Riassunto";
|
||||
"Download This Episode" = "Scarica questo Episodio";
|
||||
"Downloaded" = "Scaricato";
|
||||
"Downloaded Shows" = "Serie Scaricate";
|
||||
"Downloading" = "Download in Corso";
|
||||
"Downloads" = "Scaricati";
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Attiva Analytics";
|
||||
"Enable Subtitles" = "Attiva Sottotitoli";
|
||||
/* Data Management */
|
||||
"Erase" = "Cancella";
|
||||
"Erase all App Data" = "Cancella Tutti i Dati dell'App";
|
||||
"Erase App Data" = "Cancella Dati App";
|
||||
/* Errors */
|
||||
"Error" = "Errore";
|
||||
"Error Fetching Results" = "Errore nella ricerca dei risultati";
|
||||
"Errors and critical issues." = "Errori e Problemi Critici";
|
||||
"Failed to load contributors" = "impossibile caricare i collaboratori";
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Cercando i Metadata";
|
||||
"Files Downloaded" = "File Scaricati";
|
||||
"Font Size" = "Dimensione Carattere";
|
||||
/* Interface */
|
||||
"Force Landscape" = "Forza Vista Orizzontale";
|
||||
"General" = "Generali";
|
||||
"General events and activities." = "Eventi e Attività Generali";
|
||||
"General Preferences" = "Preferenze Generali";
|
||||
"Hide Splash Screen" = "Nascondi Splash Screen";
|
||||
"HLS video downloading." = "Scaricamento Video HLS";
|
||||
"Hold Speed" = "Mantieni per Velocizzare";
|
||||
/* Info */
|
||||
"Info" = "Info";
|
||||
"INFOS" = "INFORMAZIONI";
|
||||
"Installed Modules" = "Moduli Installati";
|
||||
"Interface" = "Interfaccia";
|
||||
/* Social */
|
||||
"Join the Discord" = "Entra Nel Nostro Discord!";
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Colonne in Vista Orizzontale";
|
||||
"Language" = "Lingua";
|
||||
"LESS" = "Mostra Meno";
|
||||
/* Library */
|
||||
"Library" = "Libreria";
|
||||
"License (GPLv3.0)" = "Licenza (GPLv3.0)";
|
||||
"Light" = "Tema Chiaro";
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Caricando Gli Episodi %lld...";
|
||||
"Loading logs..." = "Caricando i Logs";
|
||||
"Loading module information..." = "Caricando le Informazioni del modulo";
|
||||
"Loading Stream" = "Caricando lo Stream";
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Info Log Debug";
|
||||
"Log Filters" = "Filtri Log";
|
||||
"Log In with AniList" = "Accedi a AniList";
|
||||
"Log In with Trakt" = "Accedi a Trakt";
|
||||
"Log Out from AniList" = "Esci da AniList";
|
||||
"Log Out from Trakt" = "Esci da Trakt";
|
||||
"Log Types" = "Tipo Log";
|
||||
"Logged in as" = "Accesso Effettuato come";
|
||||
"Logged in as " = "Accesso Effettuato come";
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logs";
|
||||
"Long press Skip" = "Tener premuto per Skippare";
|
||||
"MAIN" = "PRINCIPALE";
|
||||
"Main Developer" = "Sviluppatore Principale";
|
||||
"MAIN SETTINGS" = "Impostazioni principali";
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Segna Tutti I Precedenti Come visti";
|
||||
"Mark as Watched" = "Segna Come Visto";
|
||||
"Mark Episode as Watched" = "Segna Episodio come Visto";
|
||||
"Mark Previous Episodes as Watched" = "Segna episodi precedenti come visti";
|
||||
"Mark watched" = "Segna Come Completato";
|
||||
"Match with AniList" = "Match with AniList";
|
||||
"Match with TMDB" = "Match with TMDB";
|
||||
"Matched ID: %lld" = "Matched ID: %lld";
|
||||
"Matched with: %@" = "Matched with:%@";
|
||||
"Max Concurrent Downloads" = "Download massimi in Contemporanea";
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Layout della Griglia dei Media";
|
||||
"Media Player" = "Media Player";
|
||||
"Media View" = "Vista media";
|
||||
"Metadata Provider" = "Provider Metadata";
|
||||
"Metadata Providers Order" = "Ordine dei Provider Metadata";
|
||||
"Module Removed" = "Modulo Rimosso";
|
||||
"Modules" = "Moduli";
|
||||
/* Headers */
|
||||
"MODULES" = "MODULI";
|
||||
"MORE" = "PIÙ";
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Nessun Download Attivo";
|
||||
"No AniList matches found" = "Nessun";
|
||||
"No Data Available" = "Nessun Dato Disponibile";
|
||||
"No Downloads" = "Nessun Download";
|
||||
"No episodes available" = "Nessun Episodio Disponibile";
|
||||
"No Episodes Available" = "Nessun Episodio Disponibile";
|
||||
"No items to continue watching." = "Nessun Contenuto da continuare a guardare";
|
||||
"No matches found" = "Nessun Elemento Trovato";
|
||||
"No Module Selected" = "Nessun Modulo Selezionato";
|
||||
"No Modules" = "Nessun Modulo";
|
||||
"No Results Found" = "Nessun Risultato Trovato";
|
||||
"No Search Results Found" = "Nessun risultato di ricerca trovato";
|
||||
"Nothing to Continue Watching" = "Nulla da Continuare a Guardare";
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Si noti che i moduli saranno sostituiti solo se c'è una stringa di versione diversa all'interno del file JSON";
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Apri Libreria Community";
|
||||
/* External Services */
|
||||
"Open in AniList" = "Apri in AniList";
|
||||
"Original Poster" = "Poster Originale";
|
||||
/* Playback */
|
||||
"Paused" = "Pausa";
|
||||
"Play" = "Play";
|
||||
"Player" = "Player";
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Riavvia l'app per effettuare i cambiamenti";
|
||||
"Please select a module from settings" = "Seleziona un Modulo dalle impostazioni";
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Colonne in Vista Verticale";
|
||||
"Progress bar Marker Color" = "Colore Barra di Progresso";
|
||||
"Provider: %@" = "Provider: %@";
|
||||
/* Queue */
|
||||
"Queue" = "Coda";
|
||||
"Queued" = "In Coda";
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "I Contenuti Precedentemente Visti Appariranno qui";
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Aggiorna i Moduli al Lancio";
|
||||
"Refresh Storage Info" = "Aggiorna informazioni storage";
|
||||
"Remember Playback speed" = "Ricorda Velocità Playback";
|
||||
/* Actions */
|
||||
"Remove" = "Rimuovi";
|
||||
"Remove All Cache" = "Pulisci Cache";
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Rimuovi Tutti i Documenti";
|
||||
"Remove Documents" = "Rimuovi Documenti";
|
||||
"Remove Downloaded Media" = "Rimuovi Media Scaricati";
|
||||
"Remove Downloads" = "Rimuovi Scaricati";
|
||||
"Remove from Bookmarks" = "Rimuovi dai Preferiti";
|
||||
"Remove Item" = "Rimuovi Elemento";
|
||||
/* Support */
|
||||
"Report an Issue" = "Segnala un Problema";
|
||||
/* Reset Options */
|
||||
"Reset" = "Reimposta";
|
||||
"Reset AniList ID" = "Reimposta AniList ID";
|
||||
"Reset Episode Progress" = "Reimposta";
|
||||
"Reset progress" = "Reimposta Progressi";
|
||||
"Reset Progress" = "Reimposta Progressi";
|
||||
/* System */
|
||||
"Restart Required" = "Riavvio Richiesto";
|
||||
"Running Sora %@ - cranci1" = "Running Sora %@ - cranc1";
|
||||
/* Actions */
|
||||
"Save" = "Salva";
|
||||
"Search" = "Cerca";
|
||||
/* Search */
|
||||
"Search downloads" = "Cerca Download";
|
||||
"Search for something..." = "Cerca Qualcosa...";
|
||||
"Search..." = "Cerca...";
|
||||
/* Content */
|
||||
"Season %d" = "Stagione %d";
|
||||
"Season %lld" = "Stagione %lld";
|
||||
"Segments Color" = "Colore Segmenti";
|
||||
/* Modules */
|
||||
"Select Module" = "Seleziona Modulo";
|
||||
"Set Custom AniList ID" = "Imposta Custom AniList ID";
|
||||
/* Interface */
|
||||
"Settings" = "Impostazioni";
|
||||
"Shadow" = "Ombra";
|
||||
"Show More (%lld more characters)" = "Mostra Di Più (%lld più Caratteri)";
|
||||
"Show PiP Button" = "Mostra Bottone PiP";
|
||||
"Show Skip 85s Button" = "Mostra Skip 85s";
|
||||
"Show Skip Intro / Outro Buttons" = "Mostra Bottoni Skip Intro/Outro";
|
||||
"Shows" = "Serie";
|
||||
"Size (%@)" = "Dimensione (%@)";
|
||||
"Skip Settings" = "Salta Impostazioni";
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Qualche Features sono limitate a Sora e al Default Player, Come Forza Visualizzazione Orizzontale, Mantieni per Velocizzare e Skip di Tempo Custom";
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ by cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way. Also note that progress updates may not be 100% accurate." = "Sora e Cranc1 non sono affiliati in nessun modo con AniList o Trakt.";
|
||||
"Sora GitHub Repository" = "Repository GitHub di Sora";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur rimarrà sempre gratis e senza pubblicità";
|
||||
/* Interface */
|
||||
"Sort" = "Filtra";
|
||||
"Speed Settings" = "Impostazioni velocità";
|
||||
/* Playback */
|
||||
"Start Watching" = "Inizia a Guardare";
|
||||
"Start Watching Episode %d" = "Guarda Episodio %d";
|
||||
"Storage Used" = "Memoria Usata";
|
||||
"Stream" = "Stream";
|
||||
"Streaming and video playback." = "Streaming e VideoPlayBack";
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Colore Sottotitoli";
|
||||
"Subtitle Settings" = "Impostazioni Sottotitoli";
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Sincronizza Progressi Anime";
|
||||
"Sync TV shows progress" = "Sincronizza Progressi Serie Tv";
|
||||
/* System */
|
||||
"System" = "Sistema";
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Premi per Sovrascrivere il";
|
||||
"Tap Skip" = "Premi per Saltare";
|
||||
"Tap to manage your modules" = "Premi Per Gestire i Tuoi Moduli";
|
||||
"Tap to select a module" = "Premi per Selezionare un Modulo";
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster. Clearing the Documents folder will delete all downloaded modules. Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "La Cache dell'App serve a caricare le immagini più velocemente, pulire la cartella dei documenti eliminerà tutti i moduli scaricati. Non cancellare i Dati dell'App senza aver capito le conseguenze- potrebbe causare malfunzionamenti all'app";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily. For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Il range degli episodi controlla quanti episodi appaiono in ogni pagina.Gli episodi sono raggruppati in set (tipo 1-25,26-50 e così via), permettendo di accedere ad essi più facilmente";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "";
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Grandezza Miniature";
|
||||
"TMDB Match" = "TMDB Match";
|
||||
"Trackers" = "Trackers";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
/* Search */
|
||||
"Try different keywords" = "Prova diverse Parole Chiave";
|
||||
"Try different search terms" = "Prova diversi Termini di Ricerca";
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Mantieni con Due Dita Per Mettere in Pausa";
|
||||
"Unable to fetch matches. Please try again later." = "Impossibile trovare i match. Riprovare più tardi.";
|
||||
"Use TMDB Poster Image" = "Usa i poster di IMDB";
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Video Player";
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Preferenze qualità video";
|
||||
"View All" = "Vedi Tutti";
|
||||
"Watched" = "Guardati";
|
||||
"Why am I not seeing any episodes?" = "Perché non sto vedendo nessun episodio?";
|
||||
"WiFi Quality" = "Qualità WiFi";
|
||||
/* User Status */
|
||||
"You are not logged in" = "Non sei Loggato";
|
||||
"You have no items saved." = "Non hai Elementi Salvati";
|
||||
"Your downloaded episodes will appear here" = "I Tuoi Download appariranno Qui";
|
||||
"Your recently watched content will appear here" = "I Contenuti Guardati Recentemente Appariranno Qui";
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Impostazioni Download";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Il valore massimo di download contemporanei controlla il numero di episodi che possono essere scaricati simultaneamente. Valori più alti possono utilizzare più larghezza di banda e risorse del dispositivo.";
|
||||
"Quality" = "Qualità";
|
||||
"Max Concurrent Downloads" = "Download Massimi in Contemporanea";
|
||||
"Allow Cellular Downloads" = "Permetti il download con Connessione Dati";
|
||||
"Quality Information" = "Informazioni Qualità";
|
||||
/* Storage */
|
||||
"Storage Management" = "Gestione Memoria";
|
||||
"Storage Used" = "Memoria Usata";
|
||||
"Library cleared successfully" = "Libreria Pulita con Successo";
|
||||
"All downloads deleted successfully" = "Tutti i Download Sono Stati Eliminati Correttamente";
|
||||
/* New additions */
|
||||
"Recent searches" = "Ricerche Recenti";
|
||||
"me frfr" = "me frfr";
|
||||
"Data" = "Dati";
|
||||
"Maximum Quality Available" = "Qualità Massima Disponibile";
|
||||
"DownloadCountFormat" = "%d di %d";
|
||||
"Error loading chapter" = "Errore nel caricamento del capitolo";
|
||||
"Font Size: %dpt" = "Dimensione carattere: %dpt";
|
||||
"Line Spacing: %.1f" = "Interlinea: %.1f";
|
||||
"Line Spacing" = "Interlinea";
|
||||
"Margin: %dpx" = "Margine: %dpx";
|
||||
"Margin" = "Margine";
|
||||
"Auto Scroll Speed" = "Velocità di scorrimento automatico";
|
||||
"Speed" = "Velocità";
|
||||
"Speed: %.1fx" = "Velocità: %.1fx";
|
||||
"Matched %@: %@" = "Corrispondenza %@: %@";
|
||||
"Enter the AniList ID for this series" = "Inserisci l'ID AniList per questa serie";
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Crea raccolta";
|
||||
"Collection Name" = "Nome raccolta";
|
||||
"Rename Collection" = "Rinomina raccolta";
|
||||
"Rename" = "Rinomina";
|
||||
"All Reading" = "Tutte le letture";
|
||||
"Recently Added" = "Aggiunti di recente";
|
||||
"Novel Title" = "Titolo del romanzo";
|
||||
"Read Progress" = "Progresso lettura";
|
||||
"Date Created" = "Data di creazione";
|
||||
"Name" = "Nome";
|
||||
"Item Count" = "Numero di elementi";
|
||||
"Date Added" = "Data di aggiunta";
|
||||
"Title" = "Titolo";
|
||||
"Source" = "Fonte";
|
||||
"Search reading..." = "Cerca nelle letture...";
|
||||
"Search collections..." = "Cerca nelle raccolte...";
|
||||
"Search bookmarks..." = "Cerca nei segnalibri...";
|
||||
"%d items" = "%d elementi";
|
||||
"Fetching Data" = "Recupero dati";
|
||||
"Please wait while fetching." = "Attendere durante il recupero.";
|
||||
"Start Reading" = "Inizia a leggere";
|
||||
"Chapters" = "Capitoli";
|
||||
"Completed" = "Completato";
|
||||
"Drag to reorder" = "Trascina per riordinare";
|
||||
"Drag to reorder sections" = "Trascina per riordinare le sezioni";
|
||||
"Library View" = "Vista libreria";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personalizza le sezioni mostrate nella tua libreria. Puoi riordinare le sezioni o disabilitarle completamente.";
|
||||
"Library Sections Order" = "Ordine delle sezioni della libreria";
|
||||
"Completion Percentage" = "Percentuale di completamento";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Alcune funzioni sono limitate al player Sora e a quello predefinito, come la forzatura del paesaggio, la velocità di mantenimento e gli intervalli di salto personalizzati.\n\nL'impostazione della percentuale di completamento determina in quale punto prima della fine di un video l'app lo segnerà come completato su AniList e Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "La cache dell'app aiuta a caricare le immagini più velocemente.\n\nCancellare la cartella Documenti eliminerà tutti i moduli scaricati.\n\nCancellare i dati dell'app eliminerà tutte le tue impostazioni e dati.";
|
||||
"Translators" = "Traduttori";
|
||||
"Paste URL" = "Incolla URL";
|
||||
"Series Title" = "Titolo della serie";
|
||||
"Content Source" = "Fonte del contenuto";
|
||||
"Watch Progress" = "Progresso di visione";
|
||||
"Recent searches" = "Ricerche recenti";
|
||||
"All Reading" = "Tutto ciò che leggi";
|
||||
"Nothing to Continue Reading" = "Niente da continuare a leggere";
|
||||
"Your recently read novels will appear here" = "I tuoi romanzi letti di recente appariranno qui";
|
||||
"No Bookmarks" = "Nessun segnalibro";
|
||||
"Add bookmarks to this collection" = "Aggiungi segnalibri a questa raccolta";
|
||||
"items" = "elementi";
|
||||
"All Watching" = "Tutto ciò che guardi";
|
||||
"No Reading History" = "Nessuna cronologia di lettura";
|
||||
"Books you're reading will appear here" = "I libri che stai leggendo appariranno qui";
|
||||
"Create Collection" = "Crea raccolta";
|
||||
"Collection Name" = "Nome raccolta";
|
||||
"Rename Collection" = "Rinomina raccolta";
|
||||
"Rename" = "Rinomina";
|
||||
"Novel Title" = "Titolo del romanzo";
|
||||
"Read Progress" = "Progresso di lettura";
|
||||
"Date Created" = "Data di creazione";
|
||||
"Name" = "Nome";
|
||||
"Item Count" = "Numero di elementi";
|
||||
"Date Added" = "Data di aggiunta";
|
||||
"Title" = "Titolo";
|
||||
"Source" = "Fonte";
|
||||
"Search reading..." = "Cerca nelle letture...";
|
||||
"Search collections..." = "Cerca nelle raccolte...";
|
||||
"Search bookmarks..." = "Cerca nei segnalibri...";
|
||||
"%d items" = "%d elementi";
|
||||
"Fetching Data" = "Recupero dati";
|
||||
"Please wait while fetching." = "Attendere durante il recupero.";
|
||||
"Start Reading" = "Inizia a leggere";
|
||||
"Chapters" = "Capitoli";
|
||||
"Completed" = "Completato";
|
||||
"Drag to reorder" = "Trascina per riordinare";
|
||||
"Drag to reorder sections" = "Trascina per riordinare le sezioni";
|
||||
"Library View" = "Vista libreria";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personalizza le sezioni mostrate nella tua libreria. Puoi riordinare le sezioni o disabilitarle completamente.";
|
||||
"Library Sections Order" = "Ordine delle sezioni della libreria";
|
||||
"Completion Percentage" = "Percentuale di completamento";
|
||||
"Translators" = "Traduttori";
|
||||
"Paste URL" = "Incolla URL";
|
||||
"Collections" = "Raccolte";
|
||||
"Continue Reading" = "Continua a leggere";
|
||||
"Backup & Restore" = "Backup e ripristino";
|
||||
"Export Backup" = "Esporta backup";
|
||||
"Import Backup" = "Importa backup";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Nota: Questa funzione è ancora sperimentale. Si prega di ricontrollare i dati dopo esportazione/importazione.";
|
||||
"Backup" = "Backup";
|
||||
506
Sora/Localization/kk.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
/* General */
|
||||
"About" = "Туралы";
|
||||
"About Sora" = "Sora туралы";
|
||||
"Active" = "Белсенді";
|
||||
"Active Downloads" = "Белсенді жүктеулер";
|
||||
"Actively downloading media can be tracked from here." = "Белсенді жүктелетін медианы осы жерден бақылауға болады.";
|
||||
"Add Module" = "Модуль қосу";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Портрет және альбом режимдерінде қатардағы медиа элементтерінің санын реттеу.";
|
||||
"Advanced" = "Кеңейтілген";
|
||||
"AKA Sulfur" = "Sulfur деп те белгілі";
|
||||
"All Bookmarks" = "Барлық бетбелгілер";
|
||||
"All Watching" = "Барлық көрілетін";
|
||||
"Also known as Sulfur" = "Sulfur деп те белгілі";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ID";
|
||||
"AniList Match" = "AniList сәйкестігі";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Қолданбаны жақсарту үшін анонимді деректер жиналады. Жеке ақпарат жиналмайды. Мұны кез келген уақытта өшіруге болады.";
|
||||
"App Info" = "Қолданба ақпараты";
|
||||
"App Language" = "Қолданба тілі";
|
||||
"App Storage" = "Қолданба қоймасы";
|
||||
"Appearance" = "Сыртқы түр";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Барлық кэштелген деректерді өшіруді қалайсыз ба? Бұл қойма орынын босатуға көмектеседі.";
|
||||
"Are you sure you want to delete '%@'?" = "'%@' жоюды қалайсыз ба?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "'%2$@' ішіндегі барлық %1$d эпизодты жоюды қалайсыз ба?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Барлық жүктелген активтерді жоюды қалайсыз ба? Жүктелген файлдарды болашақ пайдалану үшін сақтай отырып, тек кітапхананы тазартуды таңдай аласыз.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Барлық қолданба деректерін өшіруді қалайсыз ба? Бұл әрекетті қайтаруға болмайды.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Фон қосылған";
|
||||
"Bookmark items for an easier access later." = "Кейін оңай қол жеткізу үшін элементтерге бетбелгі қою.";
|
||||
"Bookmarks" = "Бетбелгілер";
|
||||
"Bottom Padding" = "Төменгі жиек";
|
||||
"Cancel" = "Болдырмау";
|
||||
"Cellular Quality" = "Ұялы байланыс сапасы";
|
||||
"Check out some community modules here!" = "Қауымдастық модульдерін осы жерден қараңыз!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "WiFi және ұялы байланыс үшін қалаулы бейне ажыратымдылығын таңдаңыз. Жоғары ажыратымдылық көбірек деректерді пайдаланады, бірақ жақсы сапа береді. Нақты сапа қолжетімді болмаса, ең жақын нұсқа автоматты түрде таңдалады.\n\nЕскерту: Барлық бейне көздері мен ойнатқыштар сапа таңдауды қолдамайды. Бұл мүмкіндік Sora ойнатқышымен HLS ағындарында ең жақсы жұмыс істейді.";
|
||||
"Clear" = "Тазарту";
|
||||
"Clear All Downloads" = "Барлық жүктеулерді тазарту";
|
||||
"Clear Cache" = "Кэшті тазарту";
|
||||
"Clear Library Only" = "Тек кітапхананы тазарту";
|
||||
"Clear Logs" = "Журналдарды тазарту";
|
||||
"Click the plus button to add a module!" = "Модуль қосу үшін плюс түймесін басыңыз!";
|
||||
"Continue Watching" = "Көруді жалғастыру";
|
||||
"Continue Watching Episode %d" = "%d эпизодты көруді жалғастыру";
|
||||
"Contributors" = "Үлес қосушылар";
|
||||
"Copied to Clipboard" = "Буферге көшірілді";
|
||||
"Copy to Clipboard" = "Буферге көшіру";
|
||||
"Copy URL" = "URL көшіру";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld эпизод";
|
||||
"%lld of %lld" = "%lld / %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% көрілді";
|
||||
"Episode %lld" = "%lld эпизод";
|
||||
"Episodes" = "Эпизодтар";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Эпизодтар әлі қолжетімді болмауы мүмкін немесе көзде мәселе болуы мүмкін.";
|
||||
"Episodes Range" = "Эпизодтар ауқымы";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Қараңғы";
|
||||
"DATA & LOGS" = "ДЕРЕКТЕР ЖӘНЕ ЖУРНАЛДАР";
|
||||
"Debug" = "Жөндеу";
|
||||
"Debugging and troubleshooting." = "Жөндеу және ақаулықтарды жою.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Жою";
|
||||
"Delete All" = "Барлығын жою";
|
||||
"Delete All Downloads" = "Барлық жүктеулерді жою";
|
||||
"Delete All Episodes" = "Барлық эпизодтарды жою";
|
||||
"Delete Download" = "Жүктеуді жою";
|
||||
"Delete Episode" = "Эпизодты жою";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Іздеу үшін екі рет басу";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Экранның жақтарын екі рет басу қысқа басу параметрімен өткізіп жіберетін болады.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Жүктеу";
|
||||
"Download Episode" = "Эпизодты жүктеу";
|
||||
"Download Summary" = "Жүктеу қорытындысы";
|
||||
"Download This Episode" = "Осы эпизодты жүктеу";
|
||||
"Downloaded" = "Жүктелді";
|
||||
"Downloaded Shows" = "Жүктелген шоулар";
|
||||
"Downloading" = "Жүктелуде";
|
||||
"Downloads" = "Жүктеулер";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Аналитиканы қосу";
|
||||
"Enable Subtitles" = "Субтитрлерді қосу";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Өшіру";
|
||||
"Erase all App Data" = "Барлық қолданба деректерін өшіру";
|
||||
"Erase App Data" = "Қолданба деректерін өшіру";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Қате";
|
||||
"Error Fetching Results" = "Нәтижелерді алуда қате";
|
||||
"Errors and critical issues." = "Қателер және сыни мәселелер.";
|
||||
"Failed to load contributors" = "Үлес қосушыларды жүктеу сәтсіз аяқталды";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Эпизод метадеректерін алу";
|
||||
"Files Downloaded" = "Жүктелген файлдар";
|
||||
"Font Size" = "Қаріп өлшемі";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Альбом режимін мәжбүрлеу";
|
||||
"General" = "Жалпы";
|
||||
"General events and activities." = "Жалпы оқиғалар мен іс-әрекеттер.";
|
||||
"General Preferences" = "Жалпы параметрлер";
|
||||
"Hide Splash Screen" = "Кіріспе экранын жасыру";
|
||||
"HLS video downloading." = "HLS бейне жүктеу.";
|
||||
"Hold Speed" = "Ұстау жылдамдығы";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Ақпарат";
|
||||
"INFOS" = "АҚПАРАТТАР";
|
||||
"Installed Modules" = "Орнатылған модульдер";
|
||||
"Interface" = "Интерфейс";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Discord-қа қосылу";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Альбом бағандары";
|
||||
"Language" = "Тіл";
|
||||
"LESS" = "АЗ";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Кітапхана";
|
||||
"License (GPLv3.0)" = "Лицензия (GPLv3.0)";
|
||||
"Light" = "Жарық";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "%lld эпизод жүктелуде...";
|
||||
"Loading logs..." = "Журналдар жүктелуде...";
|
||||
"Loading module information..." = "Модуль ақпараты жүктелуде...";
|
||||
"Loading Stream" = "Ағын жүктелуде";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Жөндеу ақпаратын жазу";
|
||||
"Log Filters" = "Журнал сүзгілері";
|
||||
"Log In with AniList" = "AniList арқылы кіру";
|
||||
"Log In with Trakt" = "Trakt арқылы кіру";
|
||||
"Log Out from AniList" = "AniList-тен шығу";
|
||||
"Log Out from Trakt" = "Trakt-тен шығу";
|
||||
"Log Types" = "Журнал түрлері";
|
||||
"Logged in as" = "Кірген пайдаланушы:";
|
||||
"Logged in as " = "Кірген пайдаланушы: ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Журналдар";
|
||||
"Long press Skip" = "Ұзақ басып өткізу";
|
||||
"MAIN" = "НЕГІЗГІ";
|
||||
"Main Developer" = "Негізгі әзірлеуші";
|
||||
"MAIN SETTINGS" = "НЕГІЗГІ ПАРАМЕТРЛЕР";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Алдыңғыларды көрілді деп белгілеу";
|
||||
"Mark as Watched" = "Көрілді деп белгілеу";
|
||||
"Mark Episode as Watched" = "Эпизодты көрілді деп белгілеу";
|
||||
"Mark Previous Episodes as Watched" = "Алдыңғы эпизодтарды көрілді деп белгілеу";
|
||||
"Mark watched" = "Көрілді деп белгілеу";
|
||||
"Match with AniList" = "AniList-пен сәйкестендіру";
|
||||
"Match with TMDB" = "TMDB-мен сәйкестендіру";
|
||||
"Matched ID: %lld" = "Сәйкестендірілген ID: %lld";
|
||||
"Matched with: %@" = "Сәйкестендірілді: %@";
|
||||
"Max Concurrent Downloads" = "Максималды бір мезгілдегі жүктеулер";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Медиа тор орналасуы";
|
||||
"Media Player" = "Медиа ойнатқыш";
|
||||
"Media View" = "Медиа көрінісі";
|
||||
"Metadata Provider" = "Метадерек провайдері";
|
||||
"Metadata Providers Order" = "Метадерек провайдерлерінің реті";
|
||||
"Module Removed" = "Модуль жойылды";
|
||||
"Modules" = "Модульдер";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "МОДУЛЬДЕР";
|
||||
"MORE" = "КӨБІРЕК";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Белсенді жүктеулер жоқ";
|
||||
"No AniList matches found" = "AniList сәйкестіктері табылмады";
|
||||
"No Data Available" = "Деректер қолжетімді емес";
|
||||
"No Downloads" = "Жүктеулер жоқ";
|
||||
"No episodes available" = "Эпизодтар қолжетімді емес";
|
||||
"No Episodes Available" = "Эпизодтар қолжетімді емес";
|
||||
"No items to continue watching." = "Көруді жалғастыратын элементтер жоқ.";
|
||||
"No matches found" = "Сәйкестіктер табылмады";
|
||||
"No Module Selected" = "Модуль таңдалмады";
|
||||
"No Modules" = "Модульдер жоқ";
|
||||
"No Results Found" = "Нәтижелер табылмады";
|
||||
"No Search Results Found" = "Іздеу нәтижелері табылмады";
|
||||
"Nothing to Continue Watching" = "Көруді жалғастыратын ешнәрсе жоқ";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Модульдер тек JSON файлының ішінде басқа нұсқа жолы болған жағдайда ғана ауыстырылатынын ескеріңіз.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "Жарайды";
|
||||
"Open Community Library" = "Қауымдастық кітапханасын ашу";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "AniList-те ашу";
|
||||
"Original Poster" = "Түпнұсқа постер";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Тоқтатылды";
|
||||
"Play" = "Ойнату";
|
||||
"Player" = "Ойнатқыш";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Тіл өзгерісін қолдану үшін қолданбаны қайта іске қосыңыз.";
|
||||
"Please select a module from settings" = "Параметрлерден модуль таңдаңыз";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Портрет бағандары";
|
||||
"Progress bar Marker Color" = "Прогресс жолағы белгішесінің түсі";
|
||||
"Provider: %@" = "Провайдер: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Кезек";
|
||||
"Queued" = "Кезекте";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Жақында көрілген мазмұн осы жерде пайда болады.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Іске қосу кезінде модульдерді жаңарту";
|
||||
"Refresh Storage Info" = "Қойма ақпаратын жаңарту";
|
||||
"Remember Playback speed" = "Ойнату жылдамдығын есте сақтау";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Жою";
|
||||
"Remove All Cache" = "Барлық кэшті жою";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Барлық құжаттарды жою";
|
||||
"Remove Documents" = "Құжаттарды жою";
|
||||
"Remove Downloaded Media" = "Жүктелген медианы жою";
|
||||
"Remove Downloads" = "Жүктеулерді жою";
|
||||
"Remove from Bookmarks" = "Бетбелгілерден жою";
|
||||
"Remove Item" = "Элементті жою";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Мәселе туралы хабарлау";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Қалпына келтіру";
|
||||
"Reset AniList ID" = "AniList ID қалпына келтіру";
|
||||
"Reset Episode Progress" = "Эпизод прогрессін қалпына келтіру";
|
||||
"Reset progress" = "Прогрессті қалпына келтіру";
|
||||
"Reset Progress" = "Прогрессті қалпына келтіру";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Қайта іске қосу қажет";
|
||||
"Running Sora %@ - cranci1" = "Sora %@ жұмыс істеп тұр - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Сақтау";
|
||||
"Search" = "Іздеу";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Жүктеулерді іздеу";
|
||||
"Search for something..." = "Бір нәрсені іздеу...";
|
||||
"Search..." = "Іздеу...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "%d маусым";
|
||||
"Season %lld" = "%lld маусым";
|
||||
"Segments Color" = "Сегменттер түсі";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Модуль таңдау";
|
||||
"Set Custom AniList ID" = "Пайдаланушы AniList ID орнату";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Параметрлер";
|
||||
"Shadow" = "Көлеңке";
|
||||
"Show More (%lld more characters)" = "Көбірек көрсету (тағы %lld таңба)";
|
||||
"Show PiP Button" = "PiP түймесін көрсету";
|
||||
"Show Skip 85s Button" = "85с өткізу түймесін көрсету";
|
||||
"Show Skip Intro / Outro Buttons" = "Кіріспе/шығыспаны өткізу түймелерін көрсету";
|
||||
"Shows" = "Шоулар";
|
||||
"Size (%@)" = "Өлшем (%@)";
|
||||
"Skip Settings" = "Өткізу параметрлері";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Кейбір мүмкіндіктер Sora және Әдепкі ойнатқышпен шектелген, мысалы ForceLandscape, holdSpeed және пайдаланушы уақыт өткізу өсімшелері.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ - cranci1 әзірлеген";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "Sora мен cranci1 AniList немесе Trakt-пен ешқандай байланысы жоқ.
|
||||
|
||||
Сонымен қатар прогресс жаңартулары 100% дәл болмауы мүмкін.";
|
||||
"Sora GitHub Repository" = "Sora GitHub репозиторийі";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur әрқашан жарнамасыз тегін болып қалады!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Сұрыптау";
|
||||
"Speed Settings" = "Жылдамдық параметрлері";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Көруді бастау";
|
||||
"Start Watching Episode %d" = "%d эпизодты көруді бастау";
|
||||
"Storage Used" = "Пайдаланылған қойма";
|
||||
"Stream" = "Ағын";
|
||||
"Streaming and video playback." = "Ағын және бейне ойнату.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Субтитр түсі";
|
||||
"Subtitle Settings" = "Субтитр параметрлері";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Аниме прогрессін синхрондау";
|
||||
"Sync TV shows progress" = "ТВ шоулар прогрессін синхрондау";
|
||||
|
||||
/* System */
|
||||
"System" = "Жүйе";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Ағымдағы сәйкестікті ауыстыру үшін атауды басыңыз.";
|
||||
"Tap Skip" = "Өткізуді басу";
|
||||
"Tap to manage your modules" = "Модульдеріңізді басқару үшін басыңыз";
|
||||
"Tap to select a module" = "Модуль таңдау үшін басыңыз";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.
|
||||
|
||||
Clearing the Documents folder will delete all downloaded modules.
|
||||
|
||||
Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Қолданба кэші қолданбаға суреттерді жылдам жүктеуге көмектеседі.
|
||||
|
||||
Құжаттар қалтасын тазарту барлық жүктелген модульдерді жояды.
|
||||
|
||||
Салдарын түсінбесеңіз, қолданба деректерін өшірмеңіз - бұл қолданбаның дұрыс жұмыс істемеуіне әкелуі мүмкін.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.
|
||||
|
||||
For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Эпизодтар ауқымы әр бетте қанша эпизод пайда болатынын басқарады. Эпизодтар топтарға бөлінеді (1–25, 26–50 сияқты), бұл олар арқылы оңайырақ жүруге мүмкіндік береді.
|
||||
|
||||
Эпизод метадеректері үшін бұл эпизодтың кішкене суреті мен атауын білдіреді, өйткені кейде ол спойлерлерді қамтуы мүмкін.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Модуль тек бір эпизод ұсынды, бұл ең ықтимал фильм, сондықтан біз осы жағдайлар үшін жеке экрандар жасауды шештік.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Кішкене суреттер ені";
|
||||
"TMDB Match" = "TMDB сәйкестігі";
|
||||
"Trackers" = "Трекерлер";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Басқа кілт сөздерді қолданып көріңіз";
|
||||
"Try different search terms" = "Басқа іздеу терминдерін қолданып көріңіз";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Тоқтату үшін екі саусақпен ұстау";
|
||||
"Unable to fetch matches. Please try again later." = "Сәйкестіктерді алу мүмкін емес. Кейінірек қайталап көріңіз.";
|
||||
"Use TMDB Poster Image" = "TMDB постер суретін пайдалану";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Бейне ойнатқыш";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Бейне сапасы таңдаулары";
|
||||
"View All" = "Барлығын көру";
|
||||
"Watched" = "Көрілді";
|
||||
"Why am I not seeing any episodes?" = "Неліктен мен ешқандай эпизод көрмеймін?";
|
||||
"WiFi Quality" = "WiFi сапасы";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Сіз кірмегенсіз";
|
||||
"You have no items saved." = "Сізде сақталған элементтер жоқ.";
|
||||
"Your downloaded episodes will appear here" = "Жүктелген эпизодтарыңыз осында пайда болады";
|
||||
"Your recently watched content will appear here" = "Жақында көрген мазмұныңыз осында пайда болады";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Жүктеу параметрлері";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Максималды бір мезгілдегі жүктеулер қанша эпизодты бір уақытта жүктеуге болатынын басқарады. Жоғары мәндер көбірек өткізу қабілеті мен құрылғы ресурстарын пайдалануы мүмкін.";
|
||||
"Quality" = "Сапа";
|
||||
"Max Concurrent Downloads" = "Максималды бір мезгілдегі жүктеулер";
|
||||
"Allow Cellular Downloads" = "Ұялы жүктеулерге рұқсат беру";
|
||||
"Quality Information" = "Сапа ақпараты";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Қойма басқаруы";
|
||||
"Storage Used" = "Пайдаланылған қойма";
|
||||
"Library cleared successfully" = "Кітапхана сәтті тазартылды";
|
||||
"All downloads deleted successfully" = "Барлық жүктеулер сәтті жойылды";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Соңғы іздеулер";
|
||||
"me frfr" = "мен шынында";
|
||||
"Data" = "Деректер";
|
||||
"Maximum Quality Available" = "Қолжетімді максималды сапа";
|
||||
"DownloadCountFormat" = "%d / %d";
|
||||
"Error loading chapter" = "Тарауды жүктеу қатесі";
|
||||
"Font Size: %dpt" = "Қаріп өлшемі: %dpt";
|
||||
"Line Spacing: %.1f" = "Жоларалық қашықтық: %.1f";
|
||||
"Line Spacing" = "Жоларалық қашықтық";
|
||||
"Margin: %dpx" = "Шеткі өріс: %dpx";
|
||||
"Margin" = "Шеткі өріс";
|
||||
"Auto Scroll Speed" = "Автоматты айналдыру жылдамдығы";
|
||||
"Speed" = "Жылдамдық";
|
||||
"Speed: %.1fx" = "Жылдамдық: %.1fx";
|
||||
"Matched %@: %@" = "Сәйкестік %@: %@";
|
||||
"Enter the AniList ID for this series" = "Осы серия үшін AniList ID енгізіңіз";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Жинақ құру";
|
||||
"Collection Name" = "Жинақ атауы";
|
||||
"Rename Collection" = "Жинақты қайта атау";
|
||||
"Rename" = "Қайта атау";
|
||||
"All Reading" = "Барлық оқу";
|
||||
"Recently Added" = "Жуырда қосылған";
|
||||
"Novel Title" = "Роман атауы";
|
||||
"Read Progress" = "Оқу барысы";
|
||||
"Date Created" = "Құрылған күні";
|
||||
"Name" = "Атауы";
|
||||
"Item Count" = "Элементтер саны";
|
||||
"Date Added" = "Қосылған күні";
|
||||
"Title" = "Тақырып";
|
||||
"Source" = "Дереккөз";
|
||||
"Search reading..." = "Оқуды іздеу...";
|
||||
"Search collections..." = "Жинақтарды іздеу...";
|
||||
"Search bookmarks..." = "Бетбелгілерді іздеу...";
|
||||
"%d items" = "%d элемент";
|
||||
"Fetching Data" = "Деректерді алу";
|
||||
"Please wait while fetching." = "Алу барысында күтіңіз.";
|
||||
"Start Reading" = "Оқуды бастау";
|
||||
"Chapters" = "Тараулар";
|
||||
"Completed" = "Аяқталды";
|
||||
"Drag to reorder" = "Ретін өзгерту үшін сүйреңіз";
|
||||
"Drag to reorder sections" = "Бөлімдердің ретін өзгерту үшін сүйреңіз";
|
||||
"Library View" = "Кітапхана көрінісі";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Кітапханаңызда көрсетілетін бөлімдерді баптаңыз. Бөлімдерді қайта реттеуге немесе толықтай өшіруге болады.";
|
||||
"Library Sections Order" = "Кітапхана бөлімдерінің реті";
|
||||
"Completion Percentage" = "Аяқталу пайызы";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Кейбір функциялар тек Sora және әдепкі ойнатқышта ғана қолжетімді, мысалы, ландшафтты мәжбүрлеу, жылдамдықты ұстау және уақытты өткізіп жіберу.\n\nАяқталу пайызы параметрі бейне соңына дейін қай жерде аяқталған деп белгіленетінін анықтайды (AniList және Trakt).";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Қолданба кэші суреттерді жылдам жүктеуге көмектеседі.\n\nDocuments қалтасын тазалау барлық жүктелген модульдерді өшіреді.\n\nҚолданба деректерін өшіру барлық баптауларыңызды және деректеріңізді өшіреді.";
|
||||
"Translators" = "Аудармашылар";
|
||||
"Paste URL" = "URL қою";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Серия атауы";
|
||||
"Content Source" = "Мазмұн көзі";
|
||||
"Watch Progress" = "Көру барысы";
|
||||
"Nothing to Continue Reading" = "Оқуды жалғастыруға ештеңе жоқ";
|
||||
"Your recently read novels will appear here" = "Соңғы оқылған романдар осында көрсетіледі";
|
||||
"No Bookmarks" = "Бетбелгілер жоқ";
|
||||
"Add bookmarks to this collection" = "Бұл жинаққа бетбелгілерді қосыңыз";
|
||||
"items" = "элементтер";
|
||||
"All Watching" = "Барлық көру";
|
||||
"No Reading History" = "Оқу тарихы жоқ";
|
||||
"Books you're reading will appear here" = "Сіз оқып жатқан кітаптар осында көрсетіледі";
|
||||
"Create Collection" = "Жинақ құру";
|
||||
"Collection Name" = "Жинақ атауы";
|
||||
"Rename Collection" = "Жинақты қайта атау";
|
||||
"Rename" = "Қайта атау";
|
||||
"Novel Title" = "Роман атауы";
|
||||
"Read Progress" = "Оқу барысы";
|
||||
"Date Created" = "Жасалған күні";
|
||||
"Name" = "Аты";
|
||||
"Item Count" = "Элементтер саны";
|
||||
"Date Added" = "Қосылған күні";
|
||||
"Title" = "Атауы";
|
||||
"Source" = "Дереккөз";
|
||||
"Search reading..." = "Оқуды іздеу...";
|
||||
"Search collections..." = "Жинақтарды іздеу...";
|
||||
"Search bookmarks..." = "Бетбелгілерді іздеу...";
|
||||
"%d items" = "%d элемент";
|
||||
"Fetching Data" = "Деректерді алу";
|
||||
"Please wait while fetching." = "Алу кезінде күтіңіз.";
|
||||
"Start Reading" = "Оқуды бастау";
|
||||
"Chapters" = "Тараулар";
|
||||
"Completed" = "Аяқталды";
|
||||
"Drag to reorder" = "Қайта реттеу үшін сүйреңіз";
|
||||
"Drag to reorder sections" = "Бөлімдерді қайта реттеу үшін сүйреңіз";
|
||||
"Library View" = "Кітапхана көрінісі";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Кітапханаңызда көрсетілетін бөлімдерді реттеңіз. Бөлімдерді қайта реттеуге немесе толықтай өшіруге болады.";
|
||||
"Library Sections Order" = "Кітапхана бөлімдерінің реті";
|
||||
"Completion Percentage" = "Аяқталу пайызы";
|
||||
"Translators" = "Аудармашылар";
|
||||
"Paste URL" = "URL қою";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Жинақтар";
|
||||
"Continue Reading" = "Оқуды жалғастыру";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Сақтық көшірме және қалпына келтіру";
|
||||
"Export Backup" = "Сақтық көшірмені экспорттау";
|
||||
"Import Backup" = "Сақтық көшірмені импорттау";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Ескерту: Бұл мүмкіндік әлі де тәжірибелік. Экспорт/импорттан кейін деректеріңізді тексеріңіз.";
|
||||
"Backup" = "Сақтық көшірме";
|
||||
467
Sora/Localization/mn.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
/* General */
|
||||
"About" = "Бидний тухайд";
|
||||
"About Sora" = "Sora аппын тухай";
|
||||
"Active" = "Идэвхтэй";
|
||||
"Active Downloads" = "Татаж байна";
|
||||
"Actively downloading media can be tracked from here." = "Татаж байгаа үзвэрүүдийг эндээс харж болно";
|
||||
"Add Module" = "Модул нэмэх";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Хэвтээ болон босоо загварын нэг мөрөн харуулах үзвэрийн тоо";
|
||||
"Advanced" = "Нарийн тохиргоо";
|
||||
"AKA Sulfur" = "өөрөөр Sulfur";
|
||||
"All Bookmarks" = "Бүх хадгалсан үзвэрүүд";
|
||||
"All Watching" = "Үзэж буй үзвэрүүд";
|
||||
"Also known as Sulfur" = "өөрөөр Sulfur гэж нэрлэдэг";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ХД";
|
||||
"AniList Match" = "AniList тохирол";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Аппыг сайжруулах зорилгоор мэдээллийг нууцалж цуглуулдаг. Таны хувийн мэдээллийг цуглуулдаггүй болно. Мөн хүссэн үедээ мэдээлэл цуглуулахыг цуцалж болно.";
|
||||
"App Info" = "Аппын мэдээлэл ";
|
||||
"App Language" = "Хэл";
|
||||
"App Storage" = "Багтаамж";
|
||||
"Appearance" = "Харагдах байдал";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Та хадгалагдсан өгөгдлийн устгахдаа итгэлтэй байна уу? Устгасан тохиолдолд багтаамж чөлөөлөгдөнө.";
|
||||
"Are you sure you want to delete '%@'?" = "Та '%@' үзвэрийг устгахдаа итгэлтэй байна уу?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Та '%2$@' үзвэрийн %1$d ангиудыг устгахдаа итгэлтэй байна уу?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Та бүх татаж авсан үзвэрийг устгахдаа итгэлтэй байна уу? Зөвхөн сангаа цэврэлсэнээр, татаж авсан үзвэрүүдээ устгахгүй байж болно.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Та аппын бүх өгөгдлийг утгахдаа итгэлтэй байна уу? Энэ үйлдлийг буцаах боломжгүй.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Аппыг идэвхгүй үед татах";
|
||||
"Bookmark items for an easier access later." = "Үзвэрийг хадгалсанаар дараа нь олоход хялбар болно";
|
||||
"Bookmarks" = "Хадгалсан үзвэр";
|
||||
"Bottom Padding" = "Доод зай";
|
||||
"Cancel" = "Цуцлах";
|
||||
"Cellular Quality" = "Утасны дата бичлэгийн чанар";
|
||||
"Check out some community modules here!" = "Илүү олон модулиудыг эндээс олоорой!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Өөрийн WiFi болон утасны датанд тааруулж бичлэгийн чанарыг сонгоорой. Өндөр чанартай бичлэг нь илүү их дата ашиглана. Хэрэв таны сонгосон бичлэгийн чанар байхгүй бол хамгийн ойролцоо чанарыг сонгож тоглуулна.\n\nТэмдэглэл: Бүх үзвэрүүд болон бичлэг тоглуулагч нь чанар сонгох үйлдэлгүй байдаг. Бичлэгийн чанар сонгох үйлдлийг HLS төрлийн үзвэрийг Sora тоглуулагч ашиглан үзэж байгаа тохиолдолд ашиглахад хамгийн тохиромжтой байдаг.";
|
||||
"Clear" = "Устгах";
|
||||
"Clear All Downloads" = "Бүх таталтыг устгах";
|
||||
"Clear Cache" = "Кэш цэвэрлэх";
|
||||
"Clear Library Only" = "Зөвхөн санг цэвэрлэх";
|
||||
"Clear Logs" = "Лог цэвэрлэх";
|
||||
"Click the plus button to add a module!" = "Нэмэх тэмдэг дээр дарж шинэ модуль нэмнэ үү!";
|
||||
"Continue Watching" = "Үргэлжлүүлж үзэх";
|
||||
"Continue Watching Episode %d" = "%d ангийг үргэлжлүүлж үзэх";
|
||||
"Contributors" = "Хувь нэмэр оруулсан";
|
||||
"Copied to Clipboard" = "Хуулсан";
|
||||
"Copy to Clipboard" = "Хуулсан";
|
||||
"Copy URL" = "Холбоосыг хуулах";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld анги";
|
||||
"%lld of %lld" = "%lld-ийн %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% үзсэн";
|
||||
"Episode %lld" = "%lld-р анги";
|
||||
"Episodes" = "Ангиуд";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = " ";
|
||||
"Episodes Range" = " ";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Хар";
|
||||
"DATA & LOGS" = "Өгөгдөл ба лог";
|
||||
"Debug" = "Алдаа илрүүлэх";
|
||||
"Debugging and troubleshooting." = "Алдааг илрүүлэх ба асуудал олох";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Устгах";
|
||||
"Delete All" = "Бүгдийг устгах";
|
||||
"Delete All Downloads" = "Бүх таталтыг устгах";
|
||||
"Delete All Episodes" = "Бүх ангийг устгах";
|
||||
"Delete Download" = "Таталт устгах";
|
||||
"Delete Episode" = "Анги устгах";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Хоёр дарж гүйлгэх";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Дэлгэцийн хоёр талд хоёр удаа хурдан дарвал богино хугацаагаар бичлэгийг гүйлгэнэ.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Татах";
|
||||
"Download Episode" = "Анги татах";
|
||||
"Download Summary" = "Таталтын түүх";
|
||||
"Download This Episode" = "Энэ ангийг татах";
|
||||
"Downloaded" = "Татсан";
|
||||
"Downloaded Shows" = "Татсан үзвэрүүд";
|
||||
"Downloading" = "Татаж байна";
|
||||
"Downloads" = "Таталтууд";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Аналитик ажилуулах";
|
||||
"Enable Subtitles" = "Хадмал харуулах";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Арилгах";
|
||||
"Erase all App Data" = "Аппын бүх өгөгдлийг арилгах";
|
||||
"Erase App Data" = "Аппын өгөгдлийг арилгах";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Алдаа";
|
||||
"Error Fetching Results" = "Илэрцийг олоход гарсан алдаа";
|
||||
"Errors and critical issues." = "Алдаанууд болон ноцтой асуудлууд";
|
||||
"Failed to load contributors" = "Контрибуторуудыг ачааллаж чадсангүй";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Ангийн мета мэдээллийг татах";
|
||||
"Files Downloaded" = "Татаж авсан файлууд";
|
||||
"Font Size" = "Үсгийн хэмжээ";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Байнга хэвтээ байлгах";
|
||||
"General" = "Ерөнхий";
|
||||
"General events and activities." = "Ерөнхий эвэнт ба үйл ажиллагаанууд";
|
||||
"General Preferences" = "Ерөнхий тохиргоо";
|
||||
"Hide Splash Screen" = "Эхлэлийн дэлгэцийг нуух";
|
||||
"HLS video downloading." = "HLS бичлэг таталт";
|
||||
"Hold Speed" = "Дарах хурд";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Мэдээлэл";
|
||||
"INFOS" = "МЭДЭЭЛЛҮҮД";
|
||||
"Installed Modules" = "Суулгасан модулиуд";
|
||||
"Interface" = "Харилцах хэсэг";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Дискорд сувагт нэгдэх";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Хэвтээ багана";
|
||||
"Language" = "Хэл";
|
||||
"LESS" = "БАГАСГАХ";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Сан";
|
||||
"License (GPLv3.0)" = "Лиценз (GPLх3.0)";
|
||||
"Light" = "Цагаан";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "%lld-р ангийг ачаалж байна...";
|
||||
"Loading logs..." = "Логийг ачаалж байна...";
|
||||
"Loading module information..." = "Модулийн мэдээллийг ачаалж байна...";
|
||||
"Loading Stream" = "Үзвэрийг ачаалж байна";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Дибаг мэдээллийг бичих";
|
||||
"Log Filters" = "Лог шүүлтүүрүүд";
|
||||
"Log In with AniList" = "AniList-ээр нэвтрэх";
|
||||
"Log In with Trakt" = "Trakt-аар нэвтрэх";
|
||||
"Log Out from AniList" = "AniList-ээс гарах";
|
||||
"Log Out from Trakt" = "Trakt-аас гарах";
|
||||
"Log Types" = "Логийн төрлүүд";
|
||||
"Logged in as" = "Нэвтэрсэн байна";
|
||||
"Logged in as " = " нэвтэрсэн байна";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Логууд";
|
||||
"Long press Skip" = "Удаан дарж алгасах";
|
||||
"MAIN" = "ҮНДСЭН";
|
||||
"Main Developer" = "Үндсэн Хөгжүүлэгч";
|
||||
"MAIN SETTINGS" = "ҮНДСЭН ТОХИРГООНУУД";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Өмнөх бүгдийг үзсэнээр тэмдэглэх";
|
||||
"Mark as Watched" = "Үзсэнээр тэмдэглэх";
|
||||
"Mark Episode as Watched" = "Ангийг үзсэнээр тэмдэглэх";
|
||||
"Mark Previous Episodes as Watched" = "Өмнөх бүх ангийг үзсэнээр тэмдэглэх";
|
||||
"Mark watched" = "Үзсэнийг тэмдэглэх";
|
||||
"Match with AniList" = "Anilist-тэй тааруулах";
|
||||
"Match with TMDB" = "TMDB-тэй тааруулах";
|
||||
"Matched ID: %lld" = "Тааруулсан ХД: %lld";
|
||||
"Matched with: %@" = "Тааруулсан: %@";
|
||||
"Max Concurrent Downloads" = "Зэрэг татах дээд хэмжээ";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Медиа грид байршил";
|
||||
"Media Player" = "Медиа тоглуулагч";
|
||||
"Media View" = "Медиа харагдац";
|
||||
"Metadata Provider" = "Нэмэлт мэдээлэл нийлүүлэгч";
|
||||
"Metadata Providers Order" = "Нэмэлт мэдээлэл нийлүүлэгчид";
|
||||
"Module Removed" = "Модуль устсан";
|
||||
"Modules" = "Модулиуд";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "МОДУЛИУД";
|
||||
"MORE" = "ИЛҮҮ";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Идэвхтэй таталт байхгүй байна";
|
||||
"No AniList matches found" = "Anilist дээр олдсонгүй";
|
||||
"No Data Available" = "Мэдээлэл байхгүй байна";
|
||||
"No Downloads" = "Таталт байхгүй байна";
|
||||
"No episodes available" = "Анги олдсонгүй";
|
||||
"No Episodes Available" = "Анги Олдсонгүй";
|
||||
"No items to continue watching." = "Үргэлүүлж үзэх зүйл байхгүй";
|
||||
"No matches found" = "Илэрц олдсонгүй";
|
||||
"No Module Selected" = "Модуль сонгоогүй байна";
|
||||
"No Modules" = "Модуль байгүй";
|
||||
"No Results Found" = "Хайлт олдсонгүй";
|
||||
"No Search Results Found" = "Хайлтын Үр Дүн Олдсонгүй";
|
||||
"Nothing to Continue Watching" = "Үргэлжлүүлж Үзэх Зүйл Байхгүй";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Модулийн JSON файл доторх хувилбарийн нэр өөрчлөгдсөн тохиолдолд л модуль шинэчлэгдэнэ.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "ЗА";
|
||||
"Open Community Library" = "Нийтлэг Санг Нээх";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Anilist дотор нээх";
|
||||
"Original Poster" = "Жинхэнэ Постлогч";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Зогсоосон";
|
||||
"Play" = "Тоглуулах";
|
||||
"Player" = "Тоглуулагч";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Аппаас гарч дахин орсноор хэл солигдоно";
|
||||
"Please select a module from settings" = "Тохиргооны хэсгээс модуль сонгоно уу";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Босоо Баганууд";
|
||||
"Progress bar Marker Color" = "Явцын зурвасын тэмдэглэгээний өнгө";
|
||||
"Provider: %@" = "Нийлүүлэгч: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Дараалал";
|
||||
"Queued" = "Хүлээлтэд орсон";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Сүүлд үзсэн үзвэрүүд энд харагдана";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Апп нээгдэх болгонд модуль шинэчлэх";
|
||||
"Refresh Storage Info" = "Багтаамжийн мэдээллийг шинэчлэх";
|
||||
"Remember Playback speed" = "Тоглуулах хурдыг сануулах";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Устгах";
|
||||
"Remove All Cache" = "Бүх Кэшийг Устгах";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Бүх мэдээлийг устгах";
|
||||
"Remove Documents" = "Мэдээллийг Устгах";
|
||||
"Remove Downloaded Media" = "Татаж авсан үзвэрийг устгах";
|
||||
"Remove Downloads" = "Таталтуудыг Устгах";
|
||||
"Remove from Bookmarks" = "Хадгалахаа болих";
|
||||
"Remove Item" = "Анги Устгах";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Алдаа мэдээлэх";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Анхны төлөвт оруулах";
|
||||
"Reset AniList ID" = "AniList ХД анхны төлөвт оруулах";
|
||||
"Reset Episode Progress" = "Эхнээс нь үзэх";
|
||||
"Reset progress" = "Анхны төлөвт оруулах явц";
|
||||
"Reset Progress" = "Явцыг ахний төлөвт оруулах";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Дахин ачааллах шаардлагатай";
|
||||
"Running Sora %@ - cranci1" = "Sora %@ ачаалж байна - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Хадгалах";
|
||||
"Search" = "Хайа";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Татсан үзвэр хайх";
|
||||
"Search for something..." = "Үзвэр хайх...";
|
||||
"Search..." = "Хайх...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "%d-р Улирал";
|
||||
"Season %lld" = "%lld-р Улирал";
|
||||
"Segments Color" = "Ерөнхий өнгө";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Модуль сонгох";
|
||||
"Set Custom AniList ID" = "AniList ХД харуулах";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Тохиргоо";
|
||||
"Shadow" = "Сүүдэр";
|
||||
"Show More (%lld more characters)" = "Илүү харуулах (%lld тэмдэгт харагдана)";
|
||||
"Show PiP Button" = "PiP товч харуулах";
|
||||
"Show Skip 85s Button" = "85с алгасах товч харуулах";
|
||||
"Show Skip Intro / Outro Buttons" = "Эхлэл/Төгсгөлийн дууг алгасах точ хөруулах";
|
||||
"Shows" = "Харуулах";
|
||||
"Size (%@)" = "Хэмжээ (%d)";
|
||||
"Skip Settings" = "Алгасах тохиргоо";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Зарим үйлдлүүд нь зөвхөн Sora болон Үндсэн тоглуулагч дээр ажилладаг, тухайлбал Үргэлж хэвтээ байдлаар үзэх, удаан дарж хурд удирдах болон алгасах хугацаа нь тохиргоо";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ by cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "
|
||||
Sora ба cranci1 нь AniList эсвэл Trakt-тэй ямар ч хамааралгүй болно.
|
||||
|
||||
Мөн явцын шинэчлэлтүүд 100% үнэн зөв байж чадахгүй гэдгийг анхаарна уу.";
|
||||
"Sora GitHub Repository" = "Sora GitHub хуудас";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur нь үргэлж үнэ төлбөргүй, зар сурталчилгаагүй байх болно!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Эрэмблэх";
|
||||
"Speed Settings" = "Тоглуулах хурд";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Үзэх";
|
||||
"Start Watching Episode %d" = "%d ангийг үзэх";
|
||||
"Storage Used" = "Ашигласан багтаамж";
|
||||
"Stream" = "Үзвэр";
|
||||
"Streaming and video playback." = "Үзвэр ба бичлэг";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Хадмалын өнгө";
|
||||
"Subtitle Settings" = "Хадмалын тохиргоо";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Аниме үзсэн ангиудыг тэмдэглэх";
|
||||
"Sync TV shows progress" = "Цувралын үзсэн ангиудыг тэмдэглэх";
|
||||
|
||||
/* System */
|
||||
"System" = "Систем";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Нэр дээр дарж одоогийн хайлтыг солино уу";
|
||||
"Tap Skip" = "Энд дарж гүйлгэнэ үү";
|
||||
"Tap to manage your modules" = "Энд дарж модуль солино уу";
|
||||
"Tap to select a module" = "Энд дарж модуль сонгоно уу";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster. Clearing the Documents folder will delete all downloaded modules. Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = " Аппын кэш нь зургуудийг илүү хурдан ачаалахад тусалдаг. Documents хавтасыг устгавал бүх татсан үзвэрүүдийг утгана. Гарах үр дагаврыг нь ойлголгүйгээр Апп датаг бүү устга - Энэ нь дараа нь аппыг буруу ажиллахад нөлөөлөх боломжтой";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily. For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = " Ангийн хязгаар нь нэг хуудсанд хэдэн харагдахыг тохируулдаг. Ингэснээр ангиуд нь багцлагдаж (Жишээ нь 1-25, 26-50 гэх мэт), үзэх ангиа сонгоход илүү хялбар болгоно. Ангийн мета өгөгдөл нь тухайн ангийн харагдац зураг болон нэрийг хадгалдаг тул зарим тохиолдолд спойлер агуулдаг.";
|
||||
|
||||
|
||||
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Модуль зөвхөн нэг анги агуулсан байсан тул, кино байх боломжтой гэж үзээд тусгай дэлгэц хийхээр шийдсэн.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Үзвэрийн зургийн урт";
|
||||
"TMDB Match" = "ТМДБ тохирол";
|
||||
"Trackers" = "Тракерууд";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Өөр үгээр хайж үзнэ үү";
|
||||
"Try different search terms" = "Өөр төрлөөр хайж үзнэ үү";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Хоёр хуруугаар дарж бичлэгийг зогсоох";
|
||||
"Unable to fetch matches. Please try again later." = "Илэрц олж чадсангүй. Дараа дахин оролдоно уу?";
|
||||
"Use TMDB Poster Image" = "ТМДБ нүүр зураг ашиглах";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "х%@";
|
||||
"Video Player" = "Бичлэг тоглуулагч";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Бичлэгийн чанарын тохиргоо";
|
||||
"View All" = "Бүгдийг харах";
|
||||
"Watched" = "Үзсэн";
|
||||
"Why am I not seeing any episodes?" = "Яагаад нэг ч анги байхгүй байна?";
|
||||
"WiFi Quality" = "WiFi чанар";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Та нэвтрээгүй байна";
|
||||
"You have no items saved." = "Танд хадгалсан үзвэр байхгүй байна";
|
||||
"Your downloaded episodes will appear here" = "Таны татсан үзвэрийн ангиуд энд харагдана";
|
||||
"Your recently watched content will appear here" = "Таны сүүлд үзсэн үзвэрүүд энд харагдана";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Таталтын тохиргоо";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Зэрэг таталт хийх хязгаар нь зэрэг татах ангийн тоо юм. Өндөр байх тусам илүү их дата болон утасны нөөцийг ашиглана.";
|
||||
"Quality" = "Чанар";
|
||||
"Max Concurrent Downloads" = "Зэрэг таталт хийх хязгаар";
|
||||
"Allow Cellular Downloads" = "Утасны датагаар татах";
|
||||
"Quality Information" = "Чанарын мэдээлэл";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Багтаамж удирдах";
|
||||
"Storage Used" = "Ашигласан багтаамж";
|
||||
"Library cleared successfully" = "Санг амжилттай цэвэрлэлээ";
|
||||
"All downloads deleted successfully" = "Бүх татсан үзвэрүүдийг амжилттай устлаа";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Сүүлд хайсан";
|
||||
"me frfr" = "me frfr";
|
||||
"Data" = "Мэдээлэл";
|
||||
"Maximum Quality Available" = "Хамгийн өндөр чанартай";
|
||||
"All Reading" = "Бүх унших зүйл";
|
||||
"No Reading History" = "Унших түүх байхгүй";
|
||||
"Books you're reading will appear here" = "Таны уншиж байгаа номууд энд харагдана";
|
||||
"All Watching" = "Бүх үзэх зүйл";
|
||||
"Continue Reading" = "Унших үргэлжлүүлэх";
|
||||
"Nothing to Continue Reading" = "Үргэлжлүүлж унших зүйл байхгүй";
|
||||
"Your recently read novels will appear here" = "Таны саяхан уншсан зохиолууд энд харагдана";
|
||||
"No Bookmarks" = "Хадгалсан зүйл байхгүй";
|
||||
"Add bookmarks to this collection" = "Энэ цуглуулгад хадгалсан зүйл нэмэх";
|
||||
"items" = "зүйл";
|
||||
"Chapter %d" = "Бүлэг %d";
|
||||
"Episode %d" = "Анги %d";
|
||||
"%d%%" = "%d%%";
|
||||
"%d%% seen" = "%d%% үзсэн";
|
||||
"DownloadCountFormat" = "Татаж авсан: %d";
|
||||
"Error loading chapter" = "Бүлэг ачаалахад алдаа гарлаа";
|
||||
"Font Size: %dpt" = "Фонтын хэмжээ: %dpt";
|
||||
"Line Spacing: %.1f" = "Мөр хоорондын зай: %.1f";
|
||||
"Line Spacing" = "Мөр хоорондын зай";
|
||||
"Margin: %dpx" = "Захын зай: %dpx";
|
||||
"Margin" = "Захын зай";
|
||||
"Auto Scroll Speed" = "Автомат гүйлгэх хурд";
|
||||
"Speed" = "Хурд";
|
||||
"Speed: %.1fx" = "Хурд: %.1fx";
|
||||
"Matched %@: %@" = "Таарсан %@: %@";
|
||||
"Enter the AniList ID for this series" = "Энэ цувралын AniList ID-г оруулна уу";
|
||||
|
||||
/* New additions */
|
||||
"Create Collection" = "Цуглуулга үүсгэх";
|
||||
"Collection Name" = "Цуглуулгын нэр";
|
||||
"Rename Collection" = "Цуглуулгын нэр солих";
|
||||
"Rename" = "Нэр солих";
|
||||
"All Reading" = "Бүх унших зүйл";
|
||||
"Recently Added" = "Саяхан нэмэгдсэн";
|
||||
"Novel Title" = "Зохиолын гарчиг";
|
||||
"Read Progress" = "Уншсан явц";
|
||||
"Date Created" = "Үүсгэсэн огноо";
|
||||
"Name" = "Нэр";
|
||||
"Item Count" = "Зүйлийн тоо";
|
||||
"Date Added" = "Нэмсэн огноо";
|
||||
"Title" = "Гарчиг";
|
||||
"Source" = "Эх сурвалж";
|
||||
"Search reading..." = "Унж байгаа зүйл хайх...";
|
||||
"Search collections..." = "Цуглуулга хайх...";
|
||||
"Search bookmarks..." = "Хадгалсан зүйл хайх...";
|
||||
"%d items" = "%d зүйл";
|
||||
"Fetching Data" = "Өгөгдөл татаж байна";
|
||||
"Please wait while fetching." = "Татаж байна, хүлээнэ үү.";
|
||||
"Start Reading" = "Унж эхлэх";
|
||||
"Chapters" = "Бүлгүүд";
|
||||
"Completed" = "Дууссан";
|
||||
"Drag to reorder" = "Дарааллаар байрлуулахын тулд чирнэ үү";
|
||||
"Drag to reorder sections" = "Хэсгүүдийг дарааллаар байрлуулахын тулд чирнэ үү";
|
||||
"Library View" = "Сангийн харагдац";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Сангийн харагдах хэсгүүдийг тохируулна уу. Хэсгүүдийг дахин эрэмбэлж эсвэл бүрэн идэвхгүй болгож болно.";
|
||||
"Library Sections Order" = "Сангийн хэсгүүдийн дараалал";
|
||||
"Completion Percentage" = "Дуусгах хувь";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Зарим үйлдлүүд нь зөвхөн Sora болон Үндсэн тоглуулагч дээр ажилладаг, тухайлбал Үргэлж хэвтээ байдлаар үзэх, удаан дарж хурд удирдах болон алгасах хугацаа нь тохиргоо\n\nДуусгах хувь нь бичлэгийн төгсгөлөөс хэдэн хувийн өмнө AniList болон Trakt дээр үзсэнээр тэмдэглэхээ тодорхойлно.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Аппын кэш нь зургуудийг илүү хурдан ачаалахад тусалдаг.\n\nDocuments хавтасыг устгавал бүх татсан үзвэрүүдийг утгана.\n\nАпп датаг устгавал аппын бүх тохиргоо болон өгөгдөл устана.";
|
||||
"Translators" = "Орчуулагчид";
|
||||
"Paste URL" = "Холбоосыг буулгах";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Цувралын гарчиг";
|
||||
"Content Source" = "Агуулгын эх сурвалж";
|
||||
"Watch Progress" = "Үзсэн явц";
|
||||
"Recent searches" = "Саяхны хайлт";
|
||||
"Collections" = "Цуглуулгууд";
|
||||
"Continue Reading" = "Унших үргэлжлүүлэх";
|
||||
488
Sora/Localization/nl.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
/* General */
|
||||
"About" = "Over";
|
||||
"About Sora" = "Over Sora";
|
||||
"Active" = "Actief";
|
||||
"Active Downloads" = "Actieve Downloads";
|
||||
"Actively downloading media can be tracked from here." = "Actief downloaden van media kan hier worden gevolgd.";
|
||||
"Add Module" = "Module Toevoegen";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Pas het aantal media-items per rij aan in staande en liggende modus.";
|
||||
"Advanced" = "Geavanceerd";
|
||||
"AKA Sulfur" = "AKA Sulfur";
|
||||
"All Bookmarks" = "Alle Bladwijzers";
|
||||
"All Prev" = "Alle vorige";
|
||||
"All Watching" = "Alles Wat Ik Kijk";
|
||||
"AniList" = "AniList";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonieme gegevens worden verzameld om de app te verbeteren. Er worden geen persoonlijke gegevens verzameld. Dit kan op elk moment worden uitgeschakeld.";
|
||||
"App Info" = "App Info";
|
||||
"App Language" = "App Taal";
|
||||
"App Storage" = "App Opslag";
|
||||
"Appearance" = "Uiterlijk";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Weet je zeker dat je alle gecachte gegevens wilt wissen? Dit helpt opslagruimte vrij te maken.";
|
||||
"Are you sure you want to delete '%@'?" = "Weet je zeker dat je '%@' wilt verwijderen?";
|
||||
"Are you sure you want to delete all %1$lld episodes in '%2$@'?" = "Weet je zeker dat je alle %1$lld afleveringen in '%2$@' wilt verwijderen?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Weet je zeker dat je alle gedownloade bestanden wilt verwijderen? Je kunt ervoor kiezen om alleen de bibliotheek te wissen terwijl je de gedownloade bestanden voor later gebruik bewaart.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Weet je zeker dat je alle app-gegevens wilt wissen? Deze actie kan niet ongedaan worden gemaakt.";
|
||||
"Are you sure you want to remove all downloaded media files (.mov, .mp4, .pkg)?" = "Weet je zeker dat je alle gedownloade mediabestanden (.mov, .mp4, .pkg) wilt verwijderen?";
|
||||
"Are you sure you want to remove all files in the Documents folder?" = "Weet je zeker dat je alle bestanden in de map Documenten wilt verwijderen?";
|
||||
|
||||
/* Features */
|
||||
"Author" = "Auteur";
|
||||
"Background Enabled" = "Achtergrond Ingeschakeld";
|
||||
"Bookmark items for an easier access later." = "Bladwijzer items voor eenvoudigere toegang later.";
|
||||
"Bookmarks" = "Bladwijzers";
|
||||
"Bottom Padding" = "Onderste Padding";
|
||||
"Cancel" = "Annuleren";
|
||||
"Cellular Quality" = "Mobiele Kwaliteit";
|
||||
"Check out some community modules here!" = "Bekijk hier enkele community modules!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Kies de gewenste videoresolutie voor WiFi- en mobiele verbindingen. Hogere resoluties gebruiken meer data maar bieden een betere kwaliteit. Als de exacte kwaliteit niet beschikbaar is, wordt automatisch de dichtstbijzijnde optie geselecteerd.\n\nLet op: Niet alle videobronnen en spelers ondersteunen kwaliteitsselectie. Deze functie werkt het beste met HLS-streams via de Sora-speler.";
|
||||
"Clear" = "Wissen";
|
||||
"Clear All Downloads" = "Alle Downloads Wissen";
|
||||
"Clear Cache" = "Wis Cache";
|
||||
"Clear Library Only" = "Alleen Bibliotheek Wissen";
|
||||
"Clear Logs" = "Wis Logs";
|
||||
"Click the plus button to add a module!" = "Klik op de plus-knop om een module toe te voegen!";
|
||||
"Continue Watching" = "Verder Kijken";
|
||||
"Continue Watching Episode %d" = "Verder Kijken Aflevering %d";
|
||||
"Contributors" = "Bijdragers";
|
||||
"Copied to Clipboard" = "Gekopieerd naar Klembord";
|
||||
"Copy to Clipboard" = "Kopiëren naar Klembord";
|
||||
"Copy URL" = "Kopieer URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld Afleveringen";
|
||||
"%lld of %lld" = "%1$lld van %2$lld";
|
||||
"%lld-%lld" = "%1$lld-%2$lld";
|
||||
"%lld%% seen" = "%lld%% gezien";
|
||||
"Episode %lld" = "Aflevering %lld";
|
||||
"Episodes" = "Afleveringen";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Afleveringen zijn mogelijk nog niet beschikbaar of er is een probleem met de bron.";
|
||||
"Episodes Range" = "Afleveringen Bereik";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Donker";
|
||||
"DATA & LOGS" = "DATA & LOGS";
|
||||
"Debug" = "Debug";
|
||||
"Debugging and troubleshooting." = "Debuggen en probleemoplossing.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Verwijderen";
|
||||
"Delete All" = "Alles Wissen";
|
||||
"Delete All Downloads" = "Alle Downloads Verwijderen";
|
||||
"Delete All Episodes" = "Alle Afleveringen Wissen";
|
||||
"Delete Download" = "Downloads Wissen";
|
||||
"Delete Episode" = "Afleveringen Wissen";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Dubbel Tikken om te Zoeken";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Dubbel tikken op de zijkanten van het scherm zal overslaan met de korte tik instelling.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Downloaden";
|
||||
"Download Episode" = "Aflevering Downloaden";
|
||||
"Download Summary" = "Download Samenvatting";
|
||||
"Download This Episode" = "Download Deze Aflevering";
|
||||
"Downloaded" = "Gedownload";
|
||||
"Downloaded Shows" = "Gedownloade Series";
|
||||
"Downloading" = "Downloaden";
|
||||
"Downloads" = "Downloads";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Analytics Inschakelen";
|
||||
"Enable Subtitles" = "Ondertiteling Inschakelen";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Verwijden";
|
||||
"Erase all App Data" = "Wis Alle App Data";
|
||||
"Erase App Data" = "Verwijder App Data";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Fout";
|
||||
"Error Fetching Results" = "Fout bij Ophalen Resultaten";
|
||||
"Errors and critical issues." = "Fouten en kritieke problemen.";
|
||||
"Failed to load contributors" = "Laden van bijdragers mislukt";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Haal Aflevering Metadata op";
|
||||
"Files Downloaded" = "Gedownloade Bestanden";
|
||||
"Font Size" = "Lettergrootte";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Forceer Landschap";
|
||||
"General" = "Algemeen";
|
||||
"General events and activities." = "Algemene gebeurtenissen en activiteiten.";
|
||||
"General Preferences" = "Algemene Voorkeuren";
|
||||
"Hide Splash Screen" = "Splash Screen Verbergen";
|
||||
"HLS video downloading." = "HLS video downloaden.";
|
||||
"Hold Speed" = "Vasthouden Snelheid";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Info";
|
||||
"INFOS" = "INFO";
|
||||
"Installed Modules" = "Geïnstalleerde Modules";
|
||||
"Interface" = "Interface";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Word lid van de Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Liggende Kolommen";
|
||||
"Language" = "Taal";
|
||||
"LESS" = "MINDER";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Bibliotheek";
|
||||
"License (GPLv3.0)" = "Licentie (GPLv3.0)";
|
||||
"Light" = "Licht";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Aflevering %lld laden...";
|
||||
"Loading logs..." = "Logboeken laden...";
|
||||
"Loading module information..." = "Module-informatie laden...";
|
||||
"Loading Stream" = "Stream Laden";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Debug Info Loggen";
|
||||
"Log Filters" = "Log Filters";
|
||||
"Log In with AniList" = "Inloggen met AniList";
|
||||
"Log In with Trakt" = "Inloggen met Trakt";
|
||||
"Log Out from AniList" = "Uitloggen van AniList";
|
||||
"Log Out from Trakt" = "Uitloggen van Trakt";
|
||||
"Log Types" = "Logboek Types";
|
||||
"Logged in as" = "Ingelogd als";
|
||||
"Logged in as " = "Ingelogd als ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logboeken";
|
||||
"Long press Skip" = "Lang Drukken Overslaan";
|
||||
"MAIN" = "Hoofdinstellingen";
|
||||
"Main Developer" = "Hoofdontwikkelaar";
|
||||
"MAIN SETTINGS" = "HOOFDINSTELLINGEN";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Markeer alles als gezien";
|
||||
"Mark as Watched" = "Markeer als gezien";
|
||||
"Mark Episode as Watched" = "Markeer aflevering als gezien";
|
||||
"Mark Previous Episodes as Watched" = "Markeer vorige afleveringen als gezien";
|
||||
"Mark watched" = "Markeer als gezien";
|
||||
"Match with AniList" = "Match met AniList";
|
||||
"Match with TMDB" = "Match met TMDB";
|
||||
"Matched ID: %lld" = "Gematchte ID: %lld";
|
||||
"Matched with: %@" = "Match met: %@";
|
||||
"Max Concurrent Downloads" = "Maximaal gelijktijdige downloads";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Media Raster Layout";
|
||||
"Media Player" = "Media Speler";
|
||||
"Media View" = "Mediaweergave";
|
||||
"Metadata Provider" = "Metadata Provider";
|
||||
"Metadata Providers Order" = "Metadata Providers Volgorde";
|
||||
"Module Removed" = "Module Verwijderd";
|
||||
"Modules" = "Modules";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULES";
|
||||
"MORE" = "MEER";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Geen Actieve Downloads";
|
||||
"No AniList matches found" = "Geen AniList overeenkomsten gevonden";
|
||||
"No Data Available" = "Geen Gegevens Beschikbaar";
|
||||
"No Downloads" = "Geen Downloads";
|
||||
"No episodes available" = "Geen afleveringen beschikbaar";
|
||||
"No Episodes Available" = "Geen Afleveringen Beschikbaar";
|
||||
"No items to continue watching." = "Geen items om verder te kijken.";
|
||||
"No matches found" = "Geen overeenkomsten gevonden";
|
||||
"No Module Selected" = "Geen Module Geselecteerd";
|
||||
"No Modules" = "Geen Modules";
|
||||
"No Results Found" = "Geen Resultaten Gevonden";
|
||||
"No Search Results Found" = "Geen Zoekresultaten Gevonden";
|
||||
"Nothing to Continue Watching" = "Niets om Verder te Kijken";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Let op: de modules worden alleen vervangen als er een andere versiestring in het JSON-bestand staat.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Open Community Bibliotheek";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Openen in AniList";
|
||||
"Original Poster" = "Originele Poster";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Gepauzeerd";
|
||||
"Play" = "Afspelen";
|
||||
"Player" = "Speler";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Herstart de app om de taalwijziging toe te passen.";
|
||||
"Please select a module from settings" = "Selecteer een module uit de instellingen";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Staande Kolommen";
|
||||
"Progress bar Marker Color" = "Voortgangsbalk Markeerkleur";
|
||||
"Provider: %@" = "Provider: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Wachtrij";
|
||||
"Queued" = "In Wachtrij";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Recent bekeken inhoud verschijnt hier.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Ververs Modules bij Opstarten";
|
||||
"Refresh Storage Info" = "Opslaginformatie Vernieuwen";
|
||||
"Remember Playback speed" = "Onthoud Afspeelsnelheid";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Verwijderen";
|
||||
"Remove All Cache" = "Verwijder Alle Cache";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Verwijder Alle Documenten";
|
||||
"Remove Documents" = "Documenten Verwijderen";
|
||||
"Remove Downloaded Media" = "Gedownloade Media Verwijderen";
|
||||
"Remove Downloads" = "Verwijder Downloads";
|
||||
"Remove from Bookmarks" = "Verwijderen uit Bladwijzers";
|
||||
"Remove Item" = "Item Verwijderen";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Rapporteer een Probleem";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Resetten";
|
||||
"Reset AniList ID" = "AniList ID Resetten";
|
||||
"Reset Episode Progress" = "Afleveringsvoortgang Resetten";
|
||||
"Reset progress" = "Voortgang resetten";
|
||||
"Reset Progress" = "Voortgang Resetten";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Herstart Vereist";
|
||||
"Running Sora %@ - cranci1" = "Sora %@ draait - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Opslaan";
|
||||
"Search" = "Zoeken";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Downloads zoeken";
|
||||
"Search for something..." = "Zoek naar iets...";
|
||||
"Search..." = "Zoeken...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Seizoen %d";
|
||||
"Season %lld" = "Seizoen %lld";
|
||||
"Segments Color" = "Segmenten Kleur";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Module Selecteren";
|
||||
"Set Custom AniList ID" = "Aangepaste AniList ID Instellen";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Instellingen";
|
||||
"Shadow" = "Schaduw";
|
||||
"Show More (%lld more characters)" = "Meer Tonen (%lld meer tekens)";
|
||||
"Show PiP Button" = "Toon PiP Knop";
|
||||
"Show Skip 85s Button" = "Toon Overslaan 85s Knop";
|
||||
"Show Skip Intro / Outro Buttons" = "Toon Overslaan Intro / Outro Knoppen";
|
||||
"Shows" = "Series";
|
||||
"Size (%@)" = "Grootte (%@)";
|
||||
"Skip Settings" = "Overslaan Instellingen";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Sommige functies zijn beperkt tot de Sora en Standaard speler, zoals ForceLandscape, holdSpeed en aangepaste tijd overslaan stappen.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ door cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora en cranci1 zijn op geen enkele manier verbonden met AniList of Trakt.\n\nHoud er ook rekening mee dat voortgangsupdates mogelijk niet 100% nauwkeurig zijn.";
|
||||
"Sora GitHub Repository" = "Sora GitHub Repository";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur blijft altijd gratis zonder advertenties!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Sorteren";
|
||||
"Speed Settings" = "Snelheidsinstellingen";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Start met Kijken";
|
||||
"Start Watching Episode %d" = "Start met Kijken Aflevering %d";
|
||||
"Storage Used" = "Gebruikte Opslag";
|
||||
"Stream" = "Stream";
|
||||
"Streaming and video playback." = "Streaming en video afspelen.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Ondertitelingskleur";
|
||||
"Subtitle Settings" = "Ondertitelingsinstellingen";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Synchroniseer anime voortgang";
|
||||
"Sync TV shows progress" = "Synchroniseer TV series voortgang";
|
||||
|
||||
/* System */
|
||||
"System" = "Systeem";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Tik op een titel om de huidige match te overschrijven.";
|
||||
"Tap Skip" = "Tik Overslaan";
|
||||
"Tap to manage your modules" = "Tik om je modules te beheren";
|
||||
"Tap to select a module" = "Tik om een module te selecteren";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "De app cache helpt de app om afbeeldingen sneller te laden.\n\nHet wissen van de Documents map zal alle gedownloade modules verwijderen.\n\nWis de App Data niet tenzij je de gevolgen begrijpt — het kan ervoor zorgen dat de app niet meer goed werkt.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Het afleveringen bereik bepaalt hoeveel afleveringen er op elke pagina verschijnen. Afleveringen worden gegroepeerd in sets (zoals 1-25, 26-50, enzovoort), waardoor je er gemakkelijker doorheen kunt navigeren.\n\nVoor aflevering metadata verwijst dit naar de aflevering miniatuur en titel, aangezien deze soms spoilers kunnen bevatten.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "De module heeft slechts één aflevering geleverd, dit is waarschijnlijk een film, daarom hebben we aparte schermen gemaakt voor deze gevallen.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Miniatuur Breedte";
|
||||
"TMDB Match" = "TMDB Match";
|
||||
"Trackers" = "Trackers";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Probeer andere zoekwoorden";
|
||||
"Try different search terms" = "Probeer andere zoektermen";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Twee Vingers Vasthouden voor Pauze";
|
||||
"Unable to fetch matches. Please try again later." = "Kan geen matches ophalen. Probeer het later opnieuw.";
|
||||
"Use TMDB Poster Image" = "TMDB Poster Afbeelding Gebruiken";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Videospeler";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Video Kwaliteit Voorkeuren";
|
||||
"View All" = "Alles Bekijken";
|
||||
"Watched" = "Bekeken";
|
||||
"Why am I not seeing any episodes?" = "Waarom zie ik geen afleveringen?";
|
||||
"WiFi Quality" = "WiFi Kwaliteit";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Je bent niet ingelogd";
|
||||
"You have no items saved." = "Je hebt geen items opgeslagen.";
|
||||
"Your downloaded episodes will appear here" = "Je gedownloade afleveringen verschijnen hier";
|
||||
"Your recently watched content will appear here" = "Je recent bekeken inhoud verschijnt hier";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Download Instellingen";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Maximum gelijktijdige downloads bepaalt hoeveel afleveringen tegelijk kunnen worden gedownload. Hogere waarden kunnen meer bandbreedte en apparaatbronnen gebruiken.";
|
||||
"Quality" = "Kwaliteit";
|
||||
"Max Concurrent Downloads" = "Maximum Gelijktijdige Downloads";
|
||||
"Allow Cellular Downloads" = "Downloads via Mobiel Netwerk Toestaan";
|
||||
"Quality Information" = "Kwaliteitsinformatie";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Opslagbeheer";
|
||||
"Storage Used" = "Gebruikte Opslag";
|
||||
"Library cleared successfully" = "Bibliotheek succesvol gewist";
|
||||
"All downloads deleted successfully" = "Alle downloads succesvol verwijderd";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Recente zoekopdrachten";
|
||||
"me frfr" = "ik frfr";
|
||||
"Data" = "Gegevens";
|
||||
"Maximum Quality Available" = "Maximale beschikbare kwaliteit";
|
||||
"DownloadCountFormat" = "%d van %d";
|
||||
"Error loading chapter" = "Fout bij het laden van hoofdstuk";
|
||||
"Font Size: %dpt" = "Lettergrootte: %dpt";
|
||||
"Line Spacing: %.1f" = "Regelafstand: %.1f";
|
||||
"Line Spacing" = "Regelafstand";
|
||||
"Margin: %dpx" = "Marge: %dpx";
|
||||
"Margin" = "Marge";
|
||||
"Auto Scroll Speed" = "Automatische scrollsnelheid";
|
||||
"Speed" = "Snelheid";
|
||||
"Speed: %.1fx" = "Snelheid: %.1fx";
|
||||
"Matched %@: %@" = "Overeenkomst %@: %@";
|
||||
"Enter the AniList ID for this series" = "Voer de AniList-ID voor deze serie in";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Collectie aanmaken";
|
||||
"Collection Name" = "Collectienaam";
|
||||
"Rename Collection" = "Collectie hernoemen";
|
||||
"Rename" = "Hernoemen";
|
||||
"All Reading" = "Alles wat je leest";
|
||||
"Recently Added" = "Recent toegevoegd";
|
||||
"Novel Title" = "Roman titel";
|
||||
"Read Progress" = "Leesvoortgang";
|
||||
"Date Created" = "Aanmaakdatum";
|
||||
"Name" = "Naam";
|
||||
"Item Count" = "Aantal items";
|
||||
"Date Added" = "Datum toegevoegd";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Bron";
|
||||
"Search reading..." = "Zoek in lezen...";
|
||||
"Search collections..." = "Zoek in collecties...";
|
||||
"Search bookmarks..." = "Zoek in bladwijzers...";
|
||||
"%d items" = "%d items";
|
||||
"Fetching Data" = "Gegevens ophalen";
|
||||
"Please wait while fetching." = "Even geduld tijdens het ophalen.";
|
||||
"Start Reading" = "Begin met lezen";
|
||||
"Chapters" = "Hoofdstukken";
|
||||
"Completed" = "Voltooid";
|
||||
"Drag to reorder" = "Sleep om te herschikken";
|
||||
"Drag to reorder sections" = "Sleep om secties te herschikken";
|
||||
"Library View" = "Bibliotheekweergave";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Pas de secties aan die in je bibliotheek worden weergegeven. Je kunt secties herschikken of volledig uitschakelen.";
|
||||
"Library Sections Order" = "Volgorde van bibliotheeksecties";
|
||||
"Completion Percentage" = "Voltooiingspercentage";
|
||||
"Translators" = "Vertalers";
|
||||
"Paste URL" = "URL plakken";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serietitel";
|
||||
"Content Source" = "Inhoudsbron";
|
||||
"Watch Progress" = "Kijkvoortgang";
|
||||
"Nothing to Continue Reading" = "Niets om verder te lezen";
|
||||
"Your recently read novels will appear here" = "Je recent gelezen romans verschijnen hier";
|
||||
"No Bookmarks" = "Geen bladwijzers";
|
||||
"Add bookmarks to this collection" = "Voeg bladwijzers toe aan deze collectie";
|
||||
"items" = "items";
|
||||
"All Watching" = "Alles wat je kijkt";
|
||||
"No Reading History" = "Geen leeshistorie";
|
||||
"Books you're reading will appear here" = "Boeken die je leest verschijnen hier";
|
||||
"Create Collection" = "Collectie aanmaken";
|
||||
"Collection Name" = "Collectienaam";
|
||||
"Rename Collection" = "Collectie hernoemen";
|
||||
"Rename" = "Hernoemen";
|
||||
"Novel Title" = "Roman titel";
|
||||
"Read Progress" = "Leesvoortgang";
|
||||
"Date Created" = "Aanmaakdatum";
|
||||
"Name" = "Naam";
|
||||
"Item Count" = "Aantal items";
|
||||
"Date Added" = "Datum toegevoegd";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Bron";
|
||||
"Search reading..." = "Zoek in lezen...";
|
||||
"Search collections..." = "Zoek in collecties...";
|
||||
"Search bookmarks..." = "Zoek in bladwijzers...";
|
||||
"%d items" = "%d items";
|
||||
"Fetching Data" = "Gegevens ophalen";
|
||||
"Please wait while fetching." = "Even geduld tijdens het ophalen.";
|
||||
"Start Reading" = "Begin met lezen";
|
||||
"Chapters" = "Hoofdstukken";
|
||||
"Completed" = "Voltooid";
|
||||
"Drag to reorder" = "Sleep om te herschikken";
|
||||
"Drag to reorder sections" = "Sleep om secties te herschikken";
|
||||
"Library View" = "Bibliotheekweergave";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Pas de secties aan die in je bibliotheek worden weergegeven. Je kunt secties herschikken of volledig uitschakelen.";
|
||||
"Library Sections Order" = "Volgorde van bibliotheeksecties";
|
||||
"Completion Percentage" = "Voltooiingspercentage";
|
||||
"Translators" = "Vertalers";
|
||||
"Paste URL" = "URL plakken";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Collecties";
|
||||
"Continue Reading" = "Doorgaan met lezen";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Back-up & Herstellen";
|
||||
"Export Backup" = "Back-up exporteren";
|
||||
"Import Backup" = "Back-up importeren";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Let op: Deze functie is nog experimenteel. Controleer je gegevens na export/import.";
|
||||
"Backup" = "Back-up";
|
||||
488
Sora/Localization/nn.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
/* General */
|
||||
"About" = "Om oss";
|
||||
"About Sora" = "Om Sora";
|
||||
"Active" = "Aktiv";
|
||||
"Active Downloads" = "Aktive nedlastinger";
|
||||
"Actively downloading media can be tracked from here." = "Aktive nedlastninger av media kan spores her.";
|
||||
"Add Module" = "Legg til Modul";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Juster antall mediaelementer per rad i portrett- og landskapsmodus.";
|
||||
"Advanced" = "Avansert";
|
||||
"AKA Sulfur" = "AKA Sulfur";
|
||||
"All Bookmarks" = "Alle bokmerker";
|
||||
"All Watching" = "Alt du ser på";
|
||||
"Also known as Sulfur" = "Også kjent som Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList-ID";
|
||||
"AniList Match" = "AniList-treff";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonyme data samles inn for å forbedre appen. Ingen personlig informasjon samles inn. Dette kan deaktiveres når som helst.";
|
||||
"App Info" = "App Informasjon";
|
||||
"App Language" = "App Språk";
|
||||
"App Storage" = "App Lagring";
|
||||
"Appearance" = "Utseende";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Er du sikker på at du vil slette alle bufrede data? Dette vil hjelpe med å frigjøre lagringsplass.";
|
||||
"Are you sure you want to delete '%@'?" = "Er du sikker på at du vil slette '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Er du sikker på at du vil slette alle %1$d episodene i '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Er du sikker på at du vil slette alle nedlastede filer? Du kan velge å kun tømme biblioteket og beholde de nedlastede filene til senere bruk.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Er du sikker på at du vil slette alle appens data? Denne handlingen kan ikke angres.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Bakgrunn Aktivert";
|
||||
"Bookmark items for an easier access later." = "Bokmerk elementer for enklere tilgang senere.";
|
||||
"Bookmarks" = "Bokmerker";
|
||||
"Bottom Padding" = "Bunnutfylling";
|
||||
"Cancel" = "Avbryt";
|
||||
"Cellular Quality" = "Mobilnettkvalitet";
|
||||
"Check out some community modules here!" = "Sjekk ut noen fellesskapsmoduler her!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "Velg foretrukken videooppløsning for WiFi og mobilnett. Høyere oppløsninger bruker mer data, men gir bedre kvalitet.";
|
||||
"Clear" = "Tøm";
|
||||
"Clear All Downloads" = "Tøm alle nedlastinger";
|
||||
"Clear Cache" = "Tøm buffer";
|
||||
"Clear Library Only" = "Tøm kun bibliotek";
|
||||
"Clear Logs" = "Tøm logger";
|
||||
"Click the plus button to add a module!" = "Klikk på pluss-knappen for å legge til en modul!";
|
||||
"Continue Watching" = "Fortsett å se";
|
||||
"Continue Watching Episode %d" = "Fortsett å se Episode %d";
|
||||
"Contributors" = "Prosjektdeltaker";
|
||||
"Copied to Clipboard" = "Kopiert til Utklippstavlen";
|
||||
"Copy to Clipboard" = "Kopier til Utklippstavlen";
|
||||
"Copy URL" = "Kopier URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld Episoder";
|
||||
"%lld of %lld" = "%lld av %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% sett";
|
||||
"Episode %lld" = "Episode %lld";
|
||||
"Episodes" = "Episoder";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Det er mulig at episodene ikke er tilgjengelige ennå, eller at det er en feil med kilden.";
|
||||
"Episodes Range" = "Episoderrekkevidde";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Mørk";
|
||||
"DATA & LOGS" = "DATA & LOGGER";
|
||||
"Debug" = "Feilsøking";
|
||||
"Debugging and troubleshooting." = "Feilsøking og debugging.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Slett";
|
||||
"Delete All" = "Slett alle";
|
||||
"Delete All Downloads" = "Slett Alle Nedlastinger";
|
||||
"Delete All Episodes" = "Slett Alle Episoder";
|
||||
"Delete Download" = "Slett Nedlasting";
|
||||
"Delete Episode" = "Slett Episode";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Dobbeltklikk for å Søke";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Ved å dobbeltklikke på sidene av skjermen vil spilleren hoppe som definert i kortklikk-innstillingen.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Last ned";
|
||||
"Download Episode" = "Last ned Episode";
|
||||
"Download Summary" = "Nedlastingssammendrag";
|
||||
"Download This Episode" = "Last ned Denne Episoden";
|
||||
"Downloaded" = "Nedlastet";
|
||||
"Downloaded Shows" = "Nedlastede Serier";
|
||||
"Downloading" = "Laster ned";
|
||||
"Downloads" = "Nedlastinger";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Aktiver Analyser";
|
||||
"Enable Subtitles" = "Aktiver Undertekster";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Slett";
|
||||
"Erase all App Data" = "Slett alle App Data";
|
||||
"Erase App Data" = "Slett App Data";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Feil";
|
||||
"Error Fetching Results" = "Feil ved henting av resultater";
|
||||
"Errors and critical issues." = "Feil og kritiske problemer.";
|
||||
"Failed to load contributors" = "Kunne ikke laste prosjektdeltakere";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Hent Episode metadata";
|
||||
"Files Downloaded" = "Nedlastede Filer";
|
||||
"Font Size" = "Skriftstørrelse";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Tving Landskapsmodus";
|
||||
"General" = "Generelt";
|
||||
"General events and activities." = "Generelle hendelser og aktiviteter.";
|
||||
"General Preferences" = "Generelle Instillinger";
|
||||
"Hide Splash Screen" = "Skjul Velkomstskjerm";
|
||||
"HLS video downloading." = "HLS videonedlasting.";
|
||||
"Hold Speed" = "Midlertidig Holdehastighet";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Info";
|
||||
"INFOS" = "INFO";
|
||||
"Installed Modules" = "Installerte moduler";
|
||||
"Interface" = "Grensesnitt";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Bli med i vår Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Kolonner i Landskapsmodus";
|
||||
"Language" = "Språk";
|
||||
"LESS" = "MINDRE";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Bibliotek";
|
||||
"License (GPLv3.0)" = "Lisens (GPLv3.0)";
|
||||
"Light" = "Lys";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Laster Episode %lld...";
|
||||
"Loading logs..." = "Laster logger...";
|
||||
"Loading module information..." = "Laster modulinformasjon...";
|
||||
"Loading Stream" = "Laster Videostrøm";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Logg Feilsøkingsinfo";
|
||||
"Log Filters" = "Loggfiltre";
|
||||
"Log In with AniList" = "Logg inn med AniList";
|
||||
"Log In with Trakt" = "Logg inn med Trakt";
|
||||
"Log Out from AniList" = "Logg ut fra AniList";
|
||||
"Log Out from Trakt" = "Logg ut fra Trakt";
|
||||
"Log Types" = "Loggtyper";
|
||||
"Logged in as" = "Logget inn som";
|
||||
"Logged in as " = "Logget inn som ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logger";
|
||||
"Long press Skip" = "Langt trykk Skip";
|
||||
"MAIN" = "HOVED";
|
||||
"Main Developer" = "Hovedutvikler";
|
||||
"MAIN SETTINGS" = "HOVEDINNSTILLINGER";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Merk Alle Tidligere som Sett";
|
||||
"Mark as Watched" = "Merk som Sett";
|
||||
"Mark Episode as Watched" = "Merk Episode som Sett";
|
||||
"Mark Previous Episodes as Watched" = "Merk Tidligere Episoder som Sett";
|
||||
"Mark watched" = "Merk som Sett";
|
||||
"Match with AniList" = "Match med AniList";
|
||||
"Match with TMDB" = "Match med TMDB";
|
||||
"Matched ID: %lld" = "Matchet ID: %lld";
|
||||
"Matched with: %@" = "Matchet med: %@";
|
||||
"Max Concurrent Downloads" = "Maks Antall Parallele Nedlastinger";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Medierutenettlayout";
|
||||
"Media Player" = "Mediaspiller";
|
||||
"Media View" = "Mediavisning";
|
||||
"Metadata Provider" = "Metadata Leverandør";
|
||||
"Metadata Providers Order" = "Metadata Leverandørs Rekkefølge";
|
||||
"Module Removed" = "Modul Fjernet";
|
||||
"Modules" = "Moduler";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULER";
|
||||
"MORE" = "MER";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Ingen Aktive Nedlastinger";
|
||||
"No AniList matches found" = "Ingen AniList-treff funnet";
|
||||
"No Data Available" = "Ingen Data Tilgjengelig";
|
||||
"No Downloads" = "Ingen Nedlastinger";
|
||||
"No episodes available" = "Ingen episoder tilgjengelig";
|
||||
"No Episodes Available" = "Ingen Episoder Tilgjengelig";
|
||||
"No items to continue watching." = "Ingen elementer å fortsette å se på.";
|
||||
"No matches found" = "Ingen treff funnet";
|
||||
"No Module Selected" = "Ingen Modul Valgt";
|
||||
"No Modules" = "Ingen Moduler";
|
||||
"No Results Found" = "Ingen Resultater Funnet";
|
||||
"No Search Results Found" = "Ingen Søkeresultater Funnet";
|
||||
"Nothing to Continue Watching" = "Ingenting å fortsette å se på";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Merk at modulene erstattes kun hvis det er en annen versjonsstreng i JSON-filen.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Åpne Fellesskapsbibliotek";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Åpne i AniList";
|
||||
"Original Poster" = "Originalplakat";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pauset";
|
||||
"Play" = "Spill av";
|
||||
"Player" = "Spiller";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Appen må startes på nytt for å aktivere språkendringen.";
|
||||
"Please select a module from settings" = "Velg en modul fra innstillinger";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Kolonner i Portrettmodus";
|
||||
"Progress bar Marker Color" = "Farge på Progresjonslinje";
|
||||
"Provider: %@" = "Leverandør: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Kø";
|
||||
"Queued" = "I kø";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Nylig sett innhold vil vises her.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Oppdater Moduler ved Oppstart";
|
||||
"Refresh Storage Info" = "Oppdater Lagringsinformasjon";
|
||||
"Remember Playback speed" = "Husk Avspillingshastighet";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Fjern";
|
||||
"Remove All Cache" = "Fjern Alle Buffer";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Fjern Alle Dokumenter";
|
||||
"Remove Documents" = "Fjern Dokumenter";
|
||||
"Remove Downloaded Media" = "Fjern Nedlastet Media";
|
||||
"Remove Downloads" = "Fjern Nedlastinger";
|
||||
"Remove from Bookmarks" = "Fjern fra Bokmerker";
|
||||
"Remove Item" = "Fjern Element";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Rapporter et Problem";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Tilbakestill";
|
||||
"Reset AniList ID" = "Tilbakestill AniList-ID";
|
||||
"Reset Episode Progress" = "Tilbakestill Episodeprogresjon";
|
||||
"Reset progress" = "Tilbakestill Progresjon";
|
||||
"Reset Progress" = "Tilbakestill Progresjon";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Omstart Kreves";
|
||||
"Running Sora %@ - cranci1" = "Kjører Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Lagre";
|
||||
"Search" = "Søk";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Søk i nedlastinger";
|
||||
"Search for something..." = "Søk etter noe...";
|
||||
"Search..." = "Søk...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Sesong %d";
|
||||
"Season %lld" = "Sesong %lld";
|
||||
"Segments Color" = "Segmentfarge";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Velg Modul";
|
||||
"Set Custom AniList ID" = "Sett Egendefinert AniList-ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Innstillinger";
|
||||
"Shadow" = "Skygge";
|
||||
"Show More (%lld more characters)" = "Vis mer (%lld flere tegn)";
|
||||
"Show PiP Button" = "Vis PiP Knapp";
|
||||
"Show Skip 85s Button" = "Vis Hopp 85s Knapp";
|
||||
"Show Skip Intro / Outro Buttons" = "Vis hopp over Intro / Outro Knapper";
|
||||
"Shows" = "Serier";
|
||||
"Size (%@)" = "Størrelse (%@)";
|
||||
"Skip Settings" = "Tidshopp Innstillinger";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Noen funksjoner er begrenset til Sora og standardavspiller, som Tving Landskapsmodus, Midlertidig Holdehastighet og tilpassede tidshopp.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ av cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora og cranci1 er ikke tilknyttet AniList eller Trakt på noen måte.\n\nVær også oppmerksom på at progresjonsoppdateringer ikke nødvendigvis er 100% nøyaktige.";
|
||||
"Sora GitHub Repository" = "Sora GitHub Kodelager";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur vil alltid være gratis og uten reklame!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Sorter";
|
||||
"Speed Settings" = "Hastighetsinnstillinger";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Start å se";
|
||||
"Start Watching Episode %d" = "Start å se Episode %d";
|
||||
"Storage Used" = "Brukt Lagring";
|
||||
"Stream" = "Strøm";
|
||||
"Streaming and video playback." = "Strømming og videoavspilling.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Undertekstfarge";
|
||||
"Subtitle Settings" = "Undertekstinnstillinger";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Synkroniser anime progresjon";
|
||||
"Sync TV shows progress" = "Synkroniser TV-serie progresjon";
|
||||
|
||||
/* System */
|
||||
"System" = "System";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Trykk på en tittel for å overstyre gjeldende treff.";
|
||||
"Tap Skip" = "Trykk for å hoppe Fram / Tilbake";
|
||||
"Tap to manage your modules" = "Trykk for å administrere modulene dine";
|
||||
"Tap to select a module" = "Trykk for å velge en modul";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Appens buffer hjelper appen med å laste bilder raskere.\n\nÅ tømme dokumentmappen vil slette alle nedlastede moduler.\n\nIkke slett App Lagring med mindre du forstår konsekvensene — det kan føre til at appen ikke fungerer som den skal.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Episoderekkevidden styrer hvor mange episoder som vises på hver side. Episoder er gruppert i sett (som 1–25, 26–50, osv.), slik at du enklere kan navigere gjennom dem.\n\nEpisode-metadata refererer til episodens miniatyrbilde og tittel, da det noen ganger kan inneholde spoilers.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modulen leverte kun en episode, dette er mest sannsynlig en film, så vi bestemte oss for å lage separate sider for disse tilfellene.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Miniatyrbildebredde";
|
||||
"TMDB Match" = "TMDB Treff";
|
||||
"Trackers" = "Sporere";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Prøv andre nøkkelord";
|
||||
"Try different search terms" = "Prøv andre søkeord";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Hold to fingre for pause";
|
||||
"Unable to fetch matches. Please try again later." = "Kunne ikke finne noen treff. Vennligst prøv igjen senere.";
|
||||
"Use TMDB Poster Image" = "Bruk TMDB Plakatbilde";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Videospiller";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Videokvalitetspreferanser";
|
||||
"View All" = "Se alle";
|
||||
"Watched" = "Sett";
|
||||
"Why am I not seeing any episodes?" = "Hvorfor ser jeg ingen episoder?";
|
||||
"WiFi Quality" = "WiFi Kvalitet";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Du er ikke logget inn";
|
||||
"You have no items saved." = "Du har ingen lagrede elementer.";
|
||||
"Your downloaded episodes will appear here" = "Dine nedlastede episoder vil vises her";
|
||||
"Your recently watched content will appear here" = "Ditt nylig sette innhold vil vises her";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Nedlastingsinnstillinger";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Maks antall parallele nedlastinger styrer hvor mange episoder som kan lastes ned samtidig. Høyere verdier kan bruke mer båndbredde og enhetsressurser.";
|
||||
"Quality" = "Kvalitet";
|
||||
"Max Concurrent Downloads" = "Maks Antall Parallele Nedlastinger";
|
||||
"Allow Cellular Downloads" = "Tillat Nedlastinger over Mobilnett";
|
||||
"Quality Information" = "Kvalitetsinformasjon";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Lagringsadministrasjon";
|
||||
"Storage Used" = "Brukt Lagring";
|
||||
"Library cleared successfully" = "Bibliotek tømt";
|
||||
"All downloads deleted successfully" = "Alle nedlastinger slettet";
|
||||
|
||||
/* New keys from English localization */
|
||||
"DownloadCountFormat" = "%d av %d";
|
||||
"Error loading chapter" = "Feil ved lasting av kapittel";
|
||||
"Font Size: %dpt" = "Skriftstorleik: %dpt";
|
||||
"Line Spacing: %.1f" = "Linjeavstand: %.1f";
|
||||
"Line Spacing" = "Linjeavstand";
|
||||
"Margin: %dpx" = "Marg: %dpx";
|
||||
"Margin" = "Marg";
|
||||
"Auto Scroll Speed" = "Fart på automatisk rulling";
|
||||
"Speed" = "Fart";
|
||||
"Speed: %.1fx" = "Fart: %.1fx";
|
||||
"Matched %@: %@" = "Treff %@: %@";
|
||||
"Enter the AniList ID for this series" = "Skriv inn AniList-ID for denne serien";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Opprett samling";
|
||||
"Collection Name" = "Samlingens navn";
|
||||
"Rename Collection" = "Gi nytt navn til samling";
|
||||
"Rename" = "Gi nytt navn";
|
||||
"All Reading" = "All lesing";
|
||||
"Recently Added" = "Nylig lagt til";
|
||||
"Novel Title" = "Roman tittel";
|
||||
"Read Progress" = "Lesefremgang";
|
||||
"Date Created" = "Opprettelsesdato";
|
||||
"Name" = "Navn";
|
||||
"Item Count" = "Antall elementer";
|
||||
"Date Added" = "Dato lagt til";
|
||||
"Title" = "Tittel";
|
||||
"Source" = "Kilde";
|
||||
"Search reading..." = "Søk i lesing...";
|
||||
"Search collections..." = "Søk i samlinger...";
|
||||
"Search bookmarks..." = "Søk i bokmerker...";
|
||||
"%d items" = "%d elementer";
|
||||
"Fetching Data" = "Henter data";
|
||||
"Please wait while fetching." = "Vennligst vent mens det hentes.";
|
||||
"Start Reading" = "Start lesing";
|
||||
"Chapters" = "Kapitler";
|
||||
"Completed" = "Fullført";
|
||||
"Drag to reorder" = "Dra for å endre rekkefølge";
|
||||
"Drag to reorder sections" = "Dra for å endre rekkefølge på seksjoner";
|
||||
"Library View" = "Bibliotekvisning";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Tilpass seksjonene som vises i biblioteket ditt. Du kan endre rekkefølge eller deaktivere seksjoner helt.";
|
||||
"Library Sections Order" = "Rekkefølge på bibliotekseksjoner";
|
||||
"Completion Percentage" = "Fullføringsprosent";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Noen funksjoner er begrenset til Sora- og standardspilleren, som tvunget landskap, holdhastighet og tilpassede tidshopp.\n\nFullføringsprosenten bestemmer når før slutten av en video appen markerer den som fullført på AniList og Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Appens cache hjelper med å laste inn bilder raskere.\n\nÅ tømme Dokumenter-mappen sletter alle nedlastede moduler.\n\nÅ slette appdata sletter alle innstillinger og data.";
|
||||
"Translators" = "Oversettere";
|
||||
"Paste URL" = "Lim inn URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serietittel";
|
||||
"Content Source" = "Innhaldskjelde";
|
||||
"Watch Progress" = "Framdrift for vising";
|
||||
"Recent searches" = "Nylege søk";
|
||||
"All Reading" = "Alt du les";
|
||||
"Nothing to Continue Reading" = "Ingenting å fortsetje å lese";
|
||||
"Your recently read novels will appear here" = "Dine nyleg lesne romanar vil visast her";
|
||||
"No Bookmarks" = "Ingen bokmerke";
|
||||
"Add bookmarks to this collection" = "Legg til bokmerke i denne samlinga";
|
||||
"items" = "element";
|
||||
"All Watching" = "Alt du ser på";
|
||||
"No Reading History" = "Ingen leseloggar";
|
||||
"Books you're reading will appear here" = "Bøker du les vil visast her";
|
||||
"Create Collection" = "Opprett samling";
|
||||
"Collection Name" = "Samlingnamn";
|
||||
"Rename Collection" = "Endre namn på samling";
|
||||
"Rename" = "Endre namn";
|
||||
"Novel Title" = "Roman tittel";
|
||||
"Read Progress" = "Leseframdrift";
|
||||
"Date Created" = "Oppretta dato";
|
||||
"Name" = "Namn";
|
||||
"Item Count" = "Tal på element";
|
||||
"Date Added" = "Dato lagt til";
|
||||
"Title" = "Tittel";
|
||||
"Source" = "Kjelde";
|
||||
"Search reading..." = "Søk i lesing...";
|
||||
"Search collections..." = "Søk i samlingar...";
|
||||
"Search bookmarks..." = "Søk i bokmerke...";
|
||||
"%d items" = "%d element";
|
||||
"Fetching Data" = "Hentar data";
|
||||
"Please wait while fetching." = "Vent medan data vert henta.";
|
||||
"Start Reading" = "Start lesing";
|
||||
"Chapters" = "Kapittel";
|
||||
"Completed" = "Fullført";
|
||||
"Drag to reorder" = "Dra for å endre rekkefølgje";
|
||||
"Drag to reorder sections" = "Dra for å endre rekkefølgje på seksjonar";
|
||||
"Library View" = "Bibliotekvising";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Tilpass seksjonane som vert viste i biblioteket ditt. Du kan endre rekkefølgje eller slå dei heilt av.";
|
||||
"Library Sections Order" = "Rekkefølgje på bibliotekseksjonar";
|
||||
"Completion Percentage" = "Fullføringsprosent";
|
||||
"Translators" = "Omsetjarar";
|
||||
"Paste URL" = "Lim inn URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Samlingar";
|
||||
"Continue Reading" = "Hald fram med å lese";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Sikkerhetskopi og gjenoppretting";
|
||||
"Export Backup" = "Eksporter sikkerhetskopi";
|
||||
"Import Backup" = "Importer sikkerhetskopi";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Merk: Denne funksjonen er fortsatt eksperimentell. Vennligst dobbeltsjekk dataene dine etter eksport/import.";
|
||||
"Backup" = "Sikkerhetskopi";
|
||||
512
Sora/Localization/ru.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
/* General */
|
||||
"About" = "О программе";
|
||||
"About Sora" = "О Sora";
|
||||
"Active" = "Активные";
|
||||
"Active Downloads" = "Активные загрузки";
|
||||
"Actively downloading media can be tracked from here." = "Активно загружаемые медиафайлы можно отслеживать отсюда.";
|
||||
"Add Module" = "Добавить модуль";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Настройте количество медиаэлементов в строке в портретном и альбомном режимах.";
|
||||
"Advanced" = "Расширенные";
|
||||
"AKA Sulfur" = "Также известен как Sulfur";
|
||||
"All Bookmarks" = "Все закладки";
|
||||
"All Watching" = "Все просматриваемое";
|
||||
"Also known as Sulfur" = "Также известен как Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ID";
|
||||
"AniList Match" = "Совпадение AniList";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Анонимные данные собираются для улучшения приложения. Личная информация не собирается. Это можно отключить в любое время.";
|
||||
"App Info" = "Информация о приложении";
|
||||
"App Language" = "Язык приложения";
|
||||
"App Storage" = "Хранилище приложения";
|
||||
"Appearance" = "Внешний вид";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Вы уверены, что хотите очистить все кэшированные данные? Это поможет освободить место в хранилище.";
|
||||
"Are you sure you want to delete '%@'?" = "Вы уверены, что хотите удалить '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Вы уверены, что хотите удалить все %1$d эпизодов в '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Вы уверены, что хотите удалить все загруженные файлы? Вы можете выбрать очистку только библиотеки, сохранив загруженные файлы для будущего использования.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Вы уверены, что хотите стереть все данные приложения? Это действие нельзя отменить.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Фон включен";
|
||||
"Bookmark items for an easier access later." = "Добавляйте элементы в закладки для более легкого доступа позже.";
|
||||
"Bookmarks" = "Закладки";
|
||||
"Bottom Padding" = "Отступ снизу";
|
||||
"Cancel" = "Отмена";
|
||||
"Cellular Quality" = "Качество мобильной сети";
|
||||
"Check out some community modules here!" = "Посмотрите модули сообщества здесь!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Выберите предпочтительное разрешение видео для WiFi и мобильных соединений. Более высокие разрешения используют больше данных, но обеспечивают лучшее качество. Если точное качество недоступно, ближайший вариант будет выбран автоматически.\n\nПримечание: Не все источники видео и плееры поддерживают выбор качества. Эта функция работает лучше всего с HLS потоками, использующими плеер Sora.";
|
||||
"Clear" = "Очистить";
|
||||
"Clear All Downloads" = "Очистить все загрузки";
|
||||
"Clear Cache" = "Очистить кэш";
|
||||
"Clear Library Only" = "Очистить только библиотеку";
|
||||
"Clear Logs" = "Очистить логи";
|
||||
"Click the plus button to add a module!" = "Нажмите кнопку плюс, чтобы добавить модуль!";
|
||||
"Continue Watching" = "Продолжить просмотр";
|
||||
"Continue Watching Episode %d" = "Продолжить просмотр эпизода %d";
|
||||
"Contributors" = "Участники разработки";
|
||||
"Copied to Clipboard" = "Скопировано в буфер обмена";
|
||||
"Copy to Clipboard" = "Скопировать в буфер обмена";
|
||||
"Copy URL" = "Скопировать URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld эпизодов";
|
||||
"%lld of %lld" = "%lld из %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% просмотрено";
|
||||
"Episode %lld" = "Эпизод %lld";
|
||||
"Episodes" = "Эпизоды";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Эпизоды могут быть еще недоступны или могут быть проблемы с источником.";
|
||||
"Episodes Range" = "Диапазон эпизодов";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Темная";
|
||||
"DATA & LOGS" = "ДАННЫЕ И ЛОГИ";
|
||||
"Debug" = "Отладка";
|
||||
"Debugging and troubleshooting." = "Отладка и устранение неполадок.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Удалить";
|
||||
"Delete All" = "Удалить все";
|
||||
"Delete All Downloads" = "Удалить все загрузки";
|
||||
"Delete All Episodes" = "Удалить все эпизоды";
|
||||
"Delete Download" = "Удалить загрузку";
|
||||
"Delete Episode" = "Удалить эпизод";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Двойное касание для перемотки";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Двойное касание по бокам экрана будет перематывать в соответствии с настройкой короткого касания.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Загрузить";
|
||||
"Download Episode" = "Загрузить эпизод";
|
||||
"Download Summary" = "Сводка загрузок";
|
||||
"Download This Episode" = "Загрузить этот эпизод";
|
||||
"Downloaded" = "Загружено";
|
||||
"Downloaded Shows" = "Загруженные шоу";
|
||||
"Downloading" = "Загружается";
|
||||
"Downloads" = "Загрузки";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Включить аналитику";
|
||||
"Enable Subtitles" = "Включить субтитры";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Стереть";
|
||||
"Erase all App Data" = "Стереть все данные приложения";
|
||||
"Erase App Data" = "Стереть данные приложения";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Ошибка";
|
||||
"Error Fetching Results" = "Ошибка получения результатов";
|
||||
"Errors and critical issues." = "Ошибки и критические проблемы.";
|
||||
"Failed to load contributors" = "Не удалось загрузить участников";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Получить метаданные эпизода";
|
||||
"Files Downloaded" = "Файлы загружены";
|
||||
"Font Size" = "Размер шрифта";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Принудительный альбомный режим";
|
||||
"General" = "Общие";
|
||||
"General events and activities." = "Общие события и действия.";
|
||||
"General Preferences" = "Общие настройки";
|
||||
"Hide Splash Screen" = "Скрыть заставку";
|
||||
"HLS video downloading." = "Загрузка HLS видео.";
|
||||
"Hold Speed" = "Скорость при удержании";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Информация";
|
||||
"INFOS" = "ИНФОРМАЦИЯ";
|
||||
"Installed Modules" = "Установленные модули";
|
||||
"Interface" = "Интерфейс";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Присоединиться к Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Столбцы в альбомном режиме";
|
||||
"Language" = "Язык";
|
||||
"LESS" = "МЕНЬШЕ";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Библиотека";
|
||||
"License (GPLv3.0)" = "Лицензия (GPLv3.0)";
|
||||
"Light" = "Светлая";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Загрузка эпизода %lld...";
|
||||
"Loading logs..." = "Загрузка логов...";
|
||||
"Loading module information..." = "Загрузка информации о модуле...";
|
||||
"Loading Stream" = "Загрузка потока";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Записывать отладочную информацию";
|
||||
"Log Filters" = "Записывать фильтры";
|
||||
"Log In with AniList" = "Войти с AniList";
|
||||
"Log In with Trakt" = "Войти с Trakt";
|
||||
"Log Out from AniList" = "Выйти из AniList";
|
||||
"Log Out from Trakt" = "Выйти из Trakt";
|
||||
"Log Types" = "Записывать Типы";
|
||||
"Logged in as" = "Вошли как";
|
||||
"Logged in as " = "Вошли как ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Логи";
|
||||
"Long press Skip" = "Долгое нажатие для пропуска";
|
||||
"MAIN" = "ОСНОВНОЕ";
|
||||
"Main Developer" = "Главный разработчик";
|
||||
"MAIN SETTINGS" = "ОСНОВНЫЕ НАСТРОЙКИ";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Отметить все предыдущие как просмотренные";
|
||||
"Mark as Watched" = "Отметить как просмотренное";
|
||||
"Mark Episode as Watched" = "Отметить эпизод как просмотренный";
|
||||
"Mark Previous Episodes as Watched" = "Отметить предыдущие эпизоды как просмотренные";
|
||||
"Mark watched" = "Отметить просмотренным";
|
||||
"Match with AniList" = "Сопоставить с AniList";
|
||||
"Match with TMDB" = "Сопоставить с TMDB";
|
||||
"Matched ID: %lld" = "Сопоставленный ID: %lld";
|
||||
"Matched with: %@" = "Сопоставлено с: %@";
|
||||
"Max Concurrent Downloads" = "Максимальное количество одновременных загрузок";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Макет сетки медиа";
|
||||
"Media Player" = "Медиаплеер";
|
||||
"Media View" = "Просмотр медиа";
|
||||
"Metadata Provider" = "Поставщик метаданных";
|
||||
"Metadata Providers Order" = "Порядок поставщиков метаданных";
|
||||
"Module Removed" = "Модуль удален";
|
||||
"Modules" = "Модули";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "МОДУЛИ";
|
||||
"MORE" = "БОЛЬШЕ";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Нет активных загрузок";
|
||||
"No AniList matches found" = "Совпадений AniList не найдено";
|
||||
"No Data Available" = "Данные недоступны";
|
||||
"No Downloads" = "Нет загрузок";
|
||||
"No episodes available" = "Эпизоды недоступны";
|
||||
"No Episodes Available" = "Эпизоды недоступны";
|
||||
"No items to continue watching." = "Нет элементов для продолжения просмотра.";
|
||||
"No matches found" = "Совпадений не найдено";
|
||||
"No Module Selected" = "Модуль не выбран";
|
||||
"No Modules" = "Нет модулей";
|
||||
"No Results Found" = "Результаты не найдены";
|
||||
"No Search Results Found" = "Результаты поиска не найдены";
|
||||
"Nothing to Continue Watching" = "Нечего продолжать смотреть";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Обратите внимание, что модули будут заменены только в том случае, если в JSON-файле есть другая строка версии.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "ОК";
|
||||
"Open Community Library" = "Открыть библиотеку сообщества";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Открыть в AniList";
|
||||
"Original Poster" = "Оригинальный постер";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Приостановлено";
|
||||
"Play" = "Воспроизвести";
|
||||
"Player" = "Плеер";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Пожалуйста, перезапустите приложение, чтобы применить изменение языка.";
|
||||
"Please select a module from settings" = "Пожалуйста, выберите модуль в настройках";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Столбцы в портретном режиме";
|
||||
"Progress bar Marker Color" = "Цвет маркера полосы прогресса";
|
||||
"Provider: %@" = "Поставщик: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Очередь";
|
||||
"Queued" = "В очереди";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Недавно просмотренный контент будет отображаться здесь.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Обновлять модули при запуске";
|
||||
"Refresh Storage Info" = "Обновить информацию о хранилище";
|
||||
"Remember Playback speed" = "Запомнить скорость воспроизведения";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Удалить";
|
||||
"Remove All Cache" = "Удалить весь кэш";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Удалить все документы";
|
||||
"Remove Documents" = "Удалить документы";
|
||||
"Remove Downloaded Media" = "Удалить загруженные медиафайлы";
|
||||
"Remove Downloads" = "Удалить загрузки";
|
||||
"Remove from Bookmarks" = "Удалить из закладок";
|
||||
"Remove Item" = "Удалить элемент";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Сообщить о проблеме";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Сбросить";
|
||||
"Reset AniList ID" = "Сбросить AniList ID";
|
||||
"Reset Episode Progress" = "Сбросить прогресс эпизода";
|
||||
"Reset progress" = "Сбросить прогресс";
|
||||
"Reset Progress" = "Сбросить прогресс";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Требуется перезапуск";
|
||||
"Running Sora %@ - cranci1" = "Запущена Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Сохранить";
|
||||
"Search" = "Поиск";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Поиск загрузок";
|
||||
"Search for something..." = "Поиск чего-либо...";
|
||||
"Search..." = "Поиск...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Сезон %d";
|
||||
"Season %lld" = "Сезон %lld";
|
||||
"Segments Color" = "Цвет сегментов";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Выбрать модуль";
|
||||
"Set Custom AniList ID" = "Установить пользовательский AniList ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Настройки";
|
||||
"Shadow" = "Тень";
|
||||
"Show More (%lld more characters)" = "Показать больше (еще %lld символов)";
|
||||
"Show PiP Button" = "Показать кнопку PiP";
|
||||
"Show Skip 85s Button" = "Показать кнопку пропуска 85с";
|
||||
"Show Skip Intro / Outro Buttons" = "Показать кнопки пропуска интро/аутро";
|
||||
"Shows" = "Шоу";
|
||||
"Size (%@)" = "Размер (%@)";
|
||||
"Skip Settings" = "Настройки перемотки";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Некоторые функции ограничены плеерами Sora и Default, такие как принудительный альбомный режим, скорость при удержании и пользовательские интервалы перемотки времени.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ от cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "Sora и cranci1 никак не связаны с AniList или Trakt.
|
||||
|
||||
Также обратите внимание, что обновления прогресса могут быть не на 100% точными.";
|
||||
"Sora GitHub Repository" = "Репозиторий Sora на GitHub";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur всегда останется бесплатной без рекламы!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Сортировка";
|
||||
"Speed Settings" = "Настройки скорости";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Начать просмотр";
|
||||
"Start Watching Episode %d" = "Начать просмотр эпизода %d";
|
||||
"Storage Used" = "Использовано хранилища";
|
||||
"Stream" = "Поток";
|
||||
"Streaming and video playback." = "Потоковая передача и воспроизведение видео.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Цвет субтитров";
|
||||
"Subtitle Settings" = "Настройки субтитров";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Синхронизировать прогресс аниме";
|
||||
"Sync TV shows progress" = "Синхронизировать прогресс ТВ-шоу";
|
||||
|
||||
/* System */
|
||||
"System" = "Система";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Нажмите на название, чтобы переопределить текущее совпадение.";
|
||||
"Tap Skip" = "Пропуск нажатием";
|
||||
"Tap to manage your modules" = "Нажмите, чтобы управлять модулями";
|
||||
"Tap to select a module" = "Нажмите, чтобы выбрать модуль";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.
|
||||
|
||||
Clearing the Documents folder will delete all downloaded modules.
|
||||
|
||||
Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Кэш приложения помогает приложению загружать изображения быстрее.
|
||||
|
||||
Очистка папки документов удалит все загруженные модули.
|
||||
|
||||
Не стирайте данные приложения, если не понимаете последствий — это может привести к сбоям в работе приложения.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.
|
||||
|
||||
For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Диапазон эпизодов определяет, сколько эпизодов отображается на каждой странице. Эпизоды группируются в наборы (например, 1–25, 26–50 и т. д.), что упрощает навигацию.
|
||||
|
||||
Для метаданных эпизода это относится к миниатюре и названию эпизода, поскольку иногда они могут содержать спойлеры.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Модуль предоставил только один эпизод, это скорее всего фильм, поэтому мы решили сделать отдельные экраны для таких случаев.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Ширина миниатюр";
|
||||
"TMDB Match" = "Совпадение TMDB";
|
||||
"Trackers" = "Трекеры";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Попробуйте другие ключевые слова";
|
||||
"Try different search terms" = "Попробуйте другие поисковые термины";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Удержание двумя пальцами для паузы";
|
||||
"Unable to fetch matches. Please try again later." = "Не удается получить совпадения. Пожалуйста, попробуйте позже.";
|
||||
"Use TMDB Poster Image" = "Использовать изображение постера TMDB";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Видеоплеер";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Настройки качества видео";
|
||||
"View All" = "Просмотреть все";
|
||||
"Watched" = "Просмотрено";
|
||||
"Why am I not seeing any episodes?" = "Почему я не вижу эпизодов?";
|
||||
"WiFi Quality" = "Качество WiFi";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Вы не вошли в";
|
||||
"You have no items saved." = "У вас нет сохраненных элементов.";
|
||||
"Your downloaded episodes will appear here" = "Ваши загруженные эпизоды будут отображаться здесь";
|
||||
"Your recently watched content will appear here" = "Ваш недавно просмотренный контент будет отображаться здесь";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Настройки загрузки";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Максимальное количество одновременных загрузок контролирует, сколько эпизодов могут загружаться одновременно. Более высокие значения могут использовать больше пропускной способности и ресурсов устройства.";
|
||||
"Quality" = "Качество";
|
||||
"Max Concurrent Downloads" = "Максимальное количество одновременных загрузок";
|
||||
"Allow Cellular Downloads" = "Разрешить загрузки по мобильной сети";
|
||||
"Quality Information" = "Информация о качестве";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Управление хранилищем";
|
||||
"Storage Used" = "Использовано хранилища";
|
||||
"Library cleared successfully" = "Библиотека успешно очищена";
|
||||
"All downloads deleted successfully" = "Все загрузки успешно удалены";
|
||||
|
||||
/* Recent searches */
|
||||
"Recent searches" = "Недавние поиски";
|
||||
"me frfr" = "я фрфр";
|
||||
"Data" = "Данные";
|
||||
|
||||
/* New string */
|
||||
"Maximum Quality Available" = "Максимальное доступное качество";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d из %d";
|
||||
"Error loading chapter" = "Ошибка загрузки главы";
|
||||
"Font Size: %dpt" = "Размер шрифта: %dpt";
|
||||
"Line Spacing: %.1f" = "Межстрочный интервал: %.1f";
|
||||
"Line Spacing" = "Межстрочный интервал";
|
||||
"Margin: %dpx" = "Поле: %dpx";
|
||||
"Margin" = "Поле";
|
||||
"Auto Scroll Speed" = "Скорость автопрокрутки";
|
||||
"Speed" = "Скорость";
|
||||
"Speed: %.1fx" = "Скорость: %.1fx";
|
||||
"Matched %@: %@" = "Совпадение %@: %@";
|
||||
"Enter the AniList ID for this series" = "Введите AniList ID для этой серии";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Создать коллекцию";
|
||||
"Collection Name" = "Название коллекции";
|
||||
"Rename Collection" = "Переименовать коллекцию";
|
||||
"Rename" = "Переименовать";
|
||||
"All Reading" = "Все чтения";
|
||||
"Recently Added" = "Недавно добавленные";
|
||||
"Novel Title" = "Название романа";
|
||||
"Read Progress" = "Прогресс чтения";
|
||||
"Date Created" = "Дата создания";
|
||||
"Name" = "Имя";
|
||||
"Item Count" = "Количество элементов";
|
||||
"Date Added" = "Дата добавления";
|
||||
"Title" = "Заголовок";
|
||||
"Source" = "Источник";
|
||||
"Search reading..." = "Поиск по чтению...";
|
||||
"Search collections..." = "Поиск по коллекциям...";
|
||||
"Search bookmarks..." = "Поиск по закладкам...";
|
||||
"%d items" = "%d элементов";
|
||||
"Fetching Data" = "Получение данных";
|
||||
"Please wait while fetching." = "Пожалуйста, подождите, идет получение данных.";
|
||||
"Start Reading" = "Начать чтение";
|
||||
"Chapters" = "Главы";
|
||||
"Completed" = "Завершено";
|
||||
"Drag to reorder" = "Перетащите для изменения порядка";
|
||||
"Drag to reorder sections" = "Перетащите для изменения порядка разделов";
|
||||
"Library View" = "Вид библиотеки";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Настройте разделы, отображаемые в вашей библиотеке. Вы можете изменить порядок разделов или полностью их отключить.";
|
||||
"Library Sections Order" = "Порядок разделов библиотеки";
|
||||
"Completion Percentage" = "Процент завершения";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Некоторые функции доступны только в Sora и стандартном плеере, такие как принудительный ландшафт, удержание скорости и пользовательские интервалы пропуска.\n\nНастройка процента завершения определяет, в какой момент до конца видео приложение отметит его как завершенное на AniList и Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Кэш приложения помогает быстрее загружать изображения.\n\nОчистка папки Documents удалит все загруженные модули.\n\nСтирание данных приложения удалит все ваши настройки и данные.";
|
||||
"Translators" = "Переводчики";
|
||||
"Paste URL" = "Вставить URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Название серии";
|
||||
"Content Source" = "Источник контента";
|
||||
"Watch Progress" = "Прогресс просмотра";
|
||||
"Recent searches" = "Недавние поиски";
|
||||
"All Reading" = "Всё для чтения";
|
||||
"Nothing to Continue Reading" = "Нечего продолжать читать";
|
||||
"Your recently read novels will appear here" = "Ваши недавно прочитанные романы появятся здесь";
|
||||
"No Bookmarks" = "Нет закладок";
|
||||
"Add bookmarks to this collection" = "Добавьте закладки в эту коллекцию";
|
||||
"items" = "элементы";
|
||||
"All Watching" = "Всё для просмотра";
|
||||
"No Reading History" = "Нет истории чтения";
|
||||
"Books you're reading will appear here" = "Книги, которые вы читаете, появятся здесь";
|
||||
"Create Collection" = "Создать коллекцию";
|
||||
"Collection Name" = "Название коллекции";
|
||||
"Rename Collection" = "Переименовать коллекцию";
|
||||
"Rename" = "Переименовать";
|
||||
"Novel Title" = "Название романа";
|
||||
"Read Progress" = "Прогресс чтения";
|
||||
"Date Created" = "Дата создания";
|
||||
"Name" = "Имя";
|
||||
"Item Count" = "Количество элементов";
|
||||
"Date Added" = "Дата добавления";
|
||||
"Title" = "Заголовок";
|
||||
"Source" = "Источник";
|
||||
"Search reading..." = "Поиск по чтению...";
|
||||
"Search collections..." = "Поиск по коллекциям...";
|
||||
"Search bookmarks..." = "Поиск по закладкам...";
|
||||
"%d items" = "%d элементов";
|
||||
"Fetching Data" = "Получение данных";
|
||||
"Please wait while fetching." = "Пожалуйста, подождите, идет получение данных.";
|
||||
"Start Reading" = "Начать чтение";
|
||||
"Chapters" = "Главы";
|
||||
"Completed" = "Завершено";
|
||||
"Drag to reorder" = "Перетащите для изменения порядка";
|
||||
"Drag to reorder sections" = "Перетащите для изменения порядка разделов";
|
||||
"Library View" = "Вид библиотеки";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Настройте разделы, отображаемые в вашей библиотеке. Вы можете изменить порядок разделов или полностью их отключить.";
|
||||
"Library Sections Order" = "Порядок разделов библиотеки";
|
||||
"Completion Percentage" = "Процент завершения";
|
||||
"Translators" = "Переводчики";
|
||||
"Paste URL" = "Вставить URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Коллекции";
|
||||
"Continue Reading" = "Продолжить чтение";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Резервное копирование и восстановление";
|
||||
"Export Backup" = "Экспорт резервной копии";
|
||||
"Import Backup" = "Импорт резервной копии";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Внимание: Эта функция все еще экспериментальная. Пожалуйста, проверьте свои данные после экспорта/импорта.";
|
||||
"Backup" = "Резервная копия";
|
||||
508
Sora/Localization/sk.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
/* General */
|
||||
"About" = "O aplikácii";
|
||||
"About Sora" = "O aplikácii Sora";
|
||||
"Active" = "Aktívne";
|
||||
"Active Downloads" = "Aktívne sťahovania";
|
||||
"Actively downloading media can be tracked from here." = "Aktívne preberané médiá môžete sledovať tu.";
|
||||
"Add Module" = "Pridať modul";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Nastavte počet mediálnych položiek na riadok v režime na výšku a na šírku.";
|
||||
"Advanced" = "Pokročilé";
|
||||
"AKA Sulfur" = "AKA Sulfur";
|
||||
"All Bookmarks" = "Všetky záložky";
|
||||
"All Watching" = "Všetky rozpozerané";
|
||||
"Also known as Sulfur" = "Tiež známe ako Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "ID AniList";
|
||||
"AniList Match" = "AniList Match";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonymné údaje sa zbierajú na zlepšenie aplikácie. Žiadne osobné údaje sa nezbierajú. Túto možnosť môžete kedykoľvek vypnúť.";
|
||||
"App Info" = "Informácie o aplikácii";
|
||||
"App Language" = "Jazyk aplikácie";
|
||||
"App Storage" = "Ukladací priestor aplikácie";
|
||||
"Appearance" = "Vzhľad";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Naozaj chcete vymazať všetku medzipamäť? Pomôže to uvoľniť miesto na disku.";
|
||||
"Are you sure you want to delete '%@'?" = "Naozaj chcete zmazať '%@'?";
|
||||
"Are you sure you want to delete all %1\$d episodes in '%2\$@'?" = "Naozaj chcete zmazať všetkých %1\$d epizód v '%2\$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Naozaj chcete zmazať všetky stiahnuté súbory? Môžete vymazať iba knižnicu a stiahnuté súbory si ponechať.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Naozaj chcete vymazať všetky dáta aplikácie? Túto akciu nie je možné vrátiť späť.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "V pozadí povolené";
|
||||
"Bookmark items for an easier access later." = "Označte položky záložkou pre ľahší prístup neskôr.";
|
||||
"Bookmarks" = "Záložky";
|
||||
"Bottom Padding" = "Spodná výplň";
|
||||
"Cancel" = "Zrušiť";
|
||||
"Cellular Quality" = "Kvalita cez mobilné dáta";
|
||||
"Check out some community modules here!" = "Prezrite si nejaké komunitou vytvorené moduly tu!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Vyberte preferované video rozlíšenie pre WiFi a mobilné pripojenia. Vyššie rozlíšenia používajú viac dát, ale poskytujú lepšiu kvalitu. Ak presné rozlíšenie nie je dostupné, automaticky sa vyberie najbližšia možnosť.\n\nPoznámka: Nie všetky video zdroje a prehrávače podporujú výber kvality. Táto funkcia funguje najlepšie s HLS streamami pomocou prehrávača Sora.";
|
||||
"Clear" = "Vymazať";
|
||||
"Clear All Downloads" = "Vymazať všetky sťiahnuté položky";
|
||||
"Clear Cache" = "Vymazať medzipamäť";
|
||||
"Clear Library Only" = "Vymazať len knižnicu";
|
||||
"Clear Logs" = "Vymazať záznamy";
|
||||
"Click the plus button to add a module!" = "Kliknite na tlačidlo plus pre pridanie modulu!";
|
||||
"Continue Watching" = "Pokračovať v sledovaní";
|
||||
"Continue Watching Episode %d" = "Pokračovať v sledovaní epizódy %d";
|
||||
"Contributors" = "Prispievatelia";
|
||||
"Copied to Clipboard" = "Skopírované do schránky";
|
||||
"Copy to Clipboard" = "Kopírovať do schránky";
|
||||
"Copy URL" = "Kopírovať URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld epizód";
|
||||
"%lld of %lld" = "%lld z %lld";
|
||||
"%lld-%lld" = "%lld–%lld";
|
||||
"%lld%% seen" = "%lld%% zhliadnuté";
|
||||
"Episode %lld" = "Epizóda %lld";
|
||||
"Episodes" = "Epizódy";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Epizódy môžu byť ešte nedostupné alebo môže byť problém so zdrojom.";
|
||||
"Episodes Range" = "Rozsah epizód";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Tmavé";
|
||||
"DATA & LOGS" = "DÁTA A ZÁZNAMY";
|
||||
"Debug" = "Ladenie";
|
||||
"Debugging and troubleshooting." = "Ladenie a troubleshooting.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Zmazať";
|
||||
"Delete All" = "Zmazať všetko";
|
||||
"Delete All Downloads" = "Zmazať všetky preberania";
|
||||
"Delete All Episodes" = "Zmazať všetky epizódy";
|
||||
"Delete Download" = "Zmazať preberanie";
|
||||
"Delete Episode" = "Zmazať epizódu";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Dvojité klepnutie pre skok";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Dvojité klepnutie na okraje obrazovky preskočí podľa nastavenia krátkeho klepnutia.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Stiahnuť";
|
||||
"Download Episode" = "Stiahnuť epizódu";
|
||||
"Download Summary" = "Súhrn sťiahnutých";
|
||||
"Download This Episode" = "Stiahnuť túto epizódu";
|
||||
"Downloaded" = "Stiahnuté";
|
||||
"Downloaded Shows" = "Stiahnuté shows";
|
||||
"Downloading" = "Sťahuje sa";
|
||||
"Downloads" = "Sťiahnuté";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Povoliť analytiku";
|
||||
"Enable Subtitles" = "Povoliť titulky";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Vymazať";
|
||||
"Erase all App Data" = "Vymazať všetky dáta aplikácie";
|
||||
"Erase App Data" = "Vymazať dáta aplikácie";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Chyba";
|
||||
"Error Fetching Results" = "Chyba pri získavaní výsledkov";
|
||||
"Errors and critical issues." = "Chyby a kritické problémy.";
|
||||
"Failed to load contributors" = "Nepodarilo sa načítať prispievateľov";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Získať metadata epizódy";
|
||||
"Files Downloaded" = "Stiahnuté súbory";
|
||||
"Font Size" = "Veľkosť písma";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Vynútiť režim na šírku";
|
||||
"General" = "Všeobecné";
|
||||
"General events and activities." = "Všeobecné udalosti a aktivity.";
|
||||
"General Preferences" = "Všeobecné nastavenia";
|
||||
"Hide Splash Screen" = "Skryť uvítaciu obrazovku";
|
||||
"HLS video downloading." = "Sťahovanie HLS videa.";
|
||||
"Hold Speed" = "Rýchlosť podržania";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Informácie";
|
||||
"INFOS" = "INFORMÁCIE";
|
||||
"Installed Modules" = "Inštalované moduly";
|
||||
"Interface" = "Rozhranie";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Pripojte sa k nášmu Discordu";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Stĺpce v režime na šírku";
|
||||
"Language" = "Jazyk";
|
||||
"LESS" = "MENEJ";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Knižnica";
|
||||
"License (GPLv3.0)" = "Licencia (GPLv3.0)";
|
||||
"Light" = "Svetlé";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Načítava sa epizóda %lld...";
|
||||
"Loading logs..." = "Načítavajú sa záznamy...";
|
||||
"Loading module information..." = "Načítavajú sa informácie o module...";
|
||||
"Loading Stream" = "Načítava sa stream";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Zaznamenať ladacie informácie";
|
||||
"Log Filters" = "Filtre záznamov";
|
||||
"Log In with AniList" = "Prihlásiť sa cez AniList";
|
||||
"Log In with Trakt" = "Prihlásiť sa cez Trakt";
|
||||
"Log Out from AniList" = "Odhlásiť sa z AniList";
|
||||
"Log Out from Trakt" = "Odhlásiť sa z Trakt";
|
||||
"Log Types" = "Typy záznamov";
|
||||
"Logged in as" = "Prihlásený ako";
|
||||
"Logged in as " = "Prihlásený ako ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Záznamy";
|
||||
"Long press Skip" = "Dlhé stlačenie preskočí";
|
||||
"MAIN" = "HLAVNÉ";
|
||||
"Main Developer" = "Hlavný vývojár";
|
||||
"MAIN SETTINGS" = "HLAVNÉ NASTAVENIA";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Označiť všetky predchádzajúce ako zhliadnuté";
|
||||
"Mark as Watched" = "Označiť ako zhliadnuté";
|
||||
"Mark Episode as Watched" = "Označiť epizódu ako zhliadnutú";
|
||||
"Mark Previous Episodes as Watched" = "Označiť predchádzajúce epizódy ako zhliadnuté";
|
||||
"Mark watched" = "Označiť ako zhliadnuté";
|
||||
"Match with AniList" = "Match with AniList";
|
||||
"Match with TMDB" = "Match with TMDB";
|
||||
"Matched ID: %lld" = "Matched ID: %lld";
|
||||
"Matched with: %@" = "Matched with: %@";
|
||||
"Max Concurrent Downloads" = "Maximálny počet súbežných šťahovaní";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Rozloženie mriežky médií";
|
||||
"Media Player" = "Prehrávač médií";
|
||||
"Media View" = "Zobrazenie médií";
|
||||
"Metadata Provider" = "Poskytovateľ metadát";
|
||||
"Metadata Providers Order" = "Poradie poskytovateľov metadata";
|
||||
"Module Removed" = "Modul odstránený";
|
||||
"Modules" = "Moduly";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULY";
|
||||
"MORE" = "VIAC";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Žiadne aktívne sťahovania";
|
||||
"No AniList matches found" = "No AniList matches found";
|
||||
"No Data Available" = "Žiadne dostupné dáta";
|
||||
"No Downloads" = "Žiadne sťahovania";
|
||||
"No episodes available" = "Žiadne dostupné epizódy";
|
||||
"No Episodes Available" = "Žiadne dostupné epizódy";
|
||||
"No items to continue watching." = "Žiadne položky na pokračovanie v sledovaní.";
|
||||
"No matches found" = "Nenašli sa žiadne zhody";
|
||||
"No Module Selected" = "Nie je vybratý žiaden modul";
|
||||
"No Modules" = "Žiadne moduly";
|
||||
"No Results Found" = "Nenašli sa žiadne výsledky";
|
||||
"No Search Results Found" = "Nenašli sa žiadne výsledky vyhľadávania";
|
||||
"Nothing to Continue Watching" = "Niž nič na pokračovanie v sledovaní";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Moduly sa nahradia iba v prípade, že v JSON súbore je odlišná verzia.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Otvoriť komunitnú knižnicu";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Otvoriť v AniList";
|
||||
"Original Poster" = "Pôvodný plagát";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pozastavené";
|
||||
"Play" = "Prehrať";
|
||||
"Player" = "Prehrávač";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Reštartujte aplikáciu pre zmenu jazyka.";
|
||||
"Please select a module from settings" = "Vyberte modul v nastaveniach";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Stĺpce na výšku";
|
||||
"Progress bar Marker Color" = "Farba značky na časového úseku";
|
||||
"Provider: %@" = "Poskytovateľ: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Fronta";
|
||||
"Queued" = "Vo fronte";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Nedávno sledovaný obsah sa zobrazí tu.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Obnoviť moduly pri spustení";
|
||||
"Refresh Storage Info" = "Obnoviť informácie o úložisku";
|
||||
"Remember Playback speed" = "Zapamätať rýchlosť prehrávania";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Odstrániť";
|
||||
"Remove All Cache" = "Odstrániť všetku medzipamäť";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Odstrániť všetky dokumenty";
|
||||
"Remove Documents" = "Odstrániť dokumenty";
|
||||
"Remove Downloaded Media" = "Odstrániť stiahnuté médiá";
|
||||
"Remove Downloads" = "Odstrániť preberania";
|
||||
"Remove from Bookmarks" = "Odstrániť zo záložiek";
|
||||
"Remove Item" = "Odstrániť položku";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Nahlásiť problém";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Obnoviť";
|
||||
"Reset AniList ID" = "Obnoviť ID AniList";
|
||||
"Reset Episode Progress" = "Obnoviť pokrok epizódy";
|
||||
"Reset progress" = "Obnoviť pokrok";
|
||||
"Reset Progress" = "Obnoviť pokrok";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Vyžaduje sa reštart";
|
||||
"Running Sora %@ - cranci1" = "Spustená Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Uložiť";
|
||||
"Search" = "Vyhľadať";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Vyhľadávať preberania";
|
||||
"Search for something..." = "Hľadať niečo...";
|
||||
"Search..." = "Hľadať...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Séria %d";
|
||||
"Season %lld" = "Séria %lld";
|
||||
"Segments Color" = "Farba segmentov";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Vybrať modul";
|
||||
"Set Custom AniList ID" = "Nastaviť vlastné AniList ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Nastavenia";
|
||||
"Shadow" = "Tieň";
|
||||
"Show More (%lld more characters)" = "Zobraziť viac (%lld znakov)";
|
||||
"Show PiP Button" = "Zobraziť tlačidlo PiP";
|
||||
"Show Skip 85s Button" = "Zobraziť tlačidlo preskočiť 85s";
|
||||
"Show Skip Intro / Outro Buttons" = "Zobraziť tlačidlá preskočiť úvod / záver";
|
||||
"Shows" = "Relácie";
|
||||
"Size (%@)" = "Veľkosť (%@)";
|
||||
"Skip Settings" = "Nastavenia preskočenia";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Niektoré funkcie sú obmedzené na prehrávač Sora a predvolený prehrávač, ako ForceLandscape, holdSpeed a vlastné inkrementy preskočenia času.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ od cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "Sora a cranci1 nie sú v žiadnom prípade spojené s AniList alebo Trakt.
|
||||
|
||||
Upozorňujeme, že aktualizácie pozerania nemusia byť stopercentne presné.";
|
||||
"Sora GitHub Repository" = "GitHub repozitár Sora";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur bude vždy zadarmo bez reklám!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Zoradiť";
|
||||
"Speed Settings" = "Nastavenia rýchlosti";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Začať pozerať";
|
||||
"Start Watching Episode %d" = "Začať pozerať epizódu %d";
|
||||
"Storage Used" = "Použitá pamäť";
|
||||
"Stream" = "Stream";
|
||||
"Streaming and video playback." = "Streamovanie a prehrávanie videa.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Farba tituliek";
|
||||
"Subtitle Settings" = "Nastavenia tituliek";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Synchronizovať pozeranie anime";
|
||||
"Sync TV shows progress" = "Synchronizovať pozeranie TV relácií";
|
||||
|
||||
/* System */
|
||||
"System" = "Systém";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Klepnite na názov prepíšte aktuálnu zhodu.";
|
||||
"Tap Skip" = "Klepnite na Preskočiť";
|
||||
"Tap to manage your modules" = "Klepnite pre správu modulov";
|
||||
"Tap to select a module" = "Klepnite na výber modulu";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.
|
||||
|
||||
Clearing the Documents folder will delete all downloaded modules.
|
||||
|
||||
Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Medzipamäť aplikácie pomáha aplikácii rýchlejšie načítať obrázky.
|
||||
|
||||
Vyprázdnením priečinka Dokumenty sa odstránia všetky stiahnuté moduly.
|
||||
|
||||
Neodstraňujte dáta aplikácie, ak nerozumiete následkom — môže to spôsobiť nesprávne fungovanie aplikácie.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.
|
||||
|
||||
For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Rozsah epizód riadi, koľko epizód sa zobrazí na každej stránke. Epizódy sú zoskupené do sád (napr. 1–25, 26–50 atď.), čo vám umožňuje ľahšie sa medzi nimi pohybovať.
|
||||
|
||||
Čo sa týka metadata epizódy, ide o náhľad epizódy a názov, pretože niekedy môže obsahovať spoilery.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modul poskytol iba jednu epizódu; pravdepodobne ide o film, preto sme sa rozhodli vytvoriť pre tieto prípady samostatné obrazovky.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Šírka náhľadov";
|
||||
"TMDB Match" = "Match s TMDB";
|
||||
"Trackers" = "Sledovače";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Skúste iné kľúčové slová";
|
||||
"Try different search terms" = "Skúste iné vyhľadávacie výrazy";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Podržanie dvoma prstami na pozastavenie";
|
||||
"Unable to fetch matches. Please try again later." = "Nie je možné načítať zhody. Skúste to prosím neskôr.";
|
||||
"Use TMDB Poster Image" = "Použiť plagát z TMDB";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Video prehrávač";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Preferencie kvality videa";
|
||||
"View All" = "Zobraziť všetko";
|
||||
"Watched" = "Zhliadnuté";
|
||||
"Why am I not seeing any episodes?" = "Prečo nevidím žiadne epizódy?";
|
||||
"WiFi Quality" = "Kvalita WiFi";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Nie ste prihlásený";
|
||||
"You have no items saved." = "Nemáte uložené žiadne položky.";
|
||||
"Your downloaded episodes will appear here" = "Stiahnuté epizódy sa zobrazia tu";
|
||||
"Your recently watched content will appear here" = "Nedávno zhliadnutý obsah sa zobrazí tu";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Nastavenia sťahovania";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Maximálny počet súbežných sťahovaní určuje, koľko epizód sa môže sťahovať v jeden moment. Vyššie hodnoty môžu využívať viacej internetu/dát a zaťažovať zariadenie.";
|
||||
"Quality" = "Kvalita";
|
||||
"Max Concurrent Downloads" = "Maximálne súbežné sťahovania";
|
||||
"Allow Cellular Downloads" = "Povoliť sťahovanie cez mobilné dáta";
|
||||
"Quality Information" = "Informácie o kvalite";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Správa ukladacieho priestoru";
|
||||
"Storage Used" = "Použitý priestor";
|
||||
"Library cleared successfully" = "Knižnica bola úspešne vymazaná";
|
||||
"All downloads deleted successfully" = "Všetky sťahovania boli úspešne zmazané";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Nedávne vyhľadávania";
|
||||
"me frfr" = "me frfr";
|
||||
"Data" = "Dáta";
|
||||
"Maximum Quality Available" = "Maximálna dostupná kvalita";
|
||||
|
||||
/* New additions */
|
||||
"DownloadCountFormat" = "%d z %d";
|
||||
"Error loading chapter" = "Chyba pri načítaní kapitoly";
|
||||
"Font Size: %dpt" = "Veľkosť písma: %dpt";
|
||||
"Line Spacing: %.1f" = "Riadkovanie: %.1f";
|
||||
"Line Spacing" = "Riadkovanie";
|
||||
"Margin: %dpx" = "Okraj: %dpx";
|
||||
"Margin" = "Okraj";
|
||||
"Auto Scroll Speed" = "Rýchlosť automatického posúvania";
|
||||
"Speed" = "Rýchlosť";
|
||||
"Speed: %.1fx" = "Rýchlosť: %.1fx";
|
||||
"Matched %@: %@" = "Zhoda %@: %@";
|
||||
"Enter the AniList ID for this series" = "Zadajte AniList ID pre túto sériu";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Vytvoriť kolekciu";
|
||||
"Collection Name" = "Názov kolekcie";
|
||||
"Rename Collection" = "Premenovať kolekciu";
|
||||
"Rename" = "Premenovať";
|
||||
"All Reading" = "Všetko na čítanie";
|
||||
"Recently Added" = "Nedávno pridané";
|
||||
"Novel Title" = "Názov románu";
|
||||
"Read Progress" = "Priebeh čítania";
|
||||
"Date Created" = "Dátum vytvorenia";
|
||||
"Name" = "Meno";
|
||||
"Item Count" = "Počet položiek";
|
||||
"Date Added" = "Dátum pridania";
|
||||
"Title" = "Názov";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hľadať v čítaní...";
|
||||
"Search collections..." = "Hľadať v kolekciách...";
|
||||
"Search bookmarks..." = "Hľadať v záložkách...";
|
||||
"%d items" = "%d položiek";
|
||||
"Fetching Data" = "Načítavanie údajov";
|
||||
"Please wait while fetching." = "Počkajte, kým sa údaje načítajú.";
|
||||
"Start Reading" = "Začať čítať";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončené";
|
||||
"Drag to reorder" = "Potiahnite na zmenu poradia";
|
||||
"Drag to reorder sections" = "Potiahnite na zmenu poradia sekcií";
|
||||
"Library View" = "Zobrazenie knižnice";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prispôsobte sekcie zobrazené vo vašej knižnici. Môžete zmeniť ich poradie alebo ich úplne vypnúť.";
|
||||
"Library Sections Order" = "Poradie sekcií knižnice";
|
||||
"Completion Percentage" = "Percento dokončenia";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Niektoré funkcie sú obmedzené na Sora a predvolený prehrávač, ako je vynútená krajina, podržanie rýchlosti a vlastné intervaly preskočenia.\n\nNastavenie percenta dokončenia určuje, v ktorom bode pred koncom videa aplikácia označí ako dokončené na AniList a Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Vyrovnávacia pamäť aplikácie pomáha rýchlejšiemu načítaniu obrázkov.\n\nVymazanie priečinka Documents odstráni všetky stiahnuté moduly.\n\nVymazanie údajov aplikácie odstráni všetky vaše nastavenia a údaje.";
|
||||
"Translators" = "Prekladatelia";
|
||||
"Paste URL" = "Vložiť URL";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "Názov série";
|
||||
"Content Source" = "Zdroj obsahu";
|
||||
"Watch Progress" = "Priebeh sledovania";
|
||||
"Nothing to Continue Reading" = "Nič na pokračovanie v čítaní";
|
||||
"Your recently read novels will appear here" = "Vaše nedávno čítané romány sa zobrazia tu";
|
||||
"No Bookmarks" = "Žiadne záložky";
|
||||
"Add bookmarks to this collection" = "Pridajte záložky do tejto kolekcie";
|
||||
"items" = "položky";
|
||||
"All Watching" = "Všetko na sledovanie";
|
||||
"No Reading History" = "Žiadna história čítania";
|
||||
"Books you're reading will appear here" = "Knihy, ktoré čítate, sa zobrazia tu";
|
||||
"Create Collection" = "Vytvoriť kolekciu";
|
||||
"Collection Name" = "Názov kolekcie";
|
||||
"Rename Collection" = "Premenovať kolekciu";
|
||||
"Rename" = "Premenovať";
|
||||
"Novel Title" = "Názov románu";
|
||||
"Read Progress" = "Priebeh čítania";
|
||||
"Date Created" = "Dátum vytvorenia";
|
||||
"Name" = "Meno";
|
||||
"Item Count" = "Počet položiek";
|
||||
"Date Added" = "Dátum pridania";
|
||||
"Title" = "Názov";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hľadať v čítaní...";
|
||||
"Search collections..." = "Hľadať v kolekciách...";
|
||||
"Search bookmarks..." = "Hľadať v záložkách...";
|
||||
"%d items" = "%d položiek";
|
||||
"Fetching Data" = "Načítavanie údajov";
|
||||
"Please wait while fetching." = "Počkajte, kým sa údaje načítajú.";
|
||||
"Start Reading" = "Začať čítať";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončené";
|
||||
"Drag to reorder" = "Potiahnite na zmenu poradia";
|
||||
"Drag to reorder sections" = "Potiahnite na zmenu poradia sekcií";
|
||||
"Library View" = "Zobrazenie knižnice";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prispôsobte sekcie zobrazené vo vašej knižnici. Môžete zmeniť ich poradie alebo ich úplne vypnúť.";
|
||||
"Library Sections Order" = "Poradie sekcií knižnice";
|
||||
"Completion Percentage" = "Percento dokončenia";
|
||||
"Translators" = "Prekladatelia";
|
||||
"Paste URL" = "Vložiť URL";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "Kolekcie";
|
||||
"Continue Reading" = "Pokračovať v čítaní";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Záloha a obnovenie";
|
||||
"Export Backup" = "Exportovať zálohu";
|
||||
"Import Backup" = "Importovať zálohu";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Upozornenie: Táto funkcia je stále experimentálna. Po exporte/importe si prosím skontrolujte svoje údaje.";
|
||||
"Backup" = "Záloha";
|
||||
487
Sora/Localization/sv.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
/* General */
|
||||
"About" = "Om oss";
|
||||
"About Sora" = "Om Sora";
|
||||
"Active" = "Aktiv";
|
||||
"Active Downloads" = "Aktiva nedladdningar";
|
||||
"Actively downloading media can be tracked from here." = "Aktiva nedladdningar av media kan spåras här.";
|
||||
"Add Module" = "Lägg till Modul";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Justera antalet mediaelement per rad i porträtt- och landskapsläge.";
|
||||
"Advanced" = "Avancerat";
|
||||
"AKA Sulfur" = "AKA Sulfur";
|
||||
"All Bookmarks" = "Alla bokmärken";
|
||||
"All Watching" = "Allt du tittar på";
|
||||
"Also known as Sulfur" = "Även känd som Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList-ID";
|
||||
"AniList Match" = "AniList-matchning";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonym data samlas in för att förbättra appen. Ingen personlig information samlas in. Detta kan inaktiveras när som helst.";
|
||||
"App Info" = "Appinformation";
|
||||
"App Language" = "Appspråk";
|
||||
"App Storage" = "Applagring";
|
||||
"Appearance" = "Utseende";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Är du säker på att du vill rensa all cachad data? Detta hjälper till att frigöra lagringsutrymme.";
|
||||
"Are you sure you want to delete '%@'?" = "Är du säker på att du vill radera '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Är du säker på att du vill radera alla %1$d avsnitt i '%2$@'?";
|
||||
"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Är du säker på att du vill radera alla nedladdade filer? Du kan välja att endast rensa biblioteket och behålla de nedladdade filerna för framtida bruk.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Är du säker på att du vill radera all appdata? Denna åtgärd kan inte ångras.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Bakgrund Aktiverad";
|
||||
"Bookmark items for an easier access later." = "Bokmärk objekt för enklare åtkomst senare.";
|
||||
"Bookmarks" = "Bokmärken";
|
||||
"Bottom Padding" = "Bottenutfyllnad";
|
||||
"Cancel" = "Avbryt";
|
||||
"Cellular Quality" = "Mobilnätskvalitet";
|
||||
"Check out some community modules here!" = "Kolla in några communitymoduler här!";
|
||||
"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "Välj önskad videoupplösning för WiFi och mobilnät. Högre upplösningar använder mer data men ger bättre kvalitet.";
|
||||
"Clear" = "Rensa";
|
||||
"Clear All Downloads" = "Rensa alla nedladdningar";
|
||||
"Clear Cache" = "Rensa cache";
|
||||
"Clear Library Only" = "Rensa endast bibliotek";
|
||||
"Clear Logs" = "Rensa loggar";
|
||||
"Click the plus button to add a module!" = "Klicka på plusknappen för att lägga till en modul!";
|
||||
"Continue Watching" = "Fortsätt titta";
|
||||
"Continue Watching Episode %d" = "Fortsätt titta på Avsnitt %d";
|
||||
"Contributors" = "Medverkande";
|
||||
"Copied to Clipboard" = "Kopierat till Urklipp";
|
||||
"Copy to Clipboard" = "Kopiera till Urklipp";
|
||||
"Copy URL" = "Kopiera URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld Avsnitt";
|
||||
"%lld of %lld" = "%lld av %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% sedda";
|
||||
"Episode %lld" = "Avsnitt %lld";
|
||||
"Episodes" = "Avsnitt";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Avsnitten kanske inte är tillgängliga än eller så kan det vara ett problem med källan.";
|
||||
"Episodes Range" = "Avsnittsintervall";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Mörk";
|
||||
"DATA & LOGS" = "DATA & LOGGAR";
|
||||
"Debug" = "Felsökning";
|
||||
"Debugging and troubleshooting." = "Felsökning och problemlösning.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Radera";
|
||||
"Delete All" = "Radera alla";
|
||||
"Delete All Downloads" = "Radera alla nedladdningar";
|
||||
"Delete All Episodes" = "Radera alla avsnitt";
|
||||
"Delete Download" = "Radera nedladdning";
|
||||
"Delete Episode" = "Radera avsnitt";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Dubbeltryck för att söka";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Genom att dubbeltrycka på skärmens sidor hoppar spelaren enligt korttrycksinställningen.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Ladda ner";
|
||||
"Download Episode" = "Ladda ner avsnitt";
|
||||
"Download Summary" = "Nedladdningssammanfattning";
|
||||
"Download This Episode" = "Ladda ner detta avsnitt";
|
||||
"Downloaded" = "Nedladdat";
|
||||
"Downloaded Shows" = "Nedladdade serier";
|
||||
"Downloading" = "Laddar ner";
|
||||
"Downloads" = "Nedladdningar";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Aktivera analys";
|
||||
"Enable Subtitles" = "Aktivera undertexter";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Radera";
|
||||
"Erase all App Data" = "Radera all appdata";
|
||||
"Erase App Data" = "Radera appdata";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Fel";
|
||||
"Error Fetching Results" = "Fel vid hämtning av resultat";
|
||||
"Errors and critical issues." = "Fel och kritiska problem.";
|
||||
"Failed to load contributors" = "Kunde inte ladda medverkande";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Hämta avsnittsmetadata";
|
||||
"Files Downloaded" = "Nedladdade filer";
|
||||
"Font Size" = "Fontstorlek";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Tvinga landskapsläge";
|
||||
"General" = "Allmänt";
|
||||
"General events and activities." = "Allmänna händelser och aktiviteter.";
|
||||
"General Preferences" = "Allmänna inställningar";
|
||||
"Hide Splash Screen" = "Dölj välkomstskärm";
|
||||
"HLS video downloading." = "HLS-videonedladdning.";
|
||||
"Hold Speed" = "Tillfällig hållhastighet";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Info";
|
||||
"INFOS" = "INFO";
|
||||
"Installed Modules" = "Installerade moduler";
|
||||
"Interface" = "Gränssnitt";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Gå med i vår Discord";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Kolumner i landskapsläge";
|
||||
"Language" = "Språk";
|
||||
"LESS" = "MINDRE";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Bibliotek";
|
||||
"License (GPLv3.0)" = "Licens (GPLv3.0)";
|
||||
"Light" = "Ljust";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Laddar avsnitt %lld...";
|
||||
"Loading logs..." = "Laddar loggar...";
|
||||
"Loading module information..." = "Laddar modulinformation...";
|
||||
"Loading Stream" = "Laddar videoström";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Logga felsökningsinfo";
|
||||
"Log Filters" = "Loggfilter";
|
||||
"Log In with AniList" = "Logga in med AniList";
|
||||
"Log In with Trakt" = "Logga in med Trakt";
|
||||
"Log Out from AniList" = "Logga ut från AniList";
|
||||
"Log Out from Trakt" = "Logga ut från Trakt";
|
||||
"Log Types" = "Loggtyper";
|
||||
"Logged in as" = "Inloggad som";
|
||||
"Logged in as " = "Inloggad som ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Loggar";
|
||||
"Long press Skip" = "Långtryck för att hoppa";
|
||||
"MAIN" = "HUVUD";
|
||||
"Main Developer" = "Huvudutvecklare";
|
||||
"MAIN SETTINGS" = "HUVUDINSTÄLLNINGAR";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Markera alla tidigare som sedda";
|
||||
"Mark as Watched" = "Markera som sedd";
|
||||
"Mark Episode as Watched" = "Markera avsnitt som sedd";
|
||||
"Mark Previous Episodes as Watched" = "Markera tidigare avsnitt som sedda";
|
||||
"Mark watched" = "Markera som sedd";
|
||||
"Match with AniList" = "Matcha med AniList";
|
||||
"Match with TMDB" = "Matcha med TMDB";
|
||||
"Matched ID: %lld" = "Matchat ID: %lld";
|
||||
"Matched with: %@" = "Matchad med: %@";
|
||||
"Max Concurrent Downloads" = "Max antal parallella nedladdningar";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Medierutnätslayout";
|
||||
"Media Player" = "Mediauppspelare";
|
||||
"Media View" = "Mediavisning";
|
||||
"Metadata Provider" = "Metadata Leverantör";
|
||||
"Metadata Providers Order" = "Metadata Leverantörers Ordning";
|
||||
"Module Removed" = "Modul Borttagen";
|
||||
"Modules" = "Moduler";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULER";
|
||||
"MORE" = "MER";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Inga Aktiva Nedladdningar";
|
||||
"No AniList matches found" = "Inga AniList matchningar hittades";
|
||||
"No Data Available" = "Ingen Data Tillgänglig";
|
||||
"No Downloads" = "Inga Nedladdningar";
|
||||
"No episodes available" = "Inga avsnitt tillgängliga";
|
||||
"No Episodes Available" = "Inga Avsnitt Tillgängliga";
|
||||
"No items to continue watching." = "Inga objekt att fortsätta titta på.";
|
||||
"No matches found" = "Inga matchningar hittades";
|
||||
"No Module Selected" = "Ingen Modul Vald";
|
||||
"No Modules" = "Inga Moduler";
|
||||
"No Results Found" = "Inga Resultat Hittades";
|
||||
"No Search Results Found" = "Inga Sökresultat Hittades";
|
||||
"Nothing to Continue Watching" = "Inget att fortsätta titta på";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Observera att modulerna ersätts endast om det finns en annan versionssträng i JSON-filen.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "OK";
|
||||
"Open Community Library" = "Öppna Community Bibliotek";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Öppna i AniList";
|
||||
"Original Poster" = "Original Miniatyrbild";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pausad";
|
||||
"Play" = "Spela";
|
||||
"Player" = "Spelare";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Starta om appen för att språkändringen ska träda i kraft.";
|
||||
"Please select a module from settings" = "Välj en modul från inställningar";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Kolumner i porträttläge";
|
||||
"Progress bar Marker Color" = "Färg på Förloppsindikator";
|
||||
"Provider: %@" = "Leverantör: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Kö";
|
||||
"Queued" = "I kö";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Nyligen sedda innehåll visas här.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Uppdatera Moduler vid Start";
|
||||
"Refresh Storage Info" = "Uppdatera Lagringsinformation";
|
||||
"Remember Playback speed" = "Kom ihåg uppspelningshastighet";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Radera";
|
||||
"Remove All Cache" = "Radera all cache";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Radera alla dokument";
|
||||
"Remove Documents" = "Radera dokument";
|
||||
"Remove Downloaded Media" = "Radera nedladdad media";
|
||||
"Remove Downloads" = "Radera nedladdningar";
|
||||
"Remove from Bookmarks" = "Radera från bokmärken";
|
||||
"Remove Item" = "Radera objekt";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Rapportera ett Problem";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Återställ";
|
||||
"Reset AniList ID" = "Återställ AniList-ID";
|
||||
"Reset Episode Progress" = "Återställ Avsnittsprogress";
|
||||
"Reset progress" = "Återställ progress";
|
||||
"Reset Progress" = "Återställ Progress";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Omstart Krävs";
|
||||
"Running Sora %@ - cranci1" = "Körar Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Spara";
|
||||
"Search" = "Sök";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Sök i nedladdningar";
|
||||
"Search for something..." = "Sök efter något...";
|
||||
"Search..." = "Sök...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Säsong %d";
|
||||
"Season %lld" = "Säsong %lld";
|
||||
"Segments Color" = "Segmentfärg";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Välj Modul";
|
||||
"Set Custom AniList ID" = "Ange Anpassat AniList-ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Inställningar";
|
||||
"Shadow" = "Skugga";
|
||||
"Show More (%lld more characters)" = "Visa Mer (%lld fler tecken)";
|
||||
"Show PiP Button" = "Visa PiP Knapp";
|
||||
"Show Skip 85s Button" = "Visa Hoppa 85s Knapp";
|
||||
"Show Skip Intro / Outro Buttons" = "Visa Hoppa över Intro / Outro Knappar";
|
||||
"Shows" = "Serier";
|
||||
"Size (%@)" = "Storlek (%@)";
|
||||
"Skip Settings" = "Hoppa Inställningar";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Vissa funktioner är begränsade till Sora och standarduppspelaren, som Tvinga landskapsläge, tillfällig hållhastighet och anpassade hoppintervall.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ av cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora och cranci1 är på inget sätt knutna till AniList eller Trakt.\n\nObservera också att progressuppdateringar kanske inte är 100% exakta.";
|
||||
"Sora GitHub Repository" = "Sora GitHub-förråd";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur kommer alltid att vara gratis och utan reklam!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Sortera";
|
||||
"Speed Settings" = "Hastighetsinställningar";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Börja Titta";
|
||||
"Start Watching Episode %d" = "Börja Titta på Avsnitt %d";
|
||||
"Storage Used" = "Använt Lagringsutrymme";
|
||||
"Stream" = "Strömma";
|
||||
"Streaming and video playback." = "Strömning och videouppspelning.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Undertextfärg";
|
||||
"Subtitle Settings" = "Undertextinställningar";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Synkronisera animeprogress";
|
||||
"Sync TV shows progress" = "Synkronisera TV-serieprogress";
|
||||
|
||||
/* System */
|
||||
"System" = "System";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Tryck på en titel för att åsidosätta aktuell matchning.";
|
||||
"Tap Skip" = "Tryck för att hoppa Fram / Tillbaka";
|
||||
"Tap to manage your modules" = "Tryck för att hantera dina moduler";
|
||||
"Tap to select a module" = "Tryck för att välja en modul";
|
||||
|
||||
/* App Information */
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Appens cache hjälper appen att ladda bilder snabbare.\n\nAtt rensa dokumentmappen tar bort alla nedladdade moduler.\n\nRadera inte appdata om du inte förstår konsekvenserna - det kan få appen att fungera fel.";
|
||||
"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Avsnittsintervallet styr hur många avsnitt som visas på varje sida. Avsnitten är grupperade i uppsättningar (som 1–25, 26–50, osv.), vilket gör det enklare att navigera mellan dem.\n\nFör avsnittsmetadata avses miniatyrbild och titel, eftersom dessa ibland kan innehålla spoilers.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modulen tillhandahöll endast ett avsnitt, detta är troligen en film, så vi beslutade att skapa separata skärmar för dessa fall.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Miniatyrbildsbredd";
|
||||
"TMDB Match" = "TMDB Matchning";
|
||||
"Trackers" = "Spårare";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Prova andra sökord";
|
||||
"Try different search terms" = "Prova andra söktermer";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Håll med två fingrar för paus";
|
||||
"Unable to fetch matches. Please try again later." = "Kunde inte hämta matchningar. Försök igen senare.";
|
||||
"Use TMDB Poster Image" = "Använd TMDB Miniatyrbild";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Videouppspelare";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Videokvalitetsinställningar";
|
||||
"View All" = "Visa alla";
|
||||
"Watched" = "Sedda";
|
||||
"Why am I not seeing any episodes?" = "Varför ser jag inga avsnitt?";
|
||||
"WiFi Quality" = "WiFi Kvalitet";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Du är inte inloggad";
|
||||
"You have no items saved." = "Du har inga sparade objekt.";
|
||||
"Your downloaded episodes will appear here" = "Dina nedladdade avsnitt visas här";
|
||||
"Your recently watched content will appear here" = "Ditt nyligen sedda innehåll visas här";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Nedladdningsinställningar";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Max antal parallella nedladdningar styr hur många avsnitt som kan laddas ner samtidigt. Högre värden kan använda mer bandbredd och enhetsresurser.";
|
||||
"Quality" = "Kvalitet";
|
||||
"Max Concurrent Downloads" = "Max antal Parallella Nedladdningar";
|
||||
"Allow Cellular Downloads" = "Tillåt Nedladdningar via Mobilnät";
|
||||
"Quality Information" = "Kvalitetsinformation";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Lagringshantering";
|
||||
"Storage Used" = "Använt Lagringsutrymme";
|
||||
"Library cleared successfully" = "Biblioteket rensat";
|
||||
"All downloads deleted successfully" = "Alla nedladdningar borttagna";
|
||||
|
||||
/* New keys from English localization */
|
||||
"DownloadCountFormat" = "%d av %d";
|
||||
"Error loading chapter" = "Fel vid inläsning av kapitel";
|
||||
"Font Size: %dpt" = "Teckenstorlek: %dpt";
|
||||
"Line Spacing: %.1f" = "Radavstånd: %.1f";
|
||||
"Line Spacing" = "Radavstånd";
|
||||
"Margin: %dpx" = "Marginal: %dpx";
|
||||
"Margin" = "Marginal";
|
||||
"Auto Scroll Speed" = "Automatisk rullningshastighet";
|
||||
"Speed" = "Hastighet";
|
||||
"Speed: %.1fx" = "Hastighet: %.1fx";
|
||||
"Matched %@: %@" = "Matchning %@: %@";
|
||||
"Enter the AniList ID for this series" = "Ange AniList-ID för denna serie";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Skapa samling";
|
||||
"Collection Name" = "Samlingens namn";
|
||||
"Rename Collection" = "Byt namn på samling";
|
||||
"Rename" = "Byt namn";
|
||||
"All Reading" = "All läsning";
|
||||
"Recently Added" = "Nyligen tillagda";
|
||||
"Novel Title" = "Romanens titel";
|
||||
"Read Progress" = "Läsningsframsteg";
|
||||
"Date Created" = "Skapad datum";
|
||||
"Name" = "Namn";
|
||||
"Item Count" = "Antal objekt";
|
||||
"Date Added" = "Datum tillagt";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Källa";
|
||||
"Search reading..." = "Sök i läsning...";
|
||||
"Search collections..." = "Sök i samlingar...";
|
||||
"Search bookmarks..." = "Sök i bokmärken...";
|
||||
"%d items" = "%d objekt";
|
||||
"Fetching Data" = "Hämtar data";
|
||||
"Please wait while fetching." = "Vänligen vänta under hämtning.";
|
||||
"Start Reading" = "Börja läsa";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Slutförd";
|
||||
"Drag to reorder" = "Dra för att ändra ordning";
|
||||
"Drag to reorder sections" = "Dra för att ändra ordning på sektioner";
|
||||
"Library View" = "Biblioteksvisning";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Anpassa sektionerna som visas i ditt bibliotek. Du kan ändra ordning eller inaktivera sektioner helt.";
|
||||
"Library Sections Order" = "Ordning på bibliotekets sektioner";
|
||||
"Completion Percentage" = "Slutförandeprocent";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Vissa funktioner är begränsade till Sora- och standardspelaren, såsom tvingat landskap, hållhastighet och anpassade tidshopp.\n\nInställningen för slutförandeprocent avgör vid vilken punkt före slutet av en video appen markerar den som slutförd på AniList och Trakt.";
|
||||
"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app." = "Appens cache hjälper till att ladda bilder snabbare.\n\nAtt rensa mappen Dokument tar bort alla nedladdade moduler.\n\nAtt radera appdata tar bort alla dina inställningar och data.";
|
||||
"Translators" = "Översättare";
|
||||
"Paste URL" = "Klistra in URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serietitel";
|
||||
"Content Source" = "Innehållskälla";
|
||||
"Watch Progress" = "Tittarframsteg";
|
||||
"Recent searches" = "Senaste sökningar";
|
||||
"Nothing to Continue Reading" = "Inget att fortsätta läsa";
|
||||
"Your recently read novels will appear here" = "Dina nyligen lästa romaner visas här";
|
||||
"No Bookmarks" = "Inga bokmärken";
|
||||
"Add bookmarks to this collection" = "Lägg till bokmärken i denna samling";
|
||||
"items" = "objekt";
|
||||
"All Watching" = "Allt du tittar på";
|
||||
"No Reading History" = "Ingen läshistorik";
|
||||
"Books you're reading will appear here" = "Böcker du läser visas här";
|
||||
"Create Collection" = "Skapa samling";
|
||||
"Collection Name" = "Samlingens namn";
|
||||
"Rename Collection" = "Byt namn på samling";
|
||||
"Rename" = "Byt namn";
|
||||
"Novel Title" = "Roman titel";
|
||||
"Read Progress" = "Läsframsteg";
|
||||
"Date Created" = "Skapad datum";
|
||||
"Name" = "Namn";
|
||||
"Item Count" = "Antal objekt";
|
||||
"Date Added" = "Datum tillagt";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Källa";
|
||||
"Search reading..." = "Sök i läsning...";
|
||||
"Search collections..." = "Sök i samlingar...";
|
||||
"Search bookmarks..." = "Sök i bokmärken...";
|
||||
"%d items" = "%d objekt";
|
||||
"Fetching Data" = "Hämtar data";
|
||||
"Please wait while fetching." = "Vänligen vänta medan data hämtas.";
|
||||
"Start Reading" = "Börja läsa";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Slutförd";
|
||||
"Drag to reorder" = "Dra för att ändra ordning";
|
||||
"Drag to reorder sections" = "Dra för att ändra ordning på sektioner";
|
||||
"Library View" = "Biblioteksvy";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Anpassa sektionerna som visas i ditt bibliotek. Du kan ändra ordning eller inaktivera dem helt.";
|
||||
"Library Sections Order" = "Bibliotekssektioners ordning";
|
||||
"Completion Percentage" = "Slutförandeprocent";
|
||||
"Translators" = "Översättare";
|
||||
"Paste URL" = "Klistra in URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Samlingar";
|
||||
"Continue Reading" = "Fortsätt läsa";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Säkerhetskopiera & Återställ";
|
||||
"Export Backup" = "Exportera säkerhetskopia";
|
||||
"Import Backup" = "Importera säkerhetskopia";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Observera: Denna funktion är fortfarande experimentell. Kontrollera dina data efter export/import.";
|
||||
"Backup" = "Säkerhetskopia";
|
||||
48
Sora/MediaUtils/ContinueWatching/ContinueReadingItem.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// ContinueReadingItem.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 26/06/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ContinueReadingItem: Identifiable, Codable {
|
||||
let id: UUID
|
||||
let mediaTitle: String
|
||||
let chapterTitle: String
|
||||
let chapterNumber: Int
|
||||
let imageUrl: String
|
||||
let href: String
|
||||
let moduleId: String
|
||||
let progress: Double
|
||||
let totalChapters: Int
|
||||
let lastReadDate: Date
|
||||
let cachedHtml: String?
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
mediaTitle: String,
|
||||
chapterTitle: String,
|
||||
chapterNumber: Int,
|
||||
imageUrl: String,
|
||||
href: String,
|
||||
moduleId: String,
|
||||
progress: Double = 0.0,
|
||||
totalChapters: Int = 0,
|
||||
lastReadDate: Date = Date(),
|
||||
cachedHtml: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.mediaTitle = mediaTitle
|
||||
self.chapterTitle = chapterTitle
|
||||
self.chapterNumber = chapterNumber
|
||||
self.imageUrl = imageUrl
|
||||
self.href = href
|
||||
self.moduleId = moduleId
|
||||
self.progress = progress
|
||||
self.totalChapters = totalChapters
|
||||
self.lastReadDate = lastReadDate
|
||||
self.cachedHtml = cachedHtml
|
||||
}
|
||||
}
|
||||
275
Sora/MediaUtils/ContinueWatching/ContinueReadingManager.swift
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
//
|
||||
// ContinueReadingManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 26/06/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ContinueReadingManager {
|
||||
static let shared = ContinueReadingManager()
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let continueReadingKey = "continueReadingItems"
|
||||
|
||||
private init() {}
|
||||
|
||||
func extractTitleFromURL(_ url: String) -> String? {
|
||||
guard let url = URL(string: url) else { return nil }
|
||||
|
||||
let pathComponents = url.pathComponents
|
||||
|
||||
for (index, component) in pathComponents.enumerated() {
|
||||
if component == "book" || component == "novel" {
|
||||
if index + 1 < pathComponents.count {
|
||||
let bookTitle = pathComponents[index + 1]
|
||||
.replacingOccurrences(of: "-", with: " ")
|
||||
.replacingOccurrences(of: "_", with: " ")
|
||||
.capitalized
|
||||
|
||||
if !bookTitle.isEmpty {
|
||||
return bookTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchItems() -> [ContinueReadingItem] {
|
||||
guard let data = userDefaults.data(forKey: continueReadingKey) else {
|
||||
Logger.shared.log("No continue reading items found in UserDefaults", type: "Debug")
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
let items = try JSONDecoder().decode([ContinueReadingItem].self, from: data)
|
||||
Logger.shared.log("Fetched \(items.count) continue reading items", type: "Debug")
|
||||
|
||||
for (index, item) in items.enumerated() {
|
||||
Logger.shared.log("Item \(index): \(item.mediaTitle), Image URL: \(item.imageUrl)", type: "Debug")
|
||||
}
|
||||
|
||||
return items.sorted(by: { $0.lastReadDate > $1.lastReadDate })
|
||||
} catch {
|
||||
Logger.shared.log("Error decoding continue reading items: \(error)", type: "Error")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func save(item: ContinueReadingItem, htmlContent: String? = nil) {
|
||||
var items = fetchItems()
|
||||
|
||||
items.removeAll { $0.href == item.href }
|
||||
|
||||
if item.progress >= 0.98 {
|
||||
userDefaults.set(item.progress, forKey: "readingProgress_\(item.href)")
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var updatedItem = item
|
||||
if item.mediaTitle.contains("-") && item.mediaTitle.count >= 30 || item.mediaTitle.contains("Unknown") {
|
||||
if let betterTitle = extractTitleFromURL(item.href) {
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: item.id,
|
||||
mediaTitle: betterTitle,
|
||||
chapterTitle: item.chapterTitle,
|
||||
chapterNumber: item.chapterNumber,
|
||||
imageUrl: item.imageUrl,
|
||||
href: item.href,
|
||||
moduleId: item.moduleId,
|
||||
progress: item.progress,
|
||||
totalChapters: item.totalChapters,
|
||||
lastReadDate: item.lastReadDate,
|
||||
cachedHtml: htmlContent ?? item.cachedHtml
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.shared.log("Incoming item image URL: \(updatedItem.imageUrl)", type: "Debug")
|
||||
|
||||
if updatedItem.imageUrl.isEmpty {
|
||||
let defaultImageUrl = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/novel_cover.jpg"
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: updatedItem.mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: defaultImageUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: updatedItem.progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: updatedItem.lastReadDate,
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
Logger.shared.log("Using default image URL: \(defaultImageUrl)", type: "Debug")
|
||||
}
|
||||
|
||||
if !updatedItem.imageUrl.isEmpty {
|
||||
if URL(string: updatedItem.imageUrl) == nil {
|
||||
Logger.shared.log("Invalid image URL format: \(updatedItem.imageUrl)", type: "Warning")
|
||||
|
||||
if let encodedUrl = updatedItem.imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let _ = URL(string: encodedUrl) {
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: updatedItem.mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: encodedUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: updatedItem.progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: updatedItem.lastReadDate,
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
Logger.shared.log("Fixed image URL with encoding: \(encodedUrl)", type: "Debug")
|
||||
} else {
|
||||
let defaultImageUrl = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/novel_cover.jpg"
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: updatedItem.mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: defaultImageUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: updatedItem.progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: updatedItem.lastReadDate,
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
Logger.shared.log("Using default image URL after encoding failed: \(defaultImageUrl)", type: "Debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.shared.log("Saving item with image URL: \(updatedItem.imageUrl)", type: "Debug")
|
||||
|
||||
items.append(updatedItem)
|
||||
|
||||
if items.count > 20 {
|
||||
items = Array(items.sorted(by: { $0.lastReadDate > $1.lastReadDate }).prefix(20))
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
Logger.shared.log("Successfully saved continue reading item", type: "Debug")
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
func remove(item: ContinueReadingItem) {
|
||||
var items = fetchItems()
|
||||
items.removeAll { $0.id == item.id }
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
func updateProgress(for href: String, progress: Double, htmlContent: String? = nil) {
|
||||
var items = fetchItems()
|
||||
if let index = items.firstIndex(where: { $0.href == href }) {
|
||||
let updatedItem = items[index]
|
||||
|
||||
if progress >= 0.98 {
|
||||
let cachedHtml = htmlContent ?? updatedItem.cachedHtml
|
||||
|
||||
if let html = cachedHtml, !html.isEmpty && !html.contains("undefined") && html.count > 50 {
|
||||
let completedChapterKey = "completedChapterHtml_\(href)"
|
||||
UserDefaults.standard.set(html, forKey: completedChapterKey)
|
||||
Logger.shared.log("Saved HTML content for completed chapter \(href)", type: "Debug")
|
||||
}
|
||||
|
||||
items.remove(at: index)
|
||||
userDefaults.set(progress, forKey: "readingProgress_\(href)")
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var mediaTitle = updatedItem.mediaTitle
|
||||
if mediaTitle.contains("-") && mediaTitle.count >= 30 || mediaTitle.contains("Unknown") {
|
||||
if let betterTitle = extractTitleFromURL(href) {
|
||||
mediaTitle = betterTitle
|
||||
}
|
||||
}
|
||||
|
||||
let newItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: updatedItem.imageUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: Date(),
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
|
||||
Logger.shared.log("Updating item with image URL: \(newItem.imageUrl)", type: "Debug")
|
||||
|
||||
items[index] = newItem
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isChapterCompleted(href: String) -> Bool {
|
||||
let progress = UserDefaults.standard.double(forKey: "readingProgress_\(href)")
|
||||
if progress >= 0.98 {
|
||||
return true
|
||||
}
|
||||
|
||||
let items = fetchItems()
|
||||
if let item = items.first(where: { $0.href == href }) {
|
||||
return item.progress >= 0.98
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getCachedHtml(for href: String) -> String? {
|
||||
let completedChapterKey = "completedChapterHtml_\(href)"
|
||||
if let completedHtml = UserDefaults.standard.string(forKey: completedChapterKey),
|
||||
!completedHtml.isEmpty && !completedHtml.contains("undefined") && completedHtml.count > 50 {
|
||||
Logger.shared.log("Using cached HTML for completed chapter \(href)", type: "Debug")
|
||||
return completedHtml
|
||||
}
|
||||
|
||||
let items = fetchItems()
|
||||
if let item = items.first(where: { $0.href == href }) {
|
||||
return item.cachedHtml
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
23
Sora/MediaUtils/ContinueWatching/ContinueWatchingItem.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// ContinueWatchingItem.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 14/02/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ContinueWatchingItem: Codable, Identifiable {
|
||||
let id: UUID
|
||||
let imageUrl: String
|
||||
let episodeNumber: Int
|
||||
let mediaTitle: String
|
||||
var progress: Double
|
||||
let streamUrl: String
|
||||
let fullUrl: String
|
||||
let subtitles: String?
|
||||
let aniListID: Int?
|
||||
let module: ScrapingModule
|
||||
let headers: [String:String]?
|
||||
let totalEpisodes: Int
|
||||
}
|
||||
146
Sora/MediaUtils/ContinueWatching/ContinueWatchingManager.swift
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
//
|
||||
// ContinueWatchingManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 14/02/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ContinueWatchingManager {
|
||||
static let shared = ContinueWatchingManager()
|
||||
private let storageKey = "continueWatchingItems"
|
||||
private let lastCleanupKey = "lastContinueWatchingCleanup"
|
||||
|
||||
private init() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudSync), name: .iCloudSyncDidComplete, object: nil)
|
||||
performCleanupIfNeeded()
|
||||
}
|
||||
|
||||
@objc private func handleiCloudSync() {
|
||||
NotificationCenter.default.post(name: .ContinueWatchingDidUpdate, object: nil)
|
||||
}
|
||||
|
||||
private func performCleanupIfNeeded() {
|
||||
let lastCleanup = UserDefaults.standard.double(forKey: lastCleanupKey)
|
||||
let currentTime = Date().timeIntervalSince1970
|
||||
|
||||
if currentTime - lastCleanup > 86400 {
|
||||
cleanupOldEpisodes()
|
||||
UserDefaults.standard.set(currentTime, forKey: lastCleanupKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupOldEpisodes() {
|
||||
var items = fetchItems()
|
||||
var itemsToRemove: Set<UUID> = []
|
||||
|
||||
let groupedItems = Dictionary(grouping: items) { item in
|
||||
let title = item.mediaTitle.replacingOccurrences(of: "Episode \\d+.*$", with: "", options: .regularExpression)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return title
|
||||
}
|
||||
|
||||
for (_, showEpisodes) in groupedItems {
|
||||
let sortedEpisodes = showEpisodes.sorted { $0.episodeNumber < $1.episodeNumber }
|
||||
|
||||
for i in 0..<sortedEpisodes.count - 1 {
|
||||
let currentEpisode = sortedEpisodes[i]
|
||||
let nextEpisode = sortedEpisodes[i + 1]
|
||||
|
||||
if currentEpisode.progress >= 0.8 && nextEpisode.episodeNumber > currentEpisode.episodeNumber {
|
||||
itemsToRemove.insert(currentEpisode.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !itemsToRemove.isEmpty {
|
||||
items.removeAll { itemsToRemove.contains($0.id) }
|
||||
if let data = try? JSONEncoder().encode(items) {
|
||||
UserDefaults.standard.set(data, forKey: storageKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func save(item: ContinueWatchingItem) {
|
||||
// Use real playback times
|
||||
let lastKey = "lastPlayedTime_\(item.fullUrl)"
|
||||
let totalKey = "totalTime_\(item.fullUrl)"
|
||||
let lastPlayed = UserDefaults.standard.double(forKey: lastKey)
|
||||
let totalTime = UserDefaults.standard.double(forKey: totalKey)
|
||||
|
||||
// Compute up-to-date progress
|
||||
let actualProgress: Double
|
||||
if totalTime > 0 {
|
||||
actualProgress = min(max(lastPlayed / totalTime, 0), 1)
|
||||
} else {
|
||||
actualProgress = item.progress
|
||||
}
|
||||
|
||||
// If watched ≥ 90%, remove it
|
||||
if actualProgress >= 0.9 {
|
||||
remove(item: item)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise update progress and remove old episodes from the same show
|
||||
var updatedItem = item
|
||||
updatedItem.progress = actualProgress
|
||||
|
||||
var items = fetchItems()
|
||||
|
||||
let showTitle = item.mediaTitle.replacingOccurrences(of: "Episode \\d+.*$", with: "", options: .regularExpression)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
items.removeAll { existingItem in
|
||||
let existingShowTitle = existingItem.mediaTitle.replacingOccurrences(of: "Episode \\d+.*$", with: "", options: .regularExpression)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
return showTitle == existingShowTitle &&
|
||||
existingItem.episodeNumber < item.episodeNumber &&
|
||||
existingItem.progress >= 0.8
|
||||
}
|
||||
|
||||
items.removeAll { existing in
|
||||
existing.fullUrl == item.fullUrl &&
|
||||
existing.episodeNumber == item.episodeNumber &&
|
||||
existing.module.metadata.sourceName == item.module.metadata.sourceName
|
||||
}
|
||||
|
||||
items.append(updatedItem)
|
||||
|
||||
if let data = try? JSONEncoder().encode(items) {
|
||||
UserDefaults.standard.set(data, forKey: storageKey)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchItems() -> [ContinueWatchingItem] {
|
||||
guard
|
||||
let data = UserDefaults.standard.data(forKey: storageKey),
|
||||
let raw = try? JSONDecoder().decode([ContinueWatchingItem].self, from: data)
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
var seen = Set<String>()
|
||||
let unique = raw.reversed().filter { item in
|
||||
let key = "\(item.fullUrl)|\(item.module.metadata.sourceName)|\(item.episodeNumber)"
|
||||
if seen.contains(key) {
|
||||
return false
|
||||
} else {
|
||||
seen.insert(key)
|
||||
return true
|
||||
}
|
||||
}.reversed()
|
||||
|
||||
return Array(unique)
|
||||
}
|
||||
|
||||
func remove(item: ContinueWatchingItem) {
|
||||
var items = fetchItems()
|
||||
items.removeAll { $0.id == item.id }
|
||||
if let data = try? JSONEncoder().encode(items) {
|
||||
UserDefaults.standard.set(data, forKey: storageKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// Double+Extension.swift
|
||||
// AppleMusicSlider
|
||||
//
|
||||
// Created by Pratik on 14/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Double {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension BinaryFloatingPoint {
|
||||
func asTimeString(style: TimeStringStyle, showHours: Bool = false) -> String {
|
||||
let totalSeconds = Int(self)
|
||||
let hours = totalSeconds / 3600
|
||||
let minutes = (totalSeconds % 3600) / 60
|
||||
let seconds = totalSeconds % 60
|
||||
|
||||
if showHours || hours > 0 {
|
||||
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
} else {
|
||||
return String(format: "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TimeStringStyle {
|
||||
case positional
|
||||
case standard
|
||||
}
|
||||
|
||||
class VolumeViewModel: ObservableObject {
|
||||
@Published var value: Double = 0.0
|
||||
}
|
||||
|
||||
class SliderViewModel: ObservableObject {
|
||||
@Published var sliderValue: Double = 0.0
|
||||
@Published var introSegments: [ClosedRange<Double>] = []
|
||||
@Published var outroSegments: [ClosedRange<Double>] = []
|
||||
}
|
||||
|
||||
struct AniListMediaResponse: Decodable {
|
||||
struct DataField: Decodable {
|
||||
struct Media: Decodable { let idMal: Int? }
|
||||
let Media: Media?
|
||||
}
|
||||
let data: DataField
|
||||
}
|
||||
|
||||
struct AniSkipResponse: Decodable {
|
||||
struct Result: Decodable {
|
||||
struct Interval: Decodable {
|
||||
let startTime: Double
|
||||
let endTime: Double
|
||||
}
|
||||
let interval: Interval
|
||||
let skipType: String
|
||||
}
|
||||
let found: Bool
|
||||
let results: [Result]
|
||||
let statusCode: Int
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// MusicProgressSlider.swift
|
||||
// Custom Seekbar
|
||||
//
|
||||
// Created by Pratik on 08/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
// I did edit some of the code for my liking (added a buffer indicator, etc.)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let textColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
let introSegments: [ClosedRange<T>]
|
||||
let outroSegments: [ClosedRange<T>]
|
||||
let introColor: Color
|
||||
let outroColor: Color
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
VStack(spacing: 8) {
|
||||
ZStack(alignment: .center) {
|
||||
ZStack(alignment: .center) {
|
||||
Capsule()
|
||||
.fill(.ultraThinMaterial)
|
||||
}
|
||||
.clipShape(Capsule())
|
||||
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(
|
||||
width: max(
|
||||
bounds.size.width * CGFloat(localRealProgress + localTempProgress),
|
||||
0
|
||||
),
|
||||
alignment: .leading
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
|
||||
ForEach(introSegments, id: \.self) { segment in
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
.frame(width: bounds.size.width * CGFloat(segment.lowerBound))
|
||||
Rectangle()
|
||||
.fill(introColor.opacity(0.5))
|
||||
.frame(width: bounds.size.width * CGFloat(segment.upperBound - segment.lowerBound))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(outroSegments, id: \.self) { segment in
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
.frame(width: bounds.size.width * CGFloat(segment.lowerBound))
|
||||
Rectangle()
|
||||
.fill(outroColor.opacity(0.5))
|
||||
.frame(width: bounds.size.width * CGFloat(segment.upperBound - segment.lowerBound))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
let shouldShowHours = inRange.upperBound >= 3600
|
||||
Text(value.asTimeString(style: .positional, showHours: shouldShowHours))
|
||||
Spacer(minLength: 0)
|
||||
Text("-" + (inRange.upperBound - value)
|
||||
.asTimeString(style: .positional, showHours: shouldShowHours))
|
||||
}
|
||||
.font(.system(size: 12.5))
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.04 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
}
|
||||
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { _, state, _ in
|
||||
state = true
|
||||
}
|
||||
.onChanged { gesture in
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
}
|
||||
.onEnded { _ in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
}
|
||||
)
|
||||
.onChange(of: isActive) { newValue in
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = getPrgPercentage(value)
|
||||
}
|
||||
.onChange(of: value) { newValue in
|
||||
if !isActive {
|
||||
localRealProgress = getPrgPercentage(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: isActive ? height * 1.25 : height, alignment: .center)
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
if isActive {
|
||||
return .spring()
|
||||
} else {
|
||||
return .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
private func getPrgPercentage(_ value: T) -> T {
|
||||
let range = inRange.upperBound - inRange.lowerBound
|
||||
let correctedStartValue = value - inRange.lowerBound
|
||||
let percentage = correctedStartValue / range
|
||||
return percentage
|
||||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
}
|
||||
}
|
||||
189
Sora/MediaUtils/CustomPlayer/Components/VolumeSlider.swift
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
//
|
||||
// VolumeSlider.swift
|
||||
// Custom Seekbar
|
||||
//
|
||||
// Created by Pratik on 08/01/23.
|
||||
// Credits to Pratik https://github.com/pratikg29/Custom-Slider-Control/blob/main/AppleMusicSlider/AppleMusicSlider/VolumeSlider.swift
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct VolumeSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@State private var lastVolumeValue: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
@State private var isAtEnd: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
HStack {
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .center) {
|
||||
Capsule().fill(emptyColor)
|
||||
Capsule().fill(isActive ? activeFillColor : fillColor)
|
||||
.mask {
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(
|
||||
width: max(geo.size.width * CGFloat(localRealProgress + localTempProgress), 0),
|
||||
alignment: .leading
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image(systemName: getIconName)
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.frame(width: 30)
|
||||
.foregroundColor(isActive ? activeFillColor : fillColor)
|
||||
.onTapGesture {
|
||||
handleIconTap()
|
||||
}
|
||||
}
|
||||
.frame(width: getStretchWidth(bounds: bounds), alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
.animation(animation, value: isAtEnd)
|
||||
}
|
||||
.frame(width: bounds.size.width, height: bounds.size.height)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { _, state, _ in state = true }
|
||||
.onChanged { gesture in
|
||||
let delta = gesture.translation.width / bounds.size.width
|
||||
localTempProgress = T(delta)
|
||||
|
||||
let totalProgress = localRealProgress + localTempProgress
|
||||
if totalProgress <= 0.0 || totalProgress >= 1.0 {
|
||||
isAtEnd = true
|
||||
} else {
|
||||
isAtEnd = false
|
||||
}
|
||||
|
||||
value = sliderValueInRange()
|
||||
}
|
||||
.onEnded { _ in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
isAtEnd = false
|
||||
}
|
||||
)
|
||||
.onChange(of: isActive) { newValue in
|
||||
if !newValue {
|
||||
value = sliderValueInRange()
|
||||
isAtEnd = false
|
||||
}
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = progress(for: value)
|
||||
if value > 0 {
|
||||
lastVolumeValue = value
|
||||
}
|
||||
}
|
||||
.onChange(of: value) { newVal in
|
||||
if !isActive {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
localRealProgress = progress(for: newVal)
|
||||
}
|
||||
if newVal > 0 {
|
||||
lastVolumeValue = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: getStretchHeight())
|
||||
}
|
||||
|
||||
private var getIconName: String {
|
||||
let p = max(0, min(localRealProgress + localTempProgress, 1))
|
||||
let muteThreshold: T = 0
|
||||
let lowThreshold: T = 0.2
|
||||
let midThreshold: T = 0.35
|
||||
let highThreshold: T = 0.7
|
||||
|
||||
switch p {
|
||||
case muteThreshold:
|
||||
return "speaker.slash.fill"
|
||||
case muteThreshold..<lowThreshold:
|
||||
return "speaker.fill"
|
||||
case lowThreshold..<midThreshold:
|
||||
return "speaker.wave.1.fill"
|
||||
case midThreshold..<highThreshold:
|
||||
return "speaker.wave.2.fill"
|
||||
default:
|
||||
return "speaker.wave.3.fill"
|
||||
}
|
||||
}
|
||||
|
||||
private func handleIconTap() {
|
||||
let currentProgress = localRealProgress + localTempProgress
|
||||
|
||||
withAnimation {
|
||||
if currentProgress <= 0 {
|
||||
value = lastVolumeValue
|
||||
localRealProgress = progress(for: lastVolumeValue)
|
||||
localTempProgress = 0
|
||||
} else {
|
||||
lastVolumeValue = sliderValueInRange()
|
||||
value = T(0)
|
||||
localRealProgress = 0
|
||||
localTempProgress = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
.interpolatingSpring(
|
||||
mass: 1.0,
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
initialVelocity: 0.0
|
||||
)
|
||||
}
|
||||
|
||||
private func progress(for val: T) -> T {
|
||||
let totalRange = inRange.upperBound - inRange.lowerBound
|
||||
let adjustedVal = val - inRange.lowerBound
|
||||
return adjustedVal / totalRange
|
||||
}
|
||||
|
||||
private func sliderValueInRange() -> T {
|
||||
let totalProgress = localRealProgress + localTempProgress
|
||||
let rawVal = totalProgress * (inRange.upperBound - inRange.lowerBound)
|
||||
+ inRange.lowerBound
|
||||
return max(min(rawVal, inRange.upperBound), inRange.lowerBound)
|
||||
}
|
||||
|
||||
private func getStretchWidth(bounds: GeometryProxy) -> CGFloat {
|
||||
let baseWidth = bounds.size.width
|
||||
if isAtEnd {
|
||||
return baseWidth * 1.08
|
||||
} else if isActive {
|
||||
return baseWidth * 1.04
|
||||
} else {
|
||||
return baseWidth
|
||||
}
|
||||
}
|
||||
|
||||
private func getStretchHeight() -> CGFloat {
|
||||
if isAtEnd {
|
||||
return height * 1.35
|
||||
} else if isActive {
|
||||
return height * 1.25
|
||||
} else {
|
||||
return height
|
||||
}
|
||||
}
|
||||
}
|
||||
3642
Sora/MediaUtils/CustomPlayer/CustomPlayer.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// SubtitleSettingsManager.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 09/03/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct SubtitleSettings: Codable {
|
||||
var enabled: Bool = true
|
||||
var foregroundColor: String = "white"
|
||||
var fontSize: Double = 20.0
|
||||
var shadowRadius: Double = 1.0
|
||||
var backgroundEnabled: Bool = true
|
||||
var bottomPadding: CGFloat = 20.0
|
||||
var subtitleDelay: Double = 0.0
|
||||
}
|
||||
|
||||
class SubtitleSettingsManager {
|
||||
static let shared = SubtitleSettingsManager()
|
||||
|
||||
private let userDefaultsKey = "SubtitleSettings"
|
||||
|
||||
var settings: SubtitleSettings {
|
||||
get {
|
||||
if let data = UserDefaults.standard.data(forKey: userDefaultsKey),
|
||||
let savedSettings = try? JSONDecoder().decode(SubtitleSettings.self, from: data) {
|
||||
return savedSettings
|
||||
}
|
||||
return SubtitleSettings()
|
||||
}
|
||||
set {
|
||||
if let data = try? JSONEncoder().encode(newValue) {
|
||||
UserDefaults.standard.set(data, forKey: userDefaultsKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(_ updateBlock: (inout SubtitleSettings) -> Void) {
|
||||
var currentSettings = settings
|
||||
updateBlock(¤tSettings)
|
||||
settings = currentSettings
|
||||
}
|
||||
}
|
||||
168
Sora/MediaUtils/CustomPlayer/Helpers/VTTSubtitlesLoader.swift
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
//
|
||||
// VTTSubtitlesLoader.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 15/02/25.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
struct SubtitleCue: Identifiable {
|
||||
let id = UUID()
|
||||
let startTime: Double
|
||||
let endTime: Double
|
||||
let text: String
|
||||
}
|
||||
|
||||
class VTTSubtitlesLoader: ObservableObject {
|
||||
@Published var cues: [SubtitleCue] = []
|
||||
|
||||
enum SubtitleFormat {
|
||||
case vtt
|
||||
case srt
|
||||
case unknown
|
||||
}
|
||||
|
||||
func load(from urlString: String) {
|
||||
guard let url = URL(string: urlString) else { return }
|
||||
|
||||
let format = determineSubtitleFormat(from: url)
|
||||
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
guard let data = data,
|
||||
let content = String(data: data, encoding: .utf8),
|
||||
error == nil else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
switch format {
|
||||
case .vtt:
|
||||
self.cues = self.parseVTT(content: content)
|
||||
case .srt:
|
||||
self.cues = self.parseSRT(content: content)
|
||||
case .unknown:
|
||||
if content.trimmed.hasPrefix("WEBVTT") {
|
||||
self.cues = self.parseVTT(content: content)
|
||||
} else {
|
||||
self.cues = self.parseSRT(content: content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
private func determineSubtitleFormat(from url: URL) -> SubtitleFormat {
|
||||
let fileExtension = url.pathExtension.lowercased()
|
||||
switch fileExtension {
|
||||
case "vtt", "webvtt":
|
||||
return .vtt
|
||||
case "srt":
|
||||
return .srt
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
private func parseVTT(content: String) -> [SubtitleCue] {
|
||||
var cues: [SubtitleCue] = []
|
||||
let lines = content.components(separatedBy: .newlines)
|
||||
var index = 0
|
||||
|
||||
while index < lines.count {
|
||||
let line = lines[index].trimmingCharacters(in: .whitespaces)
|
||||
if line.isEmpty || line == "WEBVTT" {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if !line.contains("-->") {
|
||||
index += 1
|
||||
if index >= lines.count { break }
|
||||
}
|
||||
|
||||
let timeLine = lines[index]
|
||||
let times = timeLine.components(separatedBy: "-->")
|
||||
if times.count < 2 {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
let startTime = parseTimecode(times[0].trimmingCharacters(in: .whitespaces))
|
||||
let adjustedStartTime = max(startTime - 0.5, 0)
|
||||
let endTime = parseTimecode(times[1].trimmingCharacters(in: .whitespaces))
|
||||
let adjusteEndTime = max(endTime - 0.5, 0)
|
||||
index += 1
|
||||
var cueText = ""
|
||||
while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
cueText += lines[index] + "\n"
|
||||
index += 1
|
||||
}
|
||||
cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjusteEndTime, text: cueText.trimmingCharacters(in: .whitespacesAndNewlines)))
|
||||
}
|
||||
return cues
|
||||
}
|
||||
|
||||
private func parseSRT(content: String) -> [SubtitleCue] {
|
||||
var cues: [SubtitleCue] = []
|
||||
let normalizedContent = content.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
.replacingOccurrences(of: "\r", with: "\n")
|
||||
let blocks = normalizedContent.components(separatedBy: "\n\n")
|
||||
|
||||
for block in blocks {
|
||||
let lines = block.components(separatedBy: "\n").filter { !$0.isEmpty }
|
||||
guard lines.count >= 2 else { continue }
|
||||
|
||||
let timeLine = lines[1]
|
||||
let times = timeLine.components(separatedBy: "-->")
|
||||
|
||||
guard times.count >= 2 else { continue }
|
||||
|
||||
let startTime = parseSRTTimecode(times[0].trimmingCharacters(in: .whitespaces))
|
||||
let adjustedStartTime = max(startTime - 0.5, 0)
|
||||
let endTime = parseSRTTimecode(times[1].trimmingCharacters(in: .whitespaces))
|
||||
let adjustedEndTime = max(endTime - 0.5, 0)
|
||||
|
||||
var textLines = [String]()
|
||||
if lines.count > 2 {
|
||||
textLines = Array(lines[2...])
|
||||
}
|
||||
let text = textLines.joined(separator: "\n")
|
||||
|
||||
cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjustedEndTime, text: text))
|
||||
}
|
||||
|
||||
return cues
|
||||
}
|
||||
|
||||
private func parseTimecode(_ timeString: String) -> Double {
|
||||
let parts = timeString.components(separatedBy: ":")
|
||||
var seconds = 0.0
|
||||
if parts.count == 3,
|
||||
let h = Double(parts[0]),
|
||||
let m = Double(parts[1]),
|
||||
let s = Double(parts[2].replacingOccurrences(of: ",", with: ".")) {
|
||||
seconds = h * 3600 + m * 60 + s
|
||||
} else if parts.count == 2,
|
||||
let m = Double(parts[0]),
|
||||
let s = Double(parts[1].replacingOccurrences(of: ",", with: ".")) {
|
||||
seconds = m * 60 + s
|
||||
}
|
||||
return seconds
|
||||
}
|
||||
|
||||
private func parseSRTTimecode(_ timeString: String) -> Double {
|
||||
let parts = timeString.components(separatedBy: ":")
|
||||
guard parts.count == 3 else { return 0 }
|
||||
|
||||
let secondsParts = parts[2].components(separatedBy: ",")
|
||||
guard secondsParts.count == 2,
|
||||
let hours = Double(parts[0]),
|
||||
let minutes = Double(parts[1]),
|
||||
let seconds = Double(secondsParts[0]),
|
||||
let milliseconds = Double(secondsParts[1]) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import AVKit
|
||||
import GroupActivities
|
||||
|
||||
class NormalPlayer: AVPlayerViewController {
|
||||
private var originalRate: Float = 1.0
|
||||
|
|
@ -17,22 +18,6 @@ class NormalPlayer: AVPlayerViewController {
|
|||
setupAudioSession()
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UserDefaults.standard.bool(forKey: "AlwaysLandscape") {
|
||||
return .landscape
|
||||
} else {
|
||||
return .all
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private func setupHoldGesture() {
|
||||
holdGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldGesture(_:)))
|
||||
holdGesture?.minimumPressDuration = 0.5
|
||||
|
|
@ -56,13 +41,13 @@ class NormalPlayer: AVPlayerViewController {
|
|||
guard let player = player else { return }
|
||||
originalRate = player.rate
|
||||
let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer")
|
||||
player.rate = holdSpeed
|
||||
player.rate = holdSpeed > 0 ? holdSpeed : 2.0
|
||||
}
|
||||
|
||||
private func endHoldSpeed() {
|
||||
player?.rate = originalRate
|
||||
}
|
||||
|
||||
|
||||
func setupAudioSession() {
|
||||
do {
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
|
|
@ -71,8 +56,7 @@ class NormalPlayer: AVPlayerViewController {
|
|||
|
||||
try audioSession.overrideOutputAudioPort(.speaker)
|
||||
} catch {
|
||||
print("Failed to set up AVAudioSession: \(error)")
|
||||
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
419
Sora/MediaUtils/NormalPlayer/VideoPlayer.swift
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
//
|
||||
// VideoPlayer.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 09/01/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVKit
|
||||
import Combine
|
||||
import GroupActivities
|
||||
|
||||
class VideoPlayerViewController: UIViewController {
|
||||
let module: ScrapingModule
|
||||
|
||||
var player: AVPlayer?
|
||||
var playerViewController: NormalPlayer?
|
||||
var timeObserverToken: Any?
|
||||
var streamUrl: String?
|
||||
var fullUrl: String = ""
|
||||
var subtitles: String = ""
|
||||
var aniListID: Int = 0
|
||||
var headers: [String:String]? = nil
|
||||
var totalEpisodes: Int = 0
|
||||
var tmdbID: Int? = nil
|
||||
var isMovie: Bool = false
|
||||
var seasonNumber: Int = 1
|
||||
var episodeNumber: Int = 0
|
||||
var episodeImageUrl: String = ""
|
||||
var mediaTitle: String = ""
|
||||
|
||||
private var groupSession: GroupSession<VideoWatchingActivity>?
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
private var isLaunchedFromSharePlay = false
|
||||
|
||||
private var aniListUpdateSent = false
|
||||
private var aniListUpdatedSuccessfully = false
|
||||
private var traktUpdateSent = false
|
||||
private var traktUpdatedSuccessfully = false
|
||||
|
||||
init(module: ScrapingModule) {
|
||||
self.module = module
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
configureGroupSession()
|
||||
if isLaunchedFromSharePlay {
|
||||
return
|
||||
}
|
||||
|
||||
setupVideoPlayer()
|
||||
}
|
||||
|
||||
private func setupVideoPlayer() {
|
||||
guard let streamUrl = streamUrl, let url = URL(string: streamUrl) else {
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if let mydict = headers, !mydict.isEmpty {
|
||||
for (key,value) in mydict {
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
} else {
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
|
||||
}
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
||||
player = AVPlayer(playerItem: playerItem)
|
||||
|
||||
playerViewController = NormalPlayer()
|
||||
playerViewController?.player = player
|
||||
|
||||
if let playerViewController = playerViewController {
|
||||
addChild(playerViewController)
|
||||
playerViewController.view.frame = view.bounds
|
||||
playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
view.addSubview(playerViewController.view)
|
||||
playerViewController.didMove(toParent: self)
|
||||
}
|
||||
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
||||
if lastPlayedTime > 0 {
|
||||
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
||||
self.player?.seek(to: seekTime) { _ in
|
||||
self.player?.play()
|
||||
}
|
||||
} else {
|
||||
self.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureGroupSession() {
|
||||
Task {
|
||||
for await groupSession in VideoWatchingActivity.sessions() {
|
||||
await configureGroupSession(groupSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func configureGroupSession(_ groupSession: GroupSession<VideoWatchingActivity>) async {
|
||||
self.groupSession = groupSession
|
||||
|
||||
let activity = groupSession.activity
|
||||
|
||||
if streamUrl == nil {
|
||||
streamUrl = activity.streamUrl
|
||||
fullUrl = activity.fullUrl
|
||||
subtitles = activity.subtitles
|
||||
aniListID = activity.aniListID
|
||||
headers = activity.headers
|
||||
totalEpisodes = activity.totalEpisodes
|
||||
tmdbID = activity.tmdbID
|
||||
isMovie = activity.isMovie
|
||||
seasonNumber = activity.seasonNumber
|
||||
episodeNumber = activity.episodeNumber
|
||||
episodeImageUrl = activity.episodeImageUrl
|
||||
mediaTitle = activity.mediaTitle
|
||||
|
||||
isLaunchedFromSharePlay = true
|
||||
|
||||
setupVideoPlayer()
|
||||
}
|
||||
|
||||
groupSession.$state
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] state in
|
||||
switch state {
|
||||
case .joined:
|
||||
self?.coordinatePlayback()
|
||||
Logger.shared.log("Joined SharePlay session", type: "SharePlay")
|
||||
case .invalidated:
|
||||
self?.groupSession = nil
|
||||
Logger.shared.log("SharePlay session invalidated", type: "SharePlay")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
|
||||
groupSession.join()
|
||||
Logger.shared.log("Automatically joining SharePlay session for: \(activity.mediaTitle)", type: "SharePlay")
|
||||
}
|
||||
|
||||
private func coordinatePlayback() {
|
||||
guard let player = player, let groupSession = groupSession else { return }
|
||||
|
||||
player.playbackCoordinator.coordinateWithSession(groupSession)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func startSharePlay() async {
|
||||
guard let streamUrl = streamUrl else { return }
|
||||
|
||||
var episodeImageData: Data?
|
||||
if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) {
|
||||
do {
|
||||
episodeImageData = try await URLSession.shared.data(from: imageUrl).0
|
||||
} catch {
|
||||
Logger.shared.log("Failed to load episode image: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
let activity = VideoWatchingActivity(
|
||||
mediaTitle: mediaTitle,
|
||||
episodeNumber: episodeNumber,
|
||||
streamUrl: streamUrl,
|
||||
subtitles: subtitles,
|
||||
aniListID: aniListID,
|
||||
fullUrl: fullUrl,
|
||||
headers: headers,
|
||||
episodeImageUrl: episodeImageUrl,
|
||||
episodeImageData: episodeImageData,
|
||||
totalEpisodes: totalEpisodes,
|
||||
tmdbID: tmdbID,
|
||||
isMovie: isMovie,
|
||||
seasonNumber: seasonNumber
|
||||
)
|
||||
|
||||
do {
|
||||
_ = try await activity.activate()
|
||||
Logger.shared.log("SharePlay session started successfully", type: "SharePlay")
|
||||
} catch {
|
||||
Logger.shared.log("Failed to start SharePlay: \(error)", type: "Error")
|
||||
|
||||
let alert = UIAlertController(
|
||||
title: "SharePlay Unavailable",
|
||||
message: "SharePlay is not available right now. Make sure you're connected to FaceTime or have SharePlay enabled in Control Center.",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !isLaunchedFromSharePlay {
|
||||
player?.play()
|
||||
setInitialPlayerRate()
|
||||
|
||||
Task {
|
||||
await checkForFaceTimeAndPromptSharePlay()
|
||||
}
|
||||
} else {
|
||||
setInitialPlayerRate()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func checkForFaceTimeAndPromptSharePlay() async {
|
||||
let activity = VideoWatchingActivity(
|
||||
mediaTitle: mediaTitle,
|
||||
episodeNumber: episodeNumber,
|
||||
streamUrl: streamUrl ?? "",
|
||||
subtitles: subtitles,
|
||||
aniListID: aniListID,
|
||||
fullUrl: fullUrl,
|
||||
headers: headers,
|
||||
episodeImageUrl: episodeImageUrl,
|
||||
episodeImageData: nil,
|
||||
totalEpisodes: totalEpisodes,
|
||||
tmdbID: tmdbID,
|
||||
isMovie: isMovie,
|
||||
seasonNumber: seasonNumber
|
||||
)
|
||||
|
||||
let result = await activity.prepareForActivation()
|
||||
if result == .activationPreferred {
|
||||
showSharePlayPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func showSharePlayPrompt() {
|
||||
let alert = UIAlertController(
|
||||
title: "Watch Together?",
|
||||
message: "You're in a FaceTime call. Would you like to share this video with everyone?",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Share Video", style: .default) { [weak self] _ in
|
||||
Task {
|
||||
await self?.startSharePlay()
|
||||
}
|
||||
})
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Watch Alone", style: .cancel))
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
if let playbackSpeed = player?.rate {
|
||||
UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed")
|
||||
}
|
||||
player?.pause()
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player?.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func setInitialPlayerRate() {
|
||||
if UserDefaults.standard.bool(forKey: "rememberPlaySpeed") {
|
||||
let lastPlayedSpeed = UserDefaults.standard.float(forKey: "lastPlaybackSpeed")
|
||||
player?.rate = lastPlayedSpeed > 0 ? lastPlayedSpeed : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
func addPeriodicTimeObserver(fullURL: String) {
|
||||
guard let player = self.player else { return }
|
||||
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
||||
guard let self = self,
|
||||
let currentItem = player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = time.seconds
|
||||
let duration = currentItem.duration.seconds
|
||||
|
||||
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||
|
||||
if let streamUrl = self.streamUrl {
|
||||
let progress = min(max(currentTime / duration, 0), 1.0)
|
||||
|
||||
let item = ContinueWatchingItem(
|
||||
id: UUID(),
|
||||
imageUrl: self.episodeImageUrl,
|
||||
episodeNumber: self.episodeNumber,
|
||||
mediaTitle: self.mediaTitle,
|
||||
progress: progress,
|
||||
streamUrl: streamUrl,
|
||||
fullUrl: self.fullUrl,
|
||||
subtitles: self.subtitles,
|
||||
aniListID: self.aniListID,
|
||||
module: self.module,
|
||||
headers: self.headers,
|
||||
totalEpisodes: self.totalEpisodes
|
||||
)
|
||||
ContinueWatchingManager.shared.save(item: item)
|
||||
}
|
||||
|
||||
let remainingPercentage = (duration - currentTime) / duration
|
||||
let remainingTimePercentage = UserDefaults.standard.object(forKey: "remainingTimePercentage") != nil ? UserDefaults.standard.double(forKey: "remainingTimePercentage") : 90.0
|
||||
let threshold = (100.0 - remainingTimePercentage) / 100.0
|
||||
|
||||
if remainingPercentage <= threshold {
|
||||
if self.aniListID != 0 && !self.aniListUpdateSent {
|
||||
self.sendAniListUpdate()
|
||||
}
|
||||
|
||||
if let tmdbId = self.tmdbID, tmdbId > 0, !self.traktUpdateSent {
|
||||
self.sendTraktUpdate(tmdbId: tmdbId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendAniListUpdate() {
|
||||
guard !aniListUpdateSent else { return }
|
||||
|
||||
aniListUpdateSent = true
|
||||
let aniListMutation = AniListMutation()
|
||||
|
||||
aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
self?.aniListUpdatedSuccessfully = true
|
||||
Logger.shared.log("Successfully updated AniList progress for Episode \(self?.episodeNumber ?? 0)", type: "General")
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to update AniList progress: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendTraktUpdate(tmdbId: Int) {
|
||||
guard !traktUpdateSent else { return }
|
||||
traktUpdateSent = true
|
||||
|
||||
let traktMutation = TraktMutation()
|
||||
|
||||
if self.isMovie {
|
||||
traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
self?.traktUpdatedSuccessfully = true
|
||||
Logger.shared.log("Successfully updated Trakt progress for movie (TMDB: \(tmdbId))", type: "General")
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to update Trakt progress for movie: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
guard self.episodeNumber > 0 && self.seasonNumber > 0 else {
|
||||
Logger.shared.log("Invalid episode (\(self.episodeNumber)) or season (\(self.seasonNumber)) number for Trakt update", type: "Error")
|
||||
return
|
||||
}
|
||||
|
||||
traktMutation.markAsWatched(
|
||||
type: "episode",
|
||||
tmdbID: tmdbId,
|
||||
episodeNumber: self.episodeNumber,
|
||||
seasonNumber: self.seasonNumber
|
||||
) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
self?.traktUpdatedSuccessfully = true
|
||||
Logger.shared.log("Successfully updated Trakt progress for Episode \(self?.episodeNumber ?? 0) (TMDB: \(tmdbId))", type: "General")
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to update Trakt progress for episode: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
|
||||
return .landscape
|
||||
} else {
|
||||
return .all
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
deinit {
|
||||
player?.pause()
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player?.removeTimeObserver(timeObserverToken)
|
||||
}
|
||||
|
||||
groupSession?.leave()
|
||||
subscriptions.removeAll()
|
||||
}
|
||||
}
|
||||
45
Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// VideoWatchingActivity.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 15/06/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
import GroupActivities
|
||||
|
||||
struct VideoWatchingActivity: GroupActivity {
|
||||
var metadata: GroupActivityMetadata {
|
||||
var metadata = GroupActivityMetadata()
|
||||
metadata.title = mediaTitle
|
||||
|
||||
if isMovie {
|
||||
metadata.subtitle = "Movie"
|
||||
} else {
|
||||
metadata.subtitle = "Episode \(episodeNumber)"
|
||||
}
|
||||
|
||||
if let imageData = episodeImageData,
|
||||
let uiImage = UIImage(data: imageData) {
|
||||
metadata.previewImage = uiImage.cgImage
|
||||
}
|
||||
|
||||
metadata.type = .watchTogether
|
||||
return metadata
|
||||
}
|
||||
|
||||
let mediaTitle: String
|
||||
let episodeNumber: Int
|
||||
let streamUrl: String
|
||||
let subtitles: String
|
||||
let aniListID: Int
|
||||
let fullUrl: String
|
||||
let headers: [String: String]?
|
||||
let episodeImageUrl: String
|
||||
let episodeImageData: Data?
|
||||
let totalEpisodes: Int
|
||||
let tmdbID: Int?
|
||||
let isMovie: Bool
|
||||
let seasonNumber: Int
|
||||
}
|
||||
|
|
@ -2,9 +2,19 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.group-session</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.assets.movies.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.assets.music.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// SoraApp.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
// Created by Francesco on 06/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
|
@ -10,40 +10,125 @@ import SwiftUI
|
|||
@main
|
||||
struct SoraApp: App {
|
||||
@StateObject private var settings = Settings()
|
||||
@StateObject private var modulesManager = ModulesManager()
|
||||
@StateObject private var moduleManager = ModuleManager()
|
||||
@StateObject private var libraryManager = LibraryManager()
|
||||
@StateObject private var downloadManager = DownloadManager()
|
||||
@StateObject private var jsController = JSController.shared
|
||||
|
||||
init() {
|
||||
if let userAccentColor = UserDefaults.standard.color(forKey: "accentColor") {
|
||||
UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = userAccentColor
|
||||
}
|
||||
clearTmpFolder()
|
||||
|
||||
TraktToken.checkAuthenticationStatus { isAuthenticated in
|
||||
if isAuthenticated {
|
||||
Logger.shared.log("Trakt authentication is valid", type: "Debug")
|
||||
} else {
|
||||
Logger.shared.log("Trakt authentication required", type: "Debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(settings)
|
||||
.environmentObject(modulesManager)
|
||||
.accentColor(settings.accentColor)
|
||||
.onAppear {
|
||||
settings.updateAppearance()
|
||||
Group {
|
||||
if !UserDefaults.standard.bool(forKey: "hideSplashScreen") {
|
||||
SplashScreenView()
|
||||
} else {
|
||||
ContentView()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handleURL(url)
|
||||
}
|
||||
.environment(\.layoutDirection, .leftToRight)
|
||||
.environmentObject(moduleManager)
|
||||
.environmentObject(settings)
|
||||
.environmentObject(libraryManager)
|
||||
.environmentObject(downloadManager)
|
||||
.environmentObject(jsController)
|
||||
.accentColor(settings.accentColor)
|
||||
.onAppear {
|
||||
settings.updateAppearance()
|
||||
Task {
|
||||
if UserDefaults.standard.bool(forKey: "refreshModulesOnLaunch") {
|
||||
await moduleManager.refreshModules()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handleURL(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleURL(_ url: URL) {
|
||||
guard url.scheme == "sora",
|
||||
url.host == "module",
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let moduleURL = components.queryItems?.first(where: { $0.name == "url" })?.value else {
|
||||
return
|
||||
}
|
||||
|
||||
modulesManager.addModule(from: moduleURL) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
NotificationCenter.default.post(name: .moduleAdded, object: nil)
|
||||
Logger.shared.log("Successfully added module from URL scheme: \(moduleURL)")
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to add module from URL scheme: \(error.localizedDescription)")
|
||||
guard url.scheme == "sora", let host = url.host else { return }
|
||||
switch host {
|
||||
case "default_page":
|
||||
if let comps = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let libraryURL = comps.queryItems?.first(where: { $0.name == "url" })?.value {
|
||||
|
||||
UserDefaults.standard.set(libraryURL, forKey: "lastCommunityURL")
|
||||
UserDefaults.standard.set(true, forKey: "didReceiveDefaultPageLink")
|
||||
|
||||
DropManager.shared.showDrop(
|
||||
title: "Module Library Added",
|
||||
subtitle: "You can browse the community library in settings.",
|
||||
duration: 2,
|
||||
icon: UIImage(systemName: "books.vertical.circle.fill")
|
||||
)
|
||||
}
|
||||
|
||||
case "module":
|
||||
guard url.scheme == "sora",
|
||||
url.host == "module",
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let moduleURL = components.queryItems?.first(where: { $0.name == "url" })?.value
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let addModuleView = ModuleAdditionSettingsView(moduleUrl: moduleURL).environmentObject(moduleManager)
|
||||
let hostingController = UIHostingController(rootView: addModuleView)
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first {
|
||||
window.rootViewController?.present(hostingController, animated: true)
|
||||
} else {
|
||||
Logger.shared.log(
|
||||
"Failed to present module addition view: No window scene found",
|
||||
type: "Error"
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func clearTmpFolder() {
|
||||
let fileManager = FileManager.default
|
||||
let tmpDirectory = NSTemporaryDirectory()
|
||||
|
||||
do {
|
||||
let tmpURL = URL(fileURLWithPath: tmpDirectory)
|
||||
let tmpContents = try fileManager.contentsOfDirectory(at: tmpURL, includingPropertiesForKeys: nil)
|
||||
|
||||
for url in tmpContents {
|
||||
try fileManager.removeItem(at: url)
|
||||
}
|
||||
|
||||
let parentURL = tmpURL.deletingLastPathComponent()
|
||||
let parentContents = try fileManager.contentsOfDirectory(at: parentURL, includingPropertiesForKeys: [.isDirectoryKey])
|
||||
for url in parentContents {
|
||||
if url.lastPathComponent.hasPrefix("com.apple.UserManagedAssets") {
|
||||
var isDir: ObjCBool = false
|
||||
if fileManager.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue {
|
||||
try fileManager.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Failed to clear tmp folder: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
43
Sora/Tracking & Metadata/AniList/Auth/Anilist-Login.swift
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// Login.swift
|
||||
// Ryu
|
||||
//
|
||||
// Created by Francesco on 08/08/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class AniListLogin {
|
||||
static let clientID = "19551"
|
||||
static let redirectURI = "sora://anilist"
|
||||
|
||||
static let authorizationEndpoint = "https://anilist.co/api/v2/oauth/authorize"
|
||||
|
||||
static func authenticate() {
|
||||
let urlString = "\(authorizationEndpoint)?client_id=\(clientID)&redirect_uri=\(redirectURI)&response_type=code"
|
||||
guard let url = URL(string: urlString) else {
|
||||
Logger.shared.log("Invalid authorization URL", type: "Error")
|
||||
return
|
||||
}
|
||||
|
||||
WebAuthenticationManager.shared.authenticate(url: url, callbackScheme: "sora") { result in
|
||||
switch result {
|
||||
case .success(let callbackURL):
|
||||
if let params = callbackURL.queryParameters,
|
||||
let code = params["code"] {
|
||||
AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in
|
||||
if success {
|
||||
Logger.shared.log("AniList token exchange successful", type: "Debug")
|
||||
} else {
|
||||
Logger.shared.log("AniList token exchange failed", type: "Error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("No authorization code in callback URL", type: "Error")
|
||||
}
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Authentication failed: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Sora/Tracking & Metadata/AniList/Auth/Anilist-Token.swift
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// Token.swift
|
||||
// Ryu
|
||||
//
|
||||
// Created by Francesco on 08/08/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
class AniListToken {
|
||||
static let clientID = "19551"
|
||||
static let clientSecret = "fk8EgkyFbXk95TbPwLYQLaiMaNIryMpDBwJsPXoX"
|
||||
static let redirectURI = "sora://anilist"
|
||||
|
||||
static let tokenEndpoint = "https://anilist.co/api/v2/oauth/token"
|
||||
static let serviceName = "me.cranci.sora.AniListToken"
|
||||
static let accountName = "AniListAccessToken"
|
||||
|
||||
static let authSuccessNotification = Notification.Name("AniListAuthenticationSuccess")
|
||||
static let authFailureNotification = Notification.Name("AniListAuthenticationFailure")
|
||||
|
||||
static func saveTokenToKeychain(token: String) -> Bool {
|
||||
let tokenData = token.data(using: .utf8)!
|
||||
|
||||
let deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: accountName
|
||||
]
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
|
||||
let addQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: accountName,
|
||||
kSecValueData as String: tokenData
|
||||
]
|
||||
|
||||
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
static func exchangeAuthorizationCodeForToken(code: String, completion: @escaping (Bool) -> Void) {
|
||||
Logger.shared.log("Exchanging authorization code for access token...")
|
||||
|
||||
guard let url = URL(string: tokenEndpoint) else {
|
||||
Logger.shared.log("Invalid token endpoint URL", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "Invalid token endpoint URL"])
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let bodyString = "grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(redirectURI)&code=\(code)"
|
||||
request.httpBody = bodyString.data(using: .utf8)
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
Logger.shared.log("Error: \(error.localizedDescription)", type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": error.localizedDescription])
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
Logger.shared.log("No data received", type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "No data received"])
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
if let accessToken = json["access_token"] as? String {
|
||||
let success = saveTokenToKeychain(token: accessToken)
|
||||
if success {
|
||||
NotificationCenter.default.post(name: authSuccessNotification, object: nil)
|
||||
} else {
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "Failed to save token to keychain"])
|
||||
}
|
||||
completion(success)
|
||||
} else {
|
||||
let errorMessage = (json["error"] as? String) ?? "Unexpected response"
|
||||
Logger.shared.log("Authentication error: \(errorMessage)", type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": errorMessage])
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Failed to parse JSON: \(error.localizedDescription)", type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "Failed to parse response: \(error.localizedDescription)"])
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
//
|
||||
// AniListPushUpdates.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 07/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
class AniListMutation {
|
||||
let apiURL = URL(string: "https://graphql.anilist.co")!
|
||||
|
||||
func getTokenFromKeychain() -> String? {
|
||||
let serviceName = "me.cranci.sora.AniListToken"
|
||||
let accountName = "AniListAccessToken"
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: accountName,
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
|
||||
guard status == errSecSuccess, let tokenData = item as? Data else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return String(data: tokenData, encoding: .utf8)
|
||||
}
|
||||
|
||||
func updateAnimeProgress(
|
||||
animeId: Int,
|
||||
episodeNumber: Int,
|
||||
status: String = "CURRENT",
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
if let sendPushUpdates = UserDefaults.standard.object(forKey: "sendPushUpdates") as? Bool,
|
||||
sendPushUpdates == false {
|
||||
return
|
||||
}
|
||||
|
||||
guard let userToken = getTokenFromKeychain() else {
|
||||
completion(.failure(NSError(
|
||||
domain: "",
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Access token not found"]
|
||||
)))
|
||||
return
|
||||
}
|
||||
|
||||
let query = """
|
||||
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
||||
SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status) {
|
||||
id
|
||||
progress
|
||||
status
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
let variables: [String: Any] = [
|
||||
"mediaId": animeId,
|
||||
"progress": episodeNumber,
|
||||
"status": status
|
||||
]
|
||||
|
||||
let requestBody: [String: Any] = [
|
||||
"query": query,
|
||||
"variables": variables
|
||||
]
|
||||
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody, options: []) else {
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to serialize JSON"])))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: apiURL)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = jsonData
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
||||
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||
completion(.failure(NSError(domain: "", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Unexpected response or status code"])))
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
do {
|
||||
_ = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
Logger.shared.log("Successfully updated anime progress", type: "Debug")
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func fetchMediaStatus(
|
||||
mediaId: Int,
|
||||
completion: @escaping (Result<String, Error>) -> Void
|
||||
) {
|
||||
guard let token = getTokenFromKeychain() else {
|
||||
completion(.failure(NSError(
|
||||
domain: "", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Access token not found"]
|
||||
)))
|
||||
return
|
||||
}
|
||||
|
||||
let query = """
|
||||
query ($mediaId: Int) {
|
||||
Media(id: $mediaId) {
|
||||
status
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
let vars = ["mediaId": mediaId]
|
||||
var req = URLRequest(url: URL(string: "https://graphql.anilist.co")!)
|
||||
req.httpMethod = "POST"
|
||||
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
req.httpBody = try? JSONSerialization.data(
|
||||
withJSONObject: ["query": query, "variables": vars]
|
||||
)
|
||||
|
||||
URLSession.shared.dataTask(with: req) { data, _, error in
|
||||
if let e = error { return completion(.failure(e)) }
|
||||
guard
|
||||
let d = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: d) as? [String: Any],
|
||||
let md = (json["data"] as? [String: Any])?["Media"] as? [String: Any],
|
||||
let status = md["status"] as? String
|
||||
else {
|
||||
return completion(.failure(NSError(
|
||||
domain: "", code: -2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Invalid response"]
|
||||
)))
|
||||
}
|
||||
completion(.success(status))
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchMalID(animeId: Int, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
let query = """
|
||||
query ($id: Int) {
|
||||
Media(id: $id) {
|
||||
idMal
|
||||
}
|
||||
}
|
||||
"""
|
||||
let variables: [String: Any] = ["id": animeId]
|
||||
let requestBody: [String: Any] = [
|
||||
"query": query,
|
||||
"variables": variables
|
||||
]
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody, options: []) else {
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to serialize GraphQL request"])))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: apiURL)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = jsonData
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, resp, error in
|
||||
if let e = error {
|
||||
return completion(.failure(e))
|
||||
}
|
||||
guard let data = data,
|
||||
let json = try? JSONDecoder().decode(AniListMediaResponse.self, from: data),
|
||||
let mal = json.data.Media?.idMal else {
|
||||
return completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to decode AniList response or idMal missing"])))
|
||||
}
|
||||
completion(.success(mal))
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchCoverImage(
|
||||
animeId: Int,
|
||||
completion: @escaping (Result<String, Error>) -> Void
|
||||
) {
|
||||
let query = """
|
||||
query ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
coverImage { large }
|
||||
}
|
||||
}
|
||||
"""
|
||||
let variables = ["id": animeId]
|
||||
let body: [String: Any] = ["query": query, "variables": variables]
|
||||
|
||||
guard let url = URL(string: "https://graphql.anilist.co"),
|
||||
let httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
else {
|
||||
completion(.failure(NSError(domain: "AniList", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL or payload"])))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = httpBody
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
if let error = error {
|
||||
return completion(.failure(error))
|
||||
}
|
||||
guard let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataDict = json["data"] as? [String: Any],
|
||||
let media = dataDict["Media"] as? [String: Any],
|
||||
let cover = media["coverImage"] as? [String: Any],
|
||||
let imageUrl = cover["large"] as? String
|
||||
else {
|
||||
return completion(.failure(NSError(domain: "AniList", code: 1, userInfo: [NSLocalizedDescriptionKey: "Malformed response"])))
|
||||
}
|
||||
completion(.success(imageUrl))
|
||||
}
|
||||
.resume()
|
||||
}
|
||||
|
||||
private struct AniListMediaResponse: Decodable {
|
||||
struct DataField: Decodable {
|
||||
struct Media: Decodable { let idMal: Int? }
|
||||
let Media: Media?
|
||||
}
|
||||
let data: DataField
|
||||
}
|
||||
}
|
||||
|
||||
struct EpisodeMetadataInfo: Codable, Equatable {
|
||||
let title: [String: String]
|
||||
let imageUrl: String
|
||||
let anilistId: Int
|
||||
let episodeNumber: Int
|
||||
}
|
||||
104
Sora/Tracking & Metadata/TMDB/TMDB-FetchID.swift
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// TMDB-FetchID.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 01/06/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TMDBFetcher {
|
||||
enum MediaType: String, CaseIterable {
|
||||
case tv, movie
|
||||
}
|
||||
|
||||
struct TMDBResult: Decodable {
|
||||
let id: Int
|
||||
let name: String?
|
||||
let title: String?
|
||||
let popularity: Double
|
||||
}
|
||||
|
||||
struct TMDBResponse: Decodable {
|
||||
let results: [TMDBResult]
|
||||
}
|
||||
|
||||
let apiKey = "738b4edd0a156cc126dc4a4b8aea4aca"
|
||||
private let session = URLSession.custom
|
||||
|
||||
func fetchBestMatchID(for title: String, completion: @escaping (Int?, MediaType?) -> Void) {
|
||||
let group = DispatchGroup()
|
||||
var bestResults: [(id: Int, score: Double, type: MediaType)] = []
|
||||
|
||||
for type in MediaType.allCases {
|
||||
group.enter()
|
||||
fetchBestMatchID(for: title, type: type) { id, score in
|
||||
if let id = id, let score = score {
|
||||
bestResults.append((id, score, type))
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let best = bestResults.max { $0.score < $1.score }
|
||||
completion(best?.id, best?.type)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchBestMatchID(for title: String, type: MediaType, completion: @escaping (Int?, Double?) -> Void) {
|
||||
let query = title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
let urlString = "https://api.themoviedb.org/3/search/\(type.rawValue)?api_key=\(apiKey)&query=\(query)"
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion(nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
session.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(nil, nil)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let response = try JSONDecoder().decode(TMDBResponse.self, from: data)
|
||||
let scored = response.results.map { result -> (Int, Double) in
|
||||
let candidateTitle = type == .tv ? result.name ?? "" : result.title ?? ""
|
||||
let similarity = TMDBFetcher.titleSimilarity(title, candidateTitle)
|
||||
let score = (similarity * 0.7) + ((result.popularity / 100.0) * 0.3)
|
||||
return (result.id, score)
|
||||
}
|
||||
let best = scored.max { $0.1 < $1.1 }
|
||||
completion(best?.0, best?.1)
|
||||
} catch {
|
||||
completion(nil, nil)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
static func titleSimilarity(_ a: String, _ b: String) -> Double {
|
||||
let lowerA = a.lowercased()
|
||||
let lowerB = b.lowercased()
|
||||
let distance = Double(levenshtein(lowerA, lowerB))
|
||||
let maxLen = Double(max(lowerA.count, lowerB.count))
|
||||
if maxLen == 0 { return 1.0 }
|
||||
return 1.0 - (distance / maxLen)
|
||||
}
|
||||
|
||||
static func levenshtein(_ a: String, _ b: String) -> Int {
|
||||
let a = Array(a)
|
||||
let b = Array(b)
|
||||
var dist = Array(repeating: Array(repeating: 0, count: b.count + 1), count: a.count + 1)
|
||||
for i in 0...a.count { dist[i][0] = i }
|
||||
for j in 0...b.count { dist[0][j] = j }
|
||||
for i in 1...a.count {
|
||||
for j in 1...b.count {
|
||||
if a[i-1] == b[j-1] {
|
||||
dist[i][j] = dist[i-1][j-1]
|
||||
} else {
|
||||
dist[i][j] = min(dist[i-1][j-1], dist[i][j-1], dist[i-1][j]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist[a.count][b.count]
|
||||
}
|
||||
}
|
||||
43
Sora/Tracking & Metadata/Trakt/Auth/Trakt-Login.swift
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// Trakt-Login.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TraktLogin {
|
||||
static let clientID = "6ec81bf19deb80fdfa25652eef101576ca6aaa0dc016d36079b2de413d71c369"
|
||||
static let redirectURI = "sora://trakt"
|
||||
|
||||
static let authorizationEndpoint = "https://trakt.tv/oauth/authorize"
|
||||
|
||||
static func authenticate() {
|
||||
let urlString = "\(authorizationEndpoint)?client_id=\(clientID)&redirect_uri=\(redirectURI)&response_type=code"
|
||||
guard let url = URL(string: urlString) else {
|
||||
Logger.shared.log("Invalid authorization URL", type: "Error")
|
||||
return
|
||||
}
|
||||
|
||||
WebAuthenticationManager.shared.authenticate(url: url, callbackScheme: "sora") { result in
|
||||
switch result {
|
||||
case .success(let callbackURL):
|
||||
if let params = callbackURL.queryParameters,
|
||||
let code = params["code"] {
|
||||
TraktToken.exchangeAuthorizationCodeForToken(code: code) { success in
|
||||
if success {
|
||||
Logger.shared.log("Trakt token exchange successful", type: "Debug")
|
||||
} else {
|
||||
Logger.shared.log("Trakt token exchange failed", type: "Error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("No authorization code in callback URL", type: "Error")
|
||||
}
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Authentication failed: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
246
Sora/Tracking & Metadata/Trakt/Auth/Trakt-Token.swift
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
//
|
||||
// Trakt-Token.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
class TraktToken {
|
||||
static let clientID = "6ec81bf19deb80fdfa25652eef101576ca6aaa0dc016d36079b2de413d71c369"
|
||||
static let clientSecret = "17cd92f71da3be9d755e2d8a6506fb3c3ecee19a247a6f0120ce2fb1f359850b"
|
||||
static let redirectURI = "sora://trakt"
|
||||
|
||||
static let tokenEndpoint = "https://api.trakt.tv/oauth/token"
|
||||
static let serviceName = "me.cranci.sora.TraktToken"
|
||||
static let accessTokenKey = "TraktAccessToken"
|
||||
static let refreshTokenKey = "TraktRefreshToken"
|
||||
|
||||
static let authSuccessNotification = Notification.Name("TraktAuthenticationSuccess")
|
||||
static let authFailureNotification = Notification.Name("TraktAuthenticationFailure")
|
||||
|
||||
private static func saveToKeychain(key: String, data: String) -> Bool {
|
||||
let tokenData = data.data(using: .utf8)!
|
||||
|
||||
let deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
|
||||
let addQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecValueData as String: tokenData
|
||||
]
|
||||
|
||||
return SecItemAdd(addQuery as CFDictionary, nil) == errSecSuccess
|
||||
}
|
||||
|
||||
static func exchangeAuthorizationCodeForToken(code: String, completion: @escaping (Bool) -> Void) {
|
||||
guard let url = URL(string: tokenEndpoint) else {
|
||||
Logger.shared.log("Invalid token endpoint URL", type: "Error")
|
||||
handleFailure(error: "Invalid token endpoint URL", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let bodyData: [String: Any] = [
|
||||
"code": code,
|
||||
"client_id": clientID,
|
||||
"client_secret": clientSecret,
|
||||
"redirect_uri": redirectURI,
|
||||
"grant_type": "authorization_code"
|
||||
]
|
||||
|
||||
processTokenRequest(request: request, bodyData: bodyData, completion: completion)
|
||||
}
|
||||
|
||||
static func refreshAccessToken(completion: @escaping (Bool) -> Void) {
|
||||
guard let refreshToken = getRefreshToken() else {
|
||||
handleFailure(error: "No refresh token available", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: tokenEndpoint) else {
|
||||
handleFailure(error: "Invalid token endpoint URL", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let bodyData: [String: Any] = [
|
||||
"refresh_token": refreshToken,
|
||||
"client_id": clientID,
|
||||
"client_secret": clientSecret,
|
||||
"redirect_uri": redirectURI,
|
||||
"grant_type": "refresh_token"
|
||||
]
|
||||
|
||||
processTokenRequest(request: request, bodyData: bodyData, completion: completion)
|
||||
}
|
||||
|
||||
private static func processTokenRequest(request: URLRequest, bodyData: [String: Any], completion: @escaping (Bool) -> Void) {
|
||||
var request = request
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: bodyData)
|
||||
} catch {
|
||||
handleFailure(error: "Failed to create request body", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
handleFailure(error: error.localizedDescription, completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
handleFailure(error: "No data received", completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
if let accessToken = json["access_token"] as? String,
|
||||
let refreshToken = json["refresh_token"] as? String {
|
||||
|
||||
let accessSuccess = saveToKeychain(key: accessTokenKey, data: accessToken)
|
||||
let refreshSuccess = saveToKeychain(key: refreshTokenKey, data: refreshToken)
|
||||
|
||||
if accessSuccess && refreshSuccess {
|
||||
NotificationCenter.default.post(name: authSuccessNotification, object: nil)
|
||||
completion(true)
|
||||
} else {
|
||||
handleFailure(error: "Failed to save tokens to keychain", completion: completion)
|
||||
}
|
||||
} else {
|
||||
let errorMessage = (json["error"] as? String) ?? "Unexpected response"
|
||||
handleFailure(error: errorMessage, completion: completion)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
handleFailure(error: "Failed to parse response: \(error.localizedDescription)", completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
private static func handleFailure(error: String, completion: @escaping (Bool) -> Void) {
|
||||
Logger.shared.log(error, type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": error])
|
||||
completion(false)
|
||||
}
|
||||
|
||||
private static func getRefreshToken() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: refreshTokenKey,
|
||||
kSecReturnData as String: true
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let tokenData = result as? Data,
|
||||
let token = String(data: tokenData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
static func getAccessToken() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: serviceName,
|
||||
kSecAttrAccount as String: accessTokenKey,
|
||||
kSecReturnData as String: true
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let tokenData = result as? Data,
|
||||
let token = String(data: tokenData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
static func validateToken(completion: @escaping (Bool) -> Void) {
|
||||
guard let token = getAccessToken() else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: "https://api.trakt.tv/users/settings") else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("2", forHTTPHeaderField: "trakt-api-version")
|
||||
request.setValue(clientID, forHTTPHeaderField: "trakt-api-key")
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { _, response, _ in
|
||||
DispatchQueue.main.async {
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
let isValid = httpResponse.statusCode == 200
|
||||
completion(isValid)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
static func validateAndRefreshTokenIfNeeded(completion: @escaping (Bool) -> Void) {
|
||||
if getAccessToken() == nil {
|
||||
if getRefreshToken() != nil {
|
||||
refreshAccessToken(completion: completion)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
validateToken { isValid in
|
||||
if isValid {
|
||||
completion(true)
|
||||
} else {
|
||||
if getRefreshToken() != nil {
|
||||
refreshAccessToken(completion: completion)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func checkAuthenticationStatus(completion: @escaping (Bool) -> Void) {
|
||||
validateAndRefreshTokenIfNeeded(completion: completion)
|
||||
}
|
||||
}
|
||||
153
Sora/Tracking & Metadata/Trakt/Mutations/TraktPushUpdates.swift
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
//
|
||||
// TraktPushUpdates.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
class TraktMutation {
|
||||
let apiURL = URL(string: "https://api.trakt.tv")!
|
||||
|
||||
func getTokenFromKeychain() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: TraktToken.serviceName,
|
||||
kSecAttrAccount as String: TraktToken.accessTokenKey,
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess,
|
||||
let tokenData = item as? Data,
|
||||
let token = String(data: tokenData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func markAsWatched(type: String, tmdbID: Int, episodeNumber: Int? = nil, seasonNumber: Int? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let sendTraktUpdates = UserDefaults.standard.object(forKey: "sendTraktUpdates") as? Bool ?? true
|
||||
if !sendTraktUpdates {
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Trakt updates disabled by user"])))
|
||||
return
|
||||
}
|
||||
|
||||
guard let userToken = getTokenFromKeychain() else {
|
||||
Logger.shared.log("Trakt access token not found in keychain", type: "Error")
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Access token not found"])))
|
||||
return
|
||||
}
|
||||
|
||||
let endpoint = "/sync/history"
|
||||
let watchedAt = ISO8601DateFormatter().string(from: Date())
|
||||
let body: [String: Any]
|
||||
|
||||
switch type {
|
||||
case "movie":
|
||||
body = [
|
||||
"movies": [
|
||||
[
|
||||
"ids": ["tmdb": tmdbID],
|
||||
"watched_at": watchedAt
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
case "episode":
|
||||
guard let episode = episodeNumber, let season = seasonNumber else {
|
||||
let errorMsg = "Missing episode (\(episodeNumber ?? -1)) or season (\(seasonNumber ?? -1)) number"
|
||||
Logger.shared.log(errorMsg, type: "Error")
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: errorMsg])))
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shared.log("Preparing episode watch request - TMDB ID: \(tmdbID), Season: \(season), Episode: \(episode)", type: "Debug")
|
||||
body = [
|
||||
"shows": [
|
||||
[
|
||||
"ids": ["tmdb": tmdbID],
|
||||
"seasons": [
|
||||
[
|
||||
"number": season,
|
||||
"episodes": [
|
||||
[
|
||||
"number": episode,
|
||||
"watched_at": watchedAt
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
default:
|
||||
Logger.shared.log("Invalid content type: \(type)", type: "Error")
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid content type"])))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: apiURL.appendingPathComponent(endpoint))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("2", forHTTPHeaderField: "trakt-api-version")
|
||||
request.setValue(TraktToken.clientID, forHTTPHeaderField: "trakt-api-key")
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: body, options: [.prettyPrinted])
|
||||
request.httpBody = jsonData
|
||||
|
||||
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||
Logger.shared.log("Trakt API Request Body: \(jsonString)", type: "Debug")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Failed to serialize request body: \(error.localizedDescription)", type: "Error")
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
Logger.shared.log("Trakt API network error: \(error.localizedDescription)", type: "Error")
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
Logger.shared.log("Trakt API: No HTTP response received", type: "Error")
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No HTTP response"])))
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data, let responseString = String(data: data, encoding: .utf8) {
|
||||
Logger.shared.log("Trakt API Response Body: \(responseString)", type: "Debug")
|
||||
}
|
||||
|
||||
if (200...299).contains(httpResponse.statusCode) {
|
||||
Logger.shared.log("Successfully updated watch status on Trakt for \(type)", type: "General")
|
||||
completion(.success(()))
|
||||
} else {
|
||||
var errorMessage = "HTTP \(httpResponse.statusCode)"
|
||||
if let data = data,
|
||||
let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
if let error = errorJson["error"] as? String {
|
||||
errorMessage = "\(errorMessage): \(error)"
|
||||
}
|
||||
if let errorDescription = errorJson["error_description"] as? String {
|
||||
errorMessage = "\(errorMessage) - \(errorDescription)"
|
||||
}
|
||||
}
|
||||
Logger.shared.log("Trakt API Error: \(errorMessage)", type: "Error")
|
||||
completion(.failure(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage])))
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
//
|
||||
// Double+Extension.swift
|
||||
// AppleMusicSlider
|
||||
//
|
||||
// Created by Pratik on 14/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension BinaryFloatingPoint {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: TimeInterval(self)) ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
//
|
||||
// MusicProgressSlider.swift
|
||||
// Custom Seekbar
|
||||
//
|
||||
// Created by Pratik on 08/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
// I did edit just a little bit the code for my liking
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack(alignment: .center) {
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(width: max(bounds.size.width * CGFloat((localRealProgress + localTempProgress)), 0), alignment: .leading)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(value.asTimeString(style: .positional))
|
||||
Spacer(minLength: 0)
|
||||
Text("-" + (inRange.upperBound - value).asTimeString(style: .positional))
|
||||
}
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(isActive ? fillColor : emptyColor)
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.04 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
}
|
||||
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { value, state, transaction in
|
||||
state = true
|
||||
}
|
||||
.onChanged { gesture in
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
}.onEnded { value in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
})
|
||||
.onChange(of: isActive) { newValue in
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = getPrgPercentage(value)
|
||||
}
|
||||
.onChange(of: value) { newValue in
|
||||
if !isActive {
|
||||
localRealProgress = getPrgPercentage(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: isActive ? height * 1.25 : height, alignment: .center)
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
if isActive {
|
||||
return .spring()
|
||||
} else {
|
||||
return .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
private func getPrgPercentage(_ value: T) -> T {
|
||||
let range = inRange.upperBound - inRange.lowerBound
|
||||
let correctedStartValue = value - inRange.lowerBound
|
||||
let percentage = correctedStartValue / range
|
||||
return percentage
|
||||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
}
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// test2
|
||||
//
|
||||
// Created by Francesco on 20/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct CustomVideoPlayer: UIViewControllerRepresentable {
|
||||
let player: AVPlayer
|
||||
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
let controller = NormalPlayer()
|
||||
controller.player = player
|
||||
controller.showsPlaybackControls = false
|
||||
player.play()
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomMediaPlayer: View {
|
||||
@State private var player: AVPlayer
|
||||
@State private var isPlaying = true
|
||||
@State private var currentTime: Double = 0.0
|
||||
@State private var duration: Double = 0.0
|
||||
@State private var showControls = false
|
||||
@State private var inactivityTimer: Timer?
|
||||
@State private var timeObserverToken: Any?
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
let module: ModuleStruct
|
||||
let fullUrl: String
|
||||
let title: String
|
||||
let episodeNumber: Int
|
||||
let onWatchNext: () -> Void
|
||||
|
||||
init(module: ModuleStruct, urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) {
|
||||
guard let url = URL(string: urlString) else {
|
||||
fatalError("Invalid URL string")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if urlString.contains("ascdn") {
|
||||
request.addValue("\(module.module[0].details.baseURL)", forHTTPHeaderField: "Referer")
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
_player = State(initialValue: AVPlayer(playerItem: AVPlayerItem(asset: asset)))
|
||||
|
||||
self.module = module
|
||||
self.fullUrl = fullUrl
|
||||
self.title = title
|
||||
self.episodeNumber = episodeNumber
|
||||
self.onWatchNext = onWatchNext
|
||||
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
||||
if lastPlayedTime > 0 {
|
||||
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
||||
self._player.wrappedValue.seek(to: seekTime)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack {
|
||||
CustomVideoPlayer(player: player)
|
||||
.onAppear {
|
||||
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 600), queue: .main) { time in
|
||||
currentTime = time.seconds
|
||||
if let itemDuration = player.currentItem?.duration.seconds, itemDuration.isFinite && !itemDuration.isNaN {
|
||||
duration = itemDuration
|
||||
}
|
||||
}
|
||||
startUpdatingCurrentTime()
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.overlay(
|
||||
Group {
|
||||
if showControls {
|
||||
Color.black.opacity(0.5)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {
|
||||
currentTime = max(currentTime - 10, 0)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "gobackward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Button(action: {
|
||||
if isPlaying {
|
||||
player.pause()
|
||||
} else {
|
||||
player.play()
|
||||
}
|
||||
isPlaying.toggle()
|
||||
}) {
|
||||
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 45))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Button(action: {
|
||||
currentTime = min(currentTime + 10, duration)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "goforward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2), value: showControls),
|
||||
alignment: .center
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
showControls.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack(alignment: .bottom) {
|
||||
if showControls {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
Spacer()
|
||||
if duration - currentTime <= duration * 0.10 && currentTime != duration {
|
||||
Button(action: {
|
||||
player.pause()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
onWatchNext()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "forward.fill")
|
||||
.foregroundColor(Color.black)
|
||||
Text("Watch Next")
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(32)
|
||||
}
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
if showControls {
|
||||
Menu {
|
||||
Menu("Playback Speed") {
|
||||
ForEach([0.5, 1.0, 1.25, 1.5, 1.75, 2.0], id: \.self) { speed in
|
||||
Button(action: {
|
||||
player.rate = Float(speed)
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Text("\(speed, specifier: "%.2f")")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 15))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 32)
|
||||
|
||||
if showControls {
|
||||
MusicProgressSlider(
|
||||
value: $currentTime,
|
||||
inRange: 0...duration,
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.5),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 28,
|
||||
onEditingChanged: { editing in
|
||||
if !editing {
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
startUpdatingCurrentTime()
|
||||
}
|
||||
.onDisappear {
|
||||
player.pause()
|
||||
inactivityTimer?.invalidate()
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
if showControls {
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.contentShape(Rectangle())
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startUpdatingCurrentTime() {
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
currentTime = player.currentTime().seconds
|
||||
}
|
||||
}
|
||||
|
||||
private func addPeriodicTimeObserver(fullURL: String) {
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||
guard let currentItem = player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = time.seconds
|
||||
let duration = currentItem.duration.seconds
|
||||
|
||||
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Notification.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Notification.Name {
|
||||
static let moduleAdded = Notification.Name("moduleAdded")
|
||||
static let moduleRemoved = Notification.Name("moduleRemoved")
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
//
|
||||
// URLSession.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 24/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URLSession {
|
||||
static let custom: URLSession = {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.httpAdditionalHeaders = [
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
]
|
||||
return URLSession(configuration: configuration)
|
||||
}()
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
//
|
||||
// GitHubAPI.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 31/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GitHubReleases: Codable {
|
||||
let tagName: String
|
||||
let body: String
|
||||
let htmlUrl: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tagName = "tag_name"
|
||||
case body
|
||||
case htmlUrl = "html_url"
|
||||
}
|
||||
}
|
||||
|
||||
class GitHubAPI {
|
||||
static let shared = GitHubAPI()
|
||||
|
||||
func fetchReleases(completion: @escaping ([GitHubReleases]?) -> Void) {
|
||||
let url = URL(string: "https://api.github.com/repos/cranci1/Sora/releases")!
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, response, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let releases = try? JSONDecoder().decode([GitHubReleases].self, from: data)
|
||||
completion(releases)
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
//
|
||||
// GitHubRelease.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 29/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GitHubRelease: Codable {
|
||||
let url: String
|
||||
let assetsUrl: String
|
||||
let uploadUrl: String
|
||||
let htmlUrl: String
|
||||
let id: Int
|
||||
let author: Author
|
||||
let nodeId: String
|
||||
let tagName: String
|
||||
let targetCommitish: String
|
||||
let name: String
|
||||
let draft: Bool
|
||||
let prerelease: Bool
|
||||
let createdAt: String
|
||||
let publishedAt: String
|
||||
let assets: [Asset]
|
||||
let tarballUrl: String
|
||||
let zipballUrl: String
|
||||
let body: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case assetsUrl = "assets_url"
|
||||
case uploadUrl = "upload_url"
|
||||
case htmlUrl = "html_url"
|
||||
case id
|
||||
case author
|
||||
case nodeId = "node_id"
|
||||
case tagName = "tag_name"
|
||||
case targetCommitish = "target_commitish"
|
||||
case name
|
||||
case draft
|
||||
case prerelease
|
||||
case createdAt = "created_at"
|
||||
case publishedAt = "published_at"
|
||||
case assets
|
||||
case tarballUrl = "tarball_url"
|
||||
case zipballUrl = "zipball_url"
|
||||
case body
|
||||
}
|
||||
|
||||
struct Author: Codable {
|
||||
let login: String
|
||||
let id: Int
|
||||
let nodeId: String
|
||||
let avatarUrl: String
|
||||
let gravatarId: String
|
||||
let url: String
|
||||
let htmlUrl: String
|
||||
let followersUrl: String
|
||||
let followingUrl: String
|
||||
let gistsUrl: String
|
||||
let starredUrl: String
|
||||
let subscriptionsUrl: String
|
||||
let organizationsUrl: String
|
||||
let reposUrl: String
|
||||
let eventsUrl: String
|
||||
let receivedEventsUrl: String
|
||||
let type: String
|
||||
let siteAdmin: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case login
|
||||
case id
|
||||
case nodeId = "node_id"
|
||||
case avatarUrl = "avatar_url"
|
||||
case gravatarId = "gravatar_id"
|
||||
case url
|
||||
case htmlUrl = "html_url"
|
||||
case followersUrl = "followers_url"
|
||||
case followingUrl = "following_url"
|
||||
case gistsUrl = "gists_url"
|
||||
case starredUrl = "starred_url"
|
||||
case subscriptionsUrl = "subscriptions_url"
|
||||
case organizationsUrl = "organizations_url"
|
||||
case reposUrl = "repos_url"
|
||||
case eventsUrl = "events_url"
|
||||
case receivedEventsUrl = "received_events_url"
|
||||
case type
|
||||
case siteAdmin = "site_admin"
|
||||
}
|
||||
}
|
||||
|
||||
struct Asset: Codable {
|
||||
let url: String
|
||||
let id: Int
|
||||
let nodeId: String
|
||||
let name: String
|
||||
let label: String?
|
||||
let uploader: Author
|
||||
let contentType: String
|
||||
let state: String
|
||||
let size: Int
|
||||
let downloadCount: Int
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
let browserDownloadUrl: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case id
|
||||
case nodeId = "node_id"
|
||||
case name
|
||||
case label
|
||||
case uploader
|
||||
case contentType = "content_type"
|
||||
case state
|
||||
case size
|
||||
case downloadCount = "download_count"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case browserDownloadUrl = "browser_download_url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
//
|
||||
// HistoryManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class HistoryManager: ObservableObject {
|
||||
@Published var searchHistory: [String] = UserDefaults.standard.stringArray(forKey: "SearchHistory") ?? []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
|
||||
.sink { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
self?.searchHistory = UserDefaults.standard.stringArray(forKey: "SearchHistory") ?? []
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func addSearchHistory(_ item: String) {
|
||||
if !searchHistory.contains(item) {
|
||||
searchHistory.insert(item, at: 0)
|
||||
UserDefaults.standard.set(searchHistory, forKey: "SearchHistory")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteHistoryItem(at offsets: IndexSet) {
|
||||
searchHistory.remove(atOffsets: offsets)
|
||||
UserDefaults.standard.set(searchHistory, forKey: "SearchHistory")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// MiruDataStruct.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MiruDataStruct: Codable {
|
||||
var likes: [Like]
|
||||
|
||||
struct Like: Codable {
|
||||
let anilistID: Int
|
||||
var gogoSlug: String
|
||||
let title: String
|
||||
let cover: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case anilistID = "anilist_id"
|
||||
case gogoSlug = "gogo_slug"
|
||||
case title
|
||||
case cover
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
//
|
||||
// ModuleStruct.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ModuleStruct: Codable {
|
||||
let name: String
|
||||
let version: String
|
||||
let author: Author
|
||||
let iconURL: String
|
||||
let stream: String
|
||||
let language: String
|
||||
let extractor: String
|
||||
let module: [Module]
|
||||
|
||||
struct Author: Codable {
|
||||
let name: String
|
||||
let website: String
|
||||
}
|
||||
|
||||
struct Module: Codable, Hashable {
|
||||
let search: Search
|
||||
let featured: Featured
|
||||
let details: Details
|
||||
let episodes: Episodes
|
||||
|
||||
struct Search: Codable, Hashable {
|
||||
let url: String
|
||||
let parameter: String
|
||||
let documentSelector: String
|
||||
let title: String
|
||||
let image: Image
|
||||
let href: String
|
||||
|
||||
struct Image: Codable, Hashable {
|
||||
let url: String
|
||||
let attribute: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Featured: Codable, Hashable {
|
||||
let url: String
|
||||
let documentSelector: String
|
||||
let title: String
|
||||
let image: Image
|
||||
let href: String
|
||||
|
||||
struct Image: Codable, Hashable {
|
||||
let url: String
|
||||
let attribute: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Details: Codable, Hashable {
|
||||
let baseURL: String
|
||||
let aliases: Aliases
|
||||
let synopsis: String
|
||||
let airdate: String
|
||||
let stars: String
|
||||
|
||||
struct Aliases: Codable, Hashable {
|
||||
let selector: String
|
||||
let attribute: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Episodes: Codable, Hashable {
|
||||
let selector: String
|
||||
let order: String
|
||||
let pattern: String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
//
|
||||
// ModulesManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ModulesManager: ObservableObject {
|
||||
@Published var modules: [ModuleStruct] = []
|
||||
@Published var isLoading = true
|
||||
var moduleURLs: [String: String] = [:]
|
||||
private let modulesFileName = "modules.json"
|
||||
private let moduleURLsFileName = "moduleURLs.json"
|
||||
|
||||
init() {
|
||||
loadModules()
|
||||
}
|
||||
|
||||
func loadModules() {
|
||||
isLoading = true
|
||||
loadModuleURLs()
|
||||
loadModuleData()
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func addModule(from urlString: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion(.failure(ModuleError.invalidURL))
|
||||
return
|
||||
}
|
||||
let task = URLSession.custom.dataTask(with: url) { data, response, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(.failure(error ?? ModuleError.unknown))
|
||||
return
|
||||
}
|
||||
do {
|
||||
let module = try JSONDecoder().decode(ModuleStruct.self, from: data)
|
||||
DispatchQueue.main.async {
|
||||
if !self.modules.contains(where: { $0.name == module.name }) {
|
||||
self.modules.append(module)
|
||||
self.moduleURLs[module.name] = urlString
|
||||
self.saveModuleData()
|
||||
self.saveModuleURLs()
|
||||
NotificationCenter.default.post(name: .moduleAdded, object: nil)
|
||||
completion(.success(()))
|
||||
} else {
|
||||
completion(.failure(ModuleError.duplicateModule))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func deleteModule(named name: String) {
|
||||
if let index = modules.firstIndex(where: { $0.name == name }) {
|
||||
modules.remove(at: index)
|
||||
moduleURLs.removeValue(forKey: name)
|
||||
saveModuleData()
|
||||
saveModuleURLs()
|
||||
NotificationCenter.default.post(name: .moduleRemoved, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshModules() {
|
||||
for (name, urlString) in moduleURLs {
|
||||
guard let url = URL(string: urlString) else { continue }
|
||||
let task = URLSession.custom.dataTask(with: url) { data, response, error in
|
||||
guard let data = data, error == nil else { return }
|
||||
do {
|
||||
let updatedModule = try JSONDecoder().decode(ModuleStruct.self, from: data)
|
||||
DispatchQueue.main.async {
|
||||
if let index = self.modules.firstIndex(where: { $0.name == name }) {
|
||||
self.modules[index] = updatedModule
|
||||
self.saveModuleData()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed to decode module during refresh: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to decode module during refresh: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadModuleURLs() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(moduleURLsFileName)
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
moduleURLs = try JSONDecoder().decode([String: String].self, from: data)
|
||||
} catch {
|
||||
print("Failed to load module URLs: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to load module URLs: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadModuleData() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(modulesFileName)
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
modules = try JSONDecoder().decode([ModuleStruct].self, from: data)
|
||||
} catch {
|
||||
print("Failed to load modules: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to load modules: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func saveModuleData() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(modulesFileName)
|
||||
do {
|
||||
let data = try JSONEncoder().encode(modules)
|
||||
try data.write(to: fileURL)
|
||||
} catch {
|
||||
print("Failed to save modules: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to save modules: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func saveModuleURLs() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(moduleURLsFileName)
|
||||
do {
|
||||
let data = try JSONEncoder().encode(moduleURLs)
|
||||
try data.write(to: fileURL)
|
||||
} catch {
|
||||
print("Failed to save module URLs: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to save module URLs: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func getDocumentsDirectory() -> URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
|
||||
enum ModuleError: LocalizedError {
|
||||
case invalidURL
|
||||
case duplicateModule
|
||||
case unknown
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL:
|
||||
return "The provided URL is invalid."
|
||||
case .duplicateModule:
|
||||
return "This module already exists."
|
||||
case .unknown:
|
||||
return "An unknown error occurred."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
//
|
||||
// VideoPlayerView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVKit
|
||||
|
||||
class VideoPlayerViewController: UIViewController {
|
||||
let module: ModuleStruct
|
||||
|
||||
var player: AVPlayer?
|
||||
var playerViewController: AVPlayerViewController?
|
||||
var timeObserverToken: Any?
|
||||
var streamUrl: String?
|
||||
var fullUrl: String = ""
|
||||
|
||||
init(module: ModuleStruct) {
|
||||
self.module = module
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
guard let streamUrl = streamUrl, let url = URL(string: streamUrl) else {
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if streamUrl.contains("ascdn") {
|
||||
request.addValue("\(module.module[0].details.baseURL)", forHTTPHeaderField: "Referer")
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
||||
player = AVPlayer(playerItem: playerItem)
|
||||
playerViewController = NormalPlayer()
|
||||
playerViewController?.player = player
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
|
||||
if let playerViewController = playerViewController {
|
||||
playerViewController.view.frame = self.view.frame
|
||||
self.view.addSubview(playerViewController.view)
|
||||
self.addChild(playerViewController)
|
||||
playerViewController.didMove(toParent: self)
|
||||
}
|
||||
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
||||
if lastPlayedTime > 0 {
|
||||
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
||||
self.player?.seek(to: seekTime) { _ in
|
||||
self.player?.play()
|
||||
}
|
||||
} else {
|
||||
self.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
player?.play()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
player?.pause()
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player?.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
func addPeriodicTimeObserver(fullURL: String) {
|
||||
guard let player = self.player else { return }
|
||||
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||
guard let currentItem = player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = time.seconds
|
||||
let duration = currentItem.duration.seconds
|
||||
|
||||
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Sora/Utlis & Misc/Analytics/Analytics.swift
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//
|
||||
// Analytics.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Hamzo on 28.02.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct AnalyticsResponse: Codable {
|
||||
let status: String
|
||||
let message: String
|
||||
let event: String?
|
||||
let timestamp: String?
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class AnalyticsManager {
|
||||
static let shared = AnalyticsManager()
|
||||
private let analyticsURL = URL(string: "http://151.106.3.14:47474/analytics")!
|
||||
private let moduleManager = ModuleManager()
|
||||
|
||||
private init() {}
|
||||
|
||||
func sendEvent(event: String, additionalData: [String: Any] = [:]) {
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
if defaults.object(forKey: "analyticsEnabled") == nil {
|
||||
defaults.setValue(false, forKey: "analyticsEnabled")
|
||||
}
|
||||
|
||||
let analyticsEnabled = UserDefaults.standard.bool(forKey: "analyticsEnabled")
|
||||
|
||||
guard analyticsEnabled else {
|
||||
Logger.shared.log("Analytics is disabled, skipping event: \(event)", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
||||
guard let selectedModule = getSelectedModule() else {
|
||||
Logger.shared.log("No selected module found", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
||||
var safeAdditionalData = additionalData
|
||||
|
||||
if let errorValue = additionalData["error"] as? NSError {
|
||||
safeAdditionalData["error"] = errorValue.localizedDescription
|
||||
}
|
||||
|
||||
let analyticsData: [String: Any] = [
|
||||
"event": event,
|
||||
"device": getDeviceModel(),
|
||||
"app_version": getAppVersion(),
|
||||
"module_name": selectedModule.metadata.sourceName,
|
||||
"module_version": selectedModule.metadata.version,
|
||||
"data": safeAdditionalData
|
||||
]
|
||||
|
||||
sendRequest(with: analyticsData)
|
||||
}
|
||||
|
||||
private func sendRequest(with data: [String: Any]) {
|
||||
var request = URLRequest(url: analyticsURL)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: data, options: [])
|
||||
} catch {
|
||||
Logger.shared.log("Failed to encode JSON: \(error.localizedDescription)", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: request) { (data, _, error) in
|
||||
if let error = error {
|
||||
Logger.shared.log("Request failed: \(error.localizedDescription)", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
Logger.shared.log("No data received from server", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let decodedResponse = try JSONDecoder().decode(AnalyticsResponse.self, from: data)
|
||||
if decodedResponse.status == "success" {
|
||||
Logger.shared.log("Analytics saved: \(decodedResponse.event ?? "unknown event") at \(decodedResponse.timestamp ?? "unknown time")", type: "Debug")
|
||||
} else {
|
||||
Logger.shared.log("Server error: \(decodedResponse.message)", type: "Debug")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Failed to decode response: \(error.localizedDescription)", type: "Debug")
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
private func getAppVersion() -> String {
|
||||
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown_version"
|
||||
}
|
||||
|
||||
private func getDeviceModel() -> String {
|
||||
return UIDevice.modelName
|
||||
}
|
||||
|
||||
private func getSelectedModule() -> ScrapingModule? {
|
||||
guard let selectedModuleId = UserDefaults.standard.string(forKey: "selectedModuleId") else { return nil }
|
||||
return moduleManager.modules.first { $0.id.uuidString == selectedModuleId }
|
||||
}
|
||||
}
|
||||
100
Sora/Utlis & Misc/DownloadUtils/DownloadManager.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// DownloadManager.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 29/04/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import AVFoundation
|
||||
|
||||
class DownloadManager: NSObject, ObservableObject {
|
||||
@Published var activeDownloads: [(URL, Double)] = []
|
||||
@Published var localPlaybackURL: URL?
|
||||
|
||||
private var assetDownloadURLSession: AVAssetDownloadURLSession!
|
||||
private var activeDownloadTasks: [URLSessionTask: URL] = [:]
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
initializeDownloadSession()
|
||||
loadLocalContent()
|
||||
}
|
||||
|
||||
private func initializeDownloadSession() {
|
||||
#if targetEnvironment(simulator)
|
||||
Logger.shared.log("Download Sessions are not available on Simulator", type: "Error")
|
||||
#else
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: "hls-downloader")
|
||||
|
||||
assetDownloadURLSession = AVAssetDownloadURLSession(
|
||||
configuration: configuration,
|
||||
assetDownloadDelegate: self,
|
||||
delegateQueue: .main
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
func downloadAsset(from url: URL) {
|
||||
let asset = AVURLAsset(url: url)
|
||||
let task = assetDownloadURLSession.makeAssetDownloadTask(
|
||||
asset: asset,
|
||||
assetTitle: "Offline Video",
|
||||
assetArtworkData: nil,
|
||||
options: nil
|
||||
)
|
||||
|
||||
task?.resume()
|
||||
activeDownloadTasks[task!] = url
|
||||
}
|
||||
|
||||
private func loadLocalContent() {
|
||||
guard let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
do {
|
||||
let contents = try FileManager.default.contentsOfDirectory(
|
||||
at: documents,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: .skipsHiddenFiles
|
||||
)
|
||||
|
||||
if let localURL = contents.first(where: { ["movpkg", "mp4"].contains($0.pathExtension.lowercased()) }) {
|
||||
localPlaybackURL = localURL
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Could not load local content: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadManager: AVAssetDownloadDelegate {
|
||||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
activeDownloadTasks.removeValue(forKey: assetDownloadTask)
|
||||
localPlaybackURL = location
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let error = error else { return }
|
||||
Logger.shared.log("Download failed: \(error.localizedDescription)", type: "Error")
|
||||
activeDownloadTasks.removeValue(forKey: task)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession,
|
||||
assetDownloadTask: AVAssetDownloadTask,
|
||||
didLoad timeRange: CMTimeRange,
|
||||
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
|
||||
timeRangeExpectedToLoad: CMTimeRange) {
|
||||
|
||||
guard let url = activeDownloadTasks[assetDownloadTask] else { return }
|
||||
let progress = loadedTimeRanges
|
||||
.map { $0.timeRangeValue.duration.seconds / timeRangeExpectedToLoad.duration.seconds }
|
||||
.reduce(0, +)
|
||||
|
||||
if let index = activeDownloads.firstIndex(where: { $0.0 == url }) {
|
||||
activeDownloads[index].1 = progress
|
||||
} else {
|
||||
activeDownloads.append((url, progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
648
Sora/Utlis & Misc/DownloadUtils/DownloadModels.swift
Normal file
|
|
@ -0,0 +1,648 @@
|
|||
//
|
||||
// DownloadModels.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 30/04/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Quality Preference Constants
|
||||
enum DownloadQualityPreference: String, CaseIterable {
|
||||
case best = "Best"
|
||||
case high = "High"
|
||||
case medium = "Medium"
|
||||
case low = "Low"
|
||||
|
||||
static var defaultPreference: DownloadQualityPreference {
|
||||
return .best
|
||||
}
|
||||
|
||||
static var userDefaultsKey: String {
|
||||
return "downloadQuality"
|
||||
}
|
||||
|
||||
/// Returns the current user preference for download quality
|
||||
static var current: DownloadQualityPreference {
|
||||
let storedValue = UserDefaults.standard.string(forKey: userDefaultsKey) ?? defaultPreference.rawValue
|
||||
return DownloadQualityPreference(rawValue: storedValue) ?? defaultPreference
|
||||
}
|
||||
|
||||
/// Description of what each quality preference means
|
||||
var description: String {
|
||||
switch self {
|
||||
case .best:
|
||||
return "Maximum quality available (largest file size)"
|
||||
case .high:
|
||||
return "High quality (720p or better)"
|
||||
case .medium:
|
||||
return "Medium quality (480p to 720p)"
|
||||
case .low:
|
||||
return "Minimum quality available (smallest file size)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Download Types
|
||||
enum DownloadType: String, Codable {
|
||||
case movie
|
||||
case episode
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .movie:
|
||||
return "Movie"
|
||||
case .episode:
|
||||
return "Episode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Downloaded Asset Model
|
||||
struct DownloadedAsset: Identifiable, Codable, Equatable {
|
||||
let id: UUID
|
||||
var name: String
|
||||
let downloadDate: Date
|
||||
let originalURL: URL
|
||||
let localURL: URL
|
||||
let type: DownloadType
|
||||
let metadata: AssetMetadata?
|
||||
// New fields for subtitle support
|
||||
let subtitleURL: URL?
|
||||
let localSubtitleURL: URL?
|
||||
|
||||
// For caching purposes, but not stored as part of the codable object
|
||||
private var _cachedFileSize: Int64? = nil
|
||||
|
||||
// Implement Equatable
|
||||
static func == (lhs: DownloadedAsset, rhs: DownloadedAsset) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
/// Returns the combined file size of the video file and subtitle file (if exists)
|
||||
var fileSize: Int64 {
|
||||
// This implementation calculates file size without caching it in the struct property
|
||||
// Instead we'll use a static cache dictionary
|
||||
let subtitlePathString = localSubtitleURL?.path ?? ""
|
||||
let cacheKey = localURL.path + ":" + subtitlePathString
|
||||
|
||||
// Check the static cache first
|
||||
if let size = DownloadedAsset.fileSizeCache[cacheKey] {
|
||||
return size
|
||||
}
|
||||
|
||||
// Check if this asset is currently being downloaded (avoid expensive calculations during active downloads)
|
||||
if isCurrentlyBeingDownloaded() {
|
||||
// Return cached size if available, otherwise return 0 and schedule background calculation
|
||||
if let lastKnownSize = DownloadedAsset.lastKnownSizes[cacheKey] {
|
||||
// Schedule a background update for when download completes
|
||||
scheduleBackgroundSizeCalculation(cacheKey: cacheKey)
|
||||
return lastKnownSize
|
||||
} else {
|
||||
// Return 0 for actively downloading files that we haven't calculated yet
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// For non-active downloads, calculate the size normally
|
||||
let calculatedSize = calculateFileSizeInternal()
|
||||
|
||||
// Store in both caches
|
||||
DownloadedAsset.fileSizeCache[cacheKey] = calculatedSize
|
||||
DownloadedAsset.lastKnownSizes[cacheKey] = calculatedSize
|
||||
|
||||
return calculatedSize
|
||||
}
|
||||
|
||||
/// Check if this asset is currently being downloaded
|
||||
public func isCurrentlyBeingDownloaded() -> Bool {
|
||||
// Access JSController to check active downloads
|
||||
let activeDownloads = JSController.shared.activeDownloads
|
||||
|
||||
// Check if any active download matches this asset's path
|
||||
for download in activeDownloads {
|
||||
// Compare based on the file name or title
|
||||
if let downloadTitle = download.title, downloadTitle == name {
|
||||
return true
|
||||
}
|
||||
|
||||
// Also compare based on URL path if titles don't match
|
||||
if download.originalURL.lastPathComponent.contains(name) ||
|
||||
name.contains(download.originalURL.lastPathComponent) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/// Schedule a background calculation for when the download completes
|
||||
private func scheduleBackgroundSizeCalculation(cacheKey: String) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
// Check if download is still active before calculating
|
||||
if !self.isCurrentlyBeingDownloaded() {
|
||||
let size = self.calculateFileSizeInternal()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Update caches on main thread
|
||||
DownloadedAsset.fileSizeCache[cacheKey] = size
|
||||
DownloadedAsset.lastKnownSizes[cacheKey] = size
|
||||
|
||||
// Post a notification that file size has been updated
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("fileSizeUpdated"),
|
||||
object: nil,
|
||||
userInfo: ["assetId": self.id.uuidString]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal method to calculate file size (separated for reuse)
|
||||
public func calculateFileSizeInternal() -> Int64 {
|
||||
var totalSize: Int64 = 0
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Get video file or directory size
|
||||
if fileManager.fileExists(atPath: localURL.path) {
|
||||
// Check if it's a .movpkg directory or a regular file
|
||||
var isDirectory: ObjCBool = false
|
||||
fileManager.fileExists(atPath: localURL.path, isDirectory: &isDirectory)
|
||||
|
||||
if isDirectory.boolValue {
|
||||
// If it's a directory (like .movpkg), calculate size of all contained files
|
||||
totalSize += calculateDirectorySize(localURL)
|
||||
Logger.shared.log("Calculated directory size for .movpkg: \(totalSize) bytes", type: "Info")
|
||||
} else {
|
||||
// If it's a single file, get its size
|
||||
do {
|
||||
let attributes = try fileManager.attributesOfItem(atPath: localURL.path)
|
||||
if let size = attributes[.size] as? Int64 {
|
||||
totalSize += size
|
||||
} else if let size = attributes[.size] as? Int {
|
||||
totalSize += Int64(size)
|
||||
} else if let size = attributes[.size] as? NSNumber {
|
||||
totalSize += size.int64Value
|
||||
} else {
|
||||
Logger.shared.log("Could not get file size as Int64 for: \(localURL.path)", type: "Warning")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error getting file size: \(error.localizedDescription) for \(localURL.path)", type: "Error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Video file does not exist at path: \(localURL.path)", type: "Warning")
|
||||
}
|
||||
|
||||
// Add subtitle file size if it exists
|
||||
if let subtitlePath = localSubtitleURL?.path, fileManager.fileExists(atPath: subtitlePath) {
|
||||
do {
|
||||
let attributes = try fileManager.attributesOfItem(atPath: subtitlePath)
|
||||
if let size = attributes[.size] as? Int64 {
|
||||
totalSize += size
|
||||
} else if let size = attributes[.size] as? Int {
|
||||
totalSize += Int64(size)
|
||||
} else if let size = attributes[.size] as? NSNumber {
|
||||
totalSize += size.int64Value
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error getting subtitle file size: \(error.localizedDescription)", type: "Warning")
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
/// Calculates the size of all files in a directory recursively
|
||||
private func calculateDirectorySize(_ directoryURL: URL) -> Int64 {
|
||||
let fileManager = FileManager.default
|
||||
var totalSize: Int64 = 0
|
||||
|
||||
do {
|
||||
// Get all content URLs
|
||||
let contents = try fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [])
|
||||
|
||||
// Calculate size for each item
|
||||
for url in contents {
|
||||
let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
|
||||
|
||||
if let isDirectory = resourceValues.isDirectory, isDirectory {
|
||||
// If it's a directory, recursively calculate its size
|
||||
totalSize += calculateDirectorySize(url)
|
||||
} else {
|
||||
// If it's a file, add its size
|
||||
if let fileSize = resourceValues.fileSize {
|
||||
totalSize += Int64(fileSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error calculating directory size: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
/// Global file size cache for performance
|
||||
private static var fileSizeCache: [String: Int64] = [:]
|
||||
|
||||
/// Global last known sizes cache for performance
|
||||
private static var lastKnownSizes: [String: Int64] = [:]
|
||||
|
||||
/// Clears the global file size cache
|
||||
static func clearFileSizeCache() {
|
||||
fileSizeCache.removeAll()
|
||||
lastKnownSizes.removeAll()
|
||||
}
|
||||
|
||||
/// Returns true if the main video file exists
|
||||
var fileExists: Bool {
|
||||
return FileManager.default.fileExists(atPath: localURL.path)
|
||||
}
|
||||
|
||||
// MARK: - New Grouping Properties
|
||||
|
||||
/// Returns the anime title to use for grouping (show title for episodes, name for movies)
|
||||
var groupTitle: String {
|
||||
if type == .episode, let showTitle = metadata?.showTitle, !showTitle.isEmpty {
|
||||
return showTitle
|
||||
}
|
||||
// For movies or episodes without show title, use the asset name
|
||||
return name
|
||||
}
|
||||
|
||||
/// Returns a display name suitable for showing in a list of episodes
|
||||
var episodeDisplayName: String {
|
||||
guard type == .episode else { return name }
|
||||
|
||||
// Return the name directly since titles typically already contain episode information
|
||||
return name
|
||||
}
|
||||
|
||||
/// Returns order priority for episodes within a show (by season and episode)
|
||||
var episodeOrderPriority: Int {
|
||||
guard type == .episode else { return 0 }
|
||||
|
||||
// Calculate priority: Season number * 1000 + episode number
|
||||
let seasonValue = metadata?.season ?? 0
|
||||
let episodeValue = metadata?.episode ?? 0
|
||||
|
||||
return (seasonValue * 1000) + episodeValue
|
||||
}
|
||||
|
||||
// Add coding keys to ensure backward compatibility
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, downloadDate, originalURL, localURL, type, metadata
|
||||
case subtitleURL, localSubtitleURL
|
||||
}
|
||||
|
||||
// Custom decoding to handle optional new fields
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Decode required fields
|
||||
id = try container.decode(UUID.self, forKey: .id)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
downloadDate = try container.decode(Date.self, forKey: .downloadDate)
|
||||
originalURL = try container.decode(URL.self, forKey: .originalURL)
|
||||
localURL = try container.decode(URL.self, forKey: .localURL)
|
||||
type = try container.decode(DownloadType.self, forKey: .type)
|
||||
metadata = try container.decodeIfPresent(AssetMetadata.self, forKey: .metadata)
|
||||
|
||||
// Decode new optional fields
|
||||
subtitleURL = try container.decodeIfPresent(URL.self, forKey: .subtitleURL)
|
||||
localSubtitleURL = try container.decodeIfPresent(URL.self, forKey: .localSubtitleURL)
|
||||
|
||||
// Initialize cache
|
||||
_cachedFileSize = nil
|
||||
}
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
name: String,
|
||||
downloadDate: Date,
|
||||
originalURL: URL,
|
||||
localURL: URL,
|
||||
type: DownloadType = .movie,
|
||||
metadata: AssetMetadata? = nil,
|
||||
subtitleURL: URL? = nil,
|
||||
localSubtitleURL: URL? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.downloadDate = downloadDate
|
||||
self.originalURL = originalURL
|
||||
self.localURL = localURL
|
||||
self.type = type
|
||||
self.metadata = metadata
|
||||
self.subtitleURL = subtitleURL
|
||||
self.localSubtitleURL = localSubtitleURL
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Active Download Model
|
||||
struct ActiveDownload: Identifiable, Equatable {
|
||||
let id: UUID
|
||||
let originalURL: URL
|
||||
var progress: Double
|
||||
let task: URLSessionTask
|
||||
let type: DownloadType
|
||||
let metadata: AssetMetadata?
|
||||
|
||||
// Implement Equatable
|
||||
static func == (lhs: ActiveDownload, rhs: ActiveDownload) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
// Add the same grouping properties as DownloadedAsset for consistency
|
||||
var groupTitle: String {
|
||||
if type == .episode,
|
||||
let showTitle = metadata?.showTitle,
|
||||
!showTitle.isEmpty {
|
||||
return showTitle
|
||||
}
|
||||
return metadata?.title ?? originalURL.lastPathComponent
|
||||
}
|
||||
|
||||
var episodeDisplayName: String {
|
||||
guard type == .episode else {
|
||||
return metadata?.title ?? originalURL.lastPathComponent
|
||||
}
|
||||
|
||||
// Extract base episode number from metadata or default to 1
|
||||
let episodeNumber = metadata?.episode ?? 1
|
||||
let base = "Episode \(episodeNumber)"
|
||||
|
||||
// Check if we have a valid title that's different from the base
|
||||
if let title = metadata?.title, !title.isEmpty, title != base {
|
||||
return "\(base): \(title)"
|
||||
} else {
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
originalURL: URL,
|
||||
progress: Double = 0,
|
||||
task: URLSessionTask,
|
||||
type: DownloadType = .movie,
|
||||
metadata: AssetMetadata? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.originalURL = originalURL
|
||||
self.progress = progress
|
||||
self.task = task
|
||||
self.type = type
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Asset Metadata
|
||||
struct AssetMetadata: Codable {
|
||||
let title: String
|
||||
let overview: String?
|
||||
let posterURL: URL?
|
||||
let backdropURL: URL?
|
||||
let releaseDate: String?
|
||||
// Additional fields for episodes
|
||||
let showTitle: String?
|
||||
let season: Int?
|
||||
let episode: Int?
|
||||
let showPosterURL: URL? // Main show poster URL (distinct from episode-specific images)
|
||||
|
||||
init(
|
||||
title: String,
|
||||
overview: String? = nil,
|
||||
posterURL: URL? = nil,
|
||||
backdropURL: URL? = nil,
|
||||
releaseDate: String? = nil,
|
||||
showTitle: String? = nil,
|
||||
season: Int? = nil,
|
||||
episode: Int? = nil,
|
||||
showPosterURL: URL? = nil
|
||||
) {
|
||||
self.title = title
|
||||
self.overview = overview
|
||||
self.posterURL = posterURL
|
||||
self.backdropURL = backdropURL
|
||||
self.releaseDate = releaseDate
|
||||
self.showTitle = showTitle
|
||||
self.season = season
|
||||
self.episode = episode
|
||||
self.showPosterURL = showPosterURL
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - New Group Model
|
||||
/// Represents a group of downloads (anime/show or movies)
|
||||
struct DownloadGroup: Identifiable {
|
||||
var id = UUID()
|
||||
let title: String // Anime title for shows
|
||||
let type: DownloadType
|
||||
var assets: [DownloadedAsset]
|
||||
var posterURL: URL?
|
||||
|
||||
// Cache key for this group
|
||||
private var cacheKey: String {
|
||||
return "\(id)-\(title)-\(assets.count)"
|
||||
}
|
||||
|
||||
// Static file size cache
|
||||
private static var fileSizeCache: [String: Int64] = [:]
|
||||
|
||||
// Static last known group sizes cache for performance during active downloads
|
||||
private static var lastKnownGroupSizes: [String: Int64] = [:]
|
||||
|
||||
var assetCount: Int {
|
||||
return assets.count
|
||||
}
|
||||
|
||||
var isShow: Bool {
|
||||
return type == .episode
|
||||
}
|
||||
|
||||
var isAnime: Bool {
|
||||
return isShow
|
||||
}
|
||||
|
||||
/// Returns the total file size of all assets in the group
|
||||
var totalFileSize: Int64 {
|
||||
// Check if we have a cached size for this group
|
||||
let key = cacheKey
|
||||
if let cachedSize = DownloadGroup.fileSizeCache[key] {
|
||||
return cachedSize
|
||||
}
|
||||
|
||||
// Check if any assets in this group are currently being downloaded
|
||||
let hasActiveDownloads = assets.contains { asset in
|
||||
return asset.isCurrentlyBeingDownloaded()
|
||||
}
|
||||
|
||||
if hasActiveDownloads {
|
||||
// If any downloads are active, return last known size or schedule background calculation
|
||||
if let lastKnownSize = DownloadGroup.lastKnownGroupSizes[key] {
|
||||
// Schedule a background update for when downloads complete
|
||||
scheduleBackgroundGroupSizeCalculation(cacheKey: key)
|
||||
return lastKnownSize
|
||||
} else {
|
||||
// Return 0 for groups with active downloads that we haven't calculated yet
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// For groups without active downloads, calculate the size normally
|
||||
let total = assets.reduce(0) { runningTotal, asset in
|
||||
return runningTotal + asset.fileSize
|
||||
}
|
||||
|
||||
// Store in both caches
|
||||
DownloadGroup.fileSizeCache[key] = total
|
||||
DownloadGroup.lastKnownGroupSizes[key] = total
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
/// Schedule a background calculation for when downloads complete
|
||||
private func scheduleBackgroundGroupSizeCalculation(cacheKey: String) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
// Check if any assets are still being downloaded
|
||||
let stillHasActiveDownloads = self.assets.contains { asset in
|
||||
return asset.isCurrentlyBeingDownloaded()
|
||||
}
|
||||
|
||||
if !stillHasActiveDownloads {
|
||||
// Calculate total size
|
||||
let total = self.assets.reduce(0) { runningTotal, asset in
|
||||
return runningTotal + asset.calculateFileSizeInternal()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Update caches on main thread
|
||||
DownloadGroup.fileSizeCache[cacheKey] = total
|
||||
DownloadGroup.lastKnownGroupSizes[cacheKey] = total
|
||||
|
||||
// Post a notification that group size has been updated
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("groupSizeUpdated"),
|
||||
object: nil,
|
||||
userInfo: ["groupId": self.id.uuidString]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the count of assets that actually exist on disk
|
||||
var existingAssetsCount: Int {
|
||||
return assets.filter { $0.fileExists }.count
|
||||
}
|
||||
|
||||
/// Returns true if all assets in this group exist
|
||||
var allAssetsExist: Bool {
|
||||
return existingAssetsCount == assets.count
|
||||
}
|
||||
|
||||
/// Clear the file size cache for all groups
|
||||
static func clearFileSizeCache() {
|
||||
fileSizeCache.removeAll()
|
||||
lastKnownGroupSizes.removeAll()
|
||||
}
|
||||
|
||||
// For anime/TV shows, organize episodes by season then episode number
|
||||
func organizedEpisodes() -> [DownloadedAsset] {
|
||||
guard isShow else { return assets }
|
||||
return assets.sorted { $0.episodeOrderPriority < $1.episodeOrderPriority }
|
||||
}
|
||||
|
||||
/// Refresh the calculated size for this group
|
||||
mutating func refreshFileSize() {
|
||||
DownloadGroup.fileSizeCache.removeValue(forKey: cacheKey)
|
||||
_ = totalFileSize
|
||||
}
|
||||
|
||||
init(title: String, type: DownloadType, assets: [DownloadedAsset], posterURL: URL? = nil) {
|
||||
self.title = title
|
||||
self.type = type
|
||||
self.assets = assets
|
||||
self.posterURL = posterURL
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Grouping Extensions
|
||||
extension Array where Element == DownloadedAsset {
|
||||
/// Groups assets by anime title or movie
|
||||
func groupedByTitle() -> [DownloadGroup] {
|
||||
// First group by the anime title (show title for episodes, name for movies)
|
||||
let groupedDict = Dictionary(grouping: self) { asset in
|
||||
// For episodes, prioritize the showTitle from metadata
|
||||
if asset.type == .episode, let showTitle = asset.metadata?.showTitle, !showTitle.isEmpty {
|
||||
return showTitle
|
||||
}
|
||||
|
||||
// For movies or episodes without proper metadata, use the asset name
|
||||
return asset.name
|
||||
}
|
||||
|
||||
// Convert to array of DownloadGroup objects
|
||||
return groupedDict.map { (title, assets) in
|
||||
// Determine group type (if any asset is an episode, it's a show)
|
||||
let isShow = assets.contains { $0.type == .episode }
|
||||
let type: DownloadType = isShow ? .episode : .movie
|
||||
|
||||
// Find poster URL - prioritize show-level posters over episode-specific ones
|
||||
let posterURL: URL? = {
|
||||
// First priority: Use dedicated showPosterURL if available
|
||||
if let showPosterURL = assets.compactMap({ $0.metadata?.showPosterURL }).first {
|
||||
return showPosterURL
|
||||
}
|
||||
|
||||
// Second priority: For anime/TV shows, look for consistent poster URLs that appear across multiple episodes
|
||||
// These are more likely to be show posters rather than episode-specific images
|
||||
if isShow && assets.count > 1 {
|
||||
let posterURLs = assets.compactMap { $0.metadata?.posterURL }
|
||||
let urlCounts = Dictionary(grouping: posterURLs, by: { $0 })
|
||||
|
||||
// Find the most common poster URL (likely the show poster)
|
||||
if let mostCommonPoster = urlCounts.max(by: { $0.value.count < $1.value.count })?.key {
|
||||
return mostCommonPoster
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first available poster
|
||||
return assets.compactMap { $0.metadata?.posterURL }.first
|
||||
}()
|
||||
|
||||
return DownloadGroup(
|
||||
title: title,
|
||||
type: type,
|
||||
assets: assets,
|
||||
posterURL: posterURL
|
||||
)
|
||||
}.sorted { $0.title < $1.title }
|
||||
}
|
||||
|
||||
/// Sorts assets in a way suitable for flat list display
|
||||
func sortedForDisplay(by sortOption: DownloadView.SortOption) -> [DownloadedAsset] {
|
||||
switch sortOption {
|
||||
case .newest:
|
||||
return sorted { $0.downloadDate > $1.downloadDate }
|
||||
case .oldest:
|
||||
return sorted { $0.downloadDate < $1.downloadDate }
|
||||
case .title:
|
||||
return sorted { $0.name < $1.name }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Active Downloads Grouping
|
||||
extension Array where Element == ActiveDownload {
|
||||
/// Groups active downloads by show title
|
||||
func groupedByTitle() -> [String: [ActiveDownload]] {
|
||||
let grouped = Dictionary(grouping: self) { download in
|
||||
return download.groupTitle
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
}
|
||||
337
Sora/Utlis & Misc/DownloadUtils/M3U8StreamExtractor.swift
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
//
|
||||
// M3U8StreamExtractor.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 30/04/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum M3U8StreamExtractorError: Error {
|
||||
case networkError(Error)
|
||||
case parsingError(String)
|
||||
case noStreamFound
|
||||
case invalidURL
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .networkError(let error):
|
||||
return "Connection error: \(error.localizedDescription)"
|
||||
case .parsingError(let message):
|
||||
return "Stream parsing error: \(message)"
|
||||
case .noStreamFound:
|
||||
return "No compatible stream found in playlist"
|
||||
case .invalidURL:
|
||||
return "Stream URL is invalid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class M3U8StreamExtractor {
|
||||
|
||||
// Enable verbose logging for development/testing
|
||||
static var verboseLogging: Bool = true
|
||||
|
||||
/// Logs messages with a consistent format if verbose logging is enabled
|
||||
/// - Parameters:
|
||||
/// - message: The message to log
|
||||
/// - function: The calling function (auto-filled)
|
||||
/// - line: The line number (auto-filled)
|
||||
private static func log(_ message: String, function: String = #function, line: Int = #line) {
|
||||
if verboseLogging {
|
||||
print("[M3U8Extractor:\(function):\(line)] \(message)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the appropriate stream URL from a master M3U8 playlist based on quality preference
|
||||
/// - Parameters:
|
||||
/// - masterURL: The URL of the master M3U8 playlist
|
||||
/// - headers: HTTP headers to use for the request
|
||||
/// - preferredQuality: User's preferred quality ("Best", "High", "Medium", "Low")
|
||||
/// - jsController: Optional reference to the JSController for header management
|
||||
/// - completion: Completion handler with the result containing the selected stream URL and headers
|
||||
static func extractStreamURL(
|
||||
from masterURL: URL,
|
||||
headers: [String: String],
|
||||
preferredQuality: String,
|
||||
jsController: JSController? = nil,
|
||||
completion: @escaping (Result<(streamURL: URL, headers: [String: String]), Error>) -> Void
|
||||
) {
|
||||
log("Starting extraction from master playlist: \(masterURL.absoluteString)")
|
||||
log("Preferred quality: \(preferredQuality)")
|
||||
|
||||
var requestHeaders = headers
|
||||
|
||||
// Use header manager if available
|
||||
if let controller = jsController {
|
||||
log("Using JSController for header management")
|
||||
requestHeaders = controller.ensureStreamingHeaders(headers: headers, for: masterURL)
|
||||
controller.logHeadersForRequest(headers: requestHeaders, url: masterURL, operation: "Extracting streams from")
|
||||
} else {
|
||||
log("JSController not provided, using original headers")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: masterURL)
|
||||
|
||||
// Add headers to the request
|
||||
for (key, value) in requestHeaders {
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// Add a unique request ID for tracking in logs
|
||||
let requestID = UUID().uuidString.prefix(8)
|
||||
log("Request ID: \(requestID)")
|
||||
|
||||
// Fetch the master playlist
|
||||
log("Sending request to fetch master playlist")
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
// Handle network errors
|
||||
if let error = error {
|
||||
log("Network error: \(error.localizedDescription)")
|
||||
completion(.failure(M3U8StreamExtractorError.networkError(error)))
|
||||
return
|
||||
}
|
||||
|
||||
// Log HTTP status for debugging
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
let statusCode = httpResponse.statusCode
|
||||
log("HTTP Status: \(statusCode) for \(masterURL.absoluteString)")
|
||||
|
||||
if statusCode == 403 {
|
||||
log("HTTP Error 403: Access Forbidden")
|
||||
|
||||
// Try to extract domain from URL for logging
|
||||
let domain = masterURL.host ?? "unknown domain"
|
||||
log("Access denied by server: \(domain)")
|
||||
|
||||
// Check if we have essential headers that might be missing/incorrect
|
||||
let missingCriticalHeaders = ["Origin", "Referer", "User-Agent"].filter { requestHeaders[$0] == nil }
|
||||
if !missingCriticalHeaders.isEmpty {
|
||||
log("Missing critical headers: \(missingCriticalHeaders.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
// Since we got a 403, just fall back to the master URL directly
|
||||
log("403 error - Falling back to master URL")
|
||||
completion(.success((streamURL: masterURL, headers: requestHeaders)))
|
||||
return
|
||||
} else if statusCode >= 400 {
|
||||
log("HTTP Error: \(statusCode)")
|
||||
completion(.failure(M3U8StreamExtractorError.parsingError("HTTP Error: \(statusCode)")))
|
||||
return
|
||||
}
|
||||
|
||||
// Log response headers for debugging
|
||||
log("Response Headers:")
|
||||
for (key, value) in httpResponse.allHeaderFields {
|
||||
log(" \(key): \(value)")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have data
|
||||
guard let data = data else {
|
||||
log("No data received")
|
||||
completion(.failure(M3U8StreamExtractorError.parsingError("No data received")))
|
||||
return
|
||||
}
|
||||
|
||||
// Try to parse as string
|
||||
guard let content = String(data: data, encoding: .utf8) else {
|
||||
log("Failed to decode playlist content")
|
||||
completion(.failure(M3U8StreamExtractorError.parsingError("Failed to decode playlist content")))
|
||||
return
|
||||
}
|
||||
|
||||
// Log a sample of the content (first 200 chars)
|
||||
let contentPreview = String(content.prefix(200))
|
||||
log("Playlist Content (preview): \(contentPreview)...")
|
||||
|
||||
// Count the number of lines in the content
|
||||
let lineCount = content.components(separatedBy: .newlines).count
|
||||
log("Playlist has \(lineCount) lines")
|
||||
|
||||
// Parse the M3U8 content to extract available streams
|
||||
log("Parsing M3U8 content")
|
||||
let streams = parseM3U8Content(content: content, baseURL: masterURL)
|
||||
|
||||
// Log the extracted streams
|
||||
log("Extracted \(streams.count) streams from M3U8 playlist")
|
||||
for (index, stream) in streams.enumerated() {
|
||||
log("Stream #\(index + 1): \(stream.name), \(stream.resolution.width)x\(stream.resolution.height), URL: \(stream.url)")
|
||||
}
|
||||
|
||||
if streams.isEmpty {
|
||||
log("No streams found in playlist")
|
||||
}
|
||||
|
||||
// Select the appropriate stream based on quality preference
|
||||
log("Selecting stream with quality preference: \(preferredQuality)")
|
||||
if let selectedURL = selectStream(streams: streams, preferredQuality: preferredQuality),
|
||||
let url = URL(string: selectedURL) {
|
||||
|
||||
log("Selected stream URL: \(url.absoluteString)")
|
||||
|
||||
var finalHeaders = requestHeaders
|
||||
|
||||
// Use header manager to optimize headers for the selected stream if available
|
||||
if let controller = jsController {
|
||||
log("Optimizing headers for selected stream")
|
||||
finalHeaders = controller.ensureStreamingHeaders(headers: requestHeaders, for: url)
|
||||
controller.logHeadersForRequest(headers: finalHeaders, url: url, operation: "Selected stream")
|
||||
}
|
||||
|
||||
// Return the selected stream URL along with the headers
|
||||
log("Extraction successful")
|
||||
completion(.success((streamURL: url, headers: finalHeaders)))
|
||||
} else if !streams.isEmpty, let fallbackStream = streams.first, let url = URL(string: fallbackStream.url) {
|
||||
// Fallback to first stream if preferred quality not found
|
||||
log("Preferred quality '\(preferredQuality)' not found, falling back to: \(fallbackStream.name)")
|
||||
|
||||
var finalHeaders = requestHeaders
|
||||
|
||||
// Use header manager for fallback stream
|
||||
if let controller = jsController {
|
||||
log("Optimizing headers for fallback stream")
|
||||
finalHeaders = controller.ensureStreamingHeaders(headers: requestHeaders, for: url)
|
||||
controller.logHeadersForRequest(headers: finalHeaders, url: url, operation: "Fallback stream")
|
||||
}
|
||||
|
||||
log("Fallback extraction successful")
|
||||
completion(.success((streamURL: url, headers: finalHeaders)))
|
||||
} else if streams.isEmpty {
|
||||
// If the playlist doesn't contain any streams, use the master URL as fallback
|
||||
log("No streams found in the playlist, using master URL as fallback")
|
||||
log("Using master URL as fallback")
|
||||
completion(.success((streamURL: masterURL, headers: requestHeaders)))
|
||||
} else {
|
||||
log("No suitable stream found")
|
||||
completion(.failure(M3U8StreamExtractorError.noStreamFound))
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
log("Request started")
|
||||
}
|
||||
|
||||
/// Parses M3U8 content to extract available streams
|
||||
/// - Parameters:
|
||||
/// - content: The M3U8 playlist content as string
|
||||
/// - baseURL: The base URL of the playlist for resolving relative URLs
|
||||
/// - Returns: Array of extracted streams with name, URL, and resolution
|
||||
private static func parseM3U8Content(
|
||||
content: String,
|
||||
baseURL: URL
|
||||
) -> [(name: String, url: String, resolution: (width: Int, height: Int))] {
|
||||
let lines = content.components(separatedBy: .newlines)
|
||||
var streams: [(name: String, url: String, resolution: (width: Int, height: Int))] = []
|
||||
|
||||
for (index, line) in lines.enumerated() {
|
||||
// Look for the stream info tag
|
||||
if line.contains("#EXT-X-STREAM-INF"), index + 1 < lines.count {
|
||||
// Extract resolution information
|
||||
if let resolutionRange = line.range(of: "RESOLUTION="),
|
||||
let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",")
|
||||
?? line[resolutionRange.upperBound...].range(of: "\n") {
|
||||
|
||||
let resolutionPart = String(line[resolutionRange.upperBound..<resolutionEndRange.lowerBound])
|
||||
let dimensions = resolutionPart.components(separatedBy: "x")
|
||||
|
||||
if dimensions.count == 2,
|
||||
let width = Int(dimensions[0]),
|
||||
let height = Int(dimensions[1]) {
|
||||
|
||||
// Get the URL from the next line
|
||||
let nextLine = lines[index + 1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Generate a quality name
|
||||
let qualityName = getQualityName(for: height)
|
||||
|
||||
// Handle relative URLs
|
||||
var streamURL = nextLine
|
||||
if !nextLine.hasPrefix("http") && nextLine.contains(".m3u8") {
|
||||
let baseURLString = baseURL.deletingLastPathComponent().absoluteString
|
||||
streamURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString
|
||||
?? baseURLString + "/" + nextLine
|
||||
}
|
||||
|
||||
// Add the stream to our list
|
||||
streams.append((
|
||||
name: qualityName,
|
||||
url: streamURL,
|
||||
resolution: (width: width, height: height)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return streams
|
||||
}
|
||||
|
||||
/// Selects a stream based on the user's quality preference
|
||||
/// - Parameters:
|
||||
/// - streams: Array of available streams
|
||||
/// - preferredQuality: User's preferred quality
|
||||
/// - Returns: URL of the selected stream, or nil if no suitable stream was found
|
||||
private static func selectStream(
|
||||
streams: [(name: String, url: String, resolution: (width: Int, height: Int))],
|
||||
preferredQuality: String
|
||||
) -> String? {
|
||||
guard !streams.isEmpty else { return nil }
|
||||
|
||||
// Sort streams by resolution (height) in descending order
|
||||
let sortedStreams = streams.sorted { $0.resolution.height > $1.resolution.height }
|
||||
|
||||
switch preferredQuality {
|
||||
case "Best":
|
||||
// Return the highest quality stream
|
||||
return sortedStreams.first?.url
|
||||
|
||||
case "High":
|
||||
// Return a high quality stream (720p or higher, but not the highest)
|
||||
let highStreams = sortedStreams.filter { $0.resolution.height >= 720 }
|
||||
if highStreams.count > 1 {
|
||||
return highStreams[1].url // Second highest if available
|
||||
} else if !highStreams.isEmpty {
|
||||
return highStreams[0].url // Highest if only one high quality stream
|
||||
} else if !sortedStreams.isEmpty {
|
||||
return sortedStreams.first?.url // Fallback to highest available
|
||||
}
|
||||
|
||||
case "Medium":
|
||||
// Return a medium quality stream (between 480p and 720p)
|
||||
let mediumStreams = sortedStreams.filter {
|
||||
$0.resolution.height >= 480 && $0.resolution.height < 720
|
||||
}
|
||||
if !mediumStreams.isEmpty {
|
||||
return mediumStreams.first?.url
|
||||
} else if sortedStreams.count > 1 {
|
||||
let medianIndex = sortedStreams.count / 2
|
||||
return sortedStreams[medianIndex].url // Return median quality as fallback
|
||||
} else if !sortedStreams.isEmpty {
|
||||
return sortedStreams.first?.url // Fallback to highest available
|
||||
}
|
||||
|
||||
case "Low":
|
||||
// Return the lowest quality stream
|
||||
return sortedStreams.last?.url
|
||||
|
||||
default:
|
||||
// Default to best quality
|
||||
return sortedStreams.first?.url
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Generates a quality name based on resolution height
|
||||
/// - Parameter height: The vertical resolution (height) of the stream
|
||||
/// - Returns: A human-readable quality name
|
||||
private static func getQualityName(for height: Int) -> String {
|
||||
switch height {
|
||||
case 1080...: return "\(height)p (FHD)"
|
||||
case 720..<1080: return "\(height)p (HD)"
|
||||
case 480..<720: return "\(height)p (SD)"
|
||||
default: return "\(height)p"
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Sora/Utlis & Misc/Drops/DropManager.swift
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// DropManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 25/01/25.
|
||||
//
|
||||
|
||||
import Drops
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class DropManager {
|
||||
static let shared = DropManager()
|
||||
|
||||
private var notificationQueue: [(title: String, subtitle: String, duration: TimeInterval, icon: UIImage?)] = []
|
||||
private var isProcessingQueue = false
|
||||
|
||||
private init() {}
|
||||
|
||||
func showDrop(title: String, subtitle: String, duration: TimeInterval, icon: UIImage?) {
|
||||
notificationQueue.append((title: title, subtitle: subtitle, duration: duration, icon: icon))
|
||||
|
||||
if !isProcessingQueue {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private func processQueue() {
|
||||
guard !notificationQueue.isEmpty else {
|
||||
isProcessingQueue = false
|
||||
return
|
||||
}
|
||||
|
||||
isProcessingQueue = true
|
||||
let notification = notificationQueue.removeFirst()
|
||||
|
||||
let drop = Drop(
|
||||
title: notification.title,
|
||||
subtitle: notification.subtitle,
|
||||
icon: notification.icon,
|
||||
position: .top,
|
||||
duration: .seconds(notification.duration)
|
||||
)
|
||||
|
||||
Drops.show(drop)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + notification.duration) { [weak self] in
|
||||
self?.processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
func success(_ message: String, duration: TimeInterval = 2.0) {
|
||||
let icon = UIImage(systemName: "checkmark.circle.fill")?.withTintColor(.green, renderingMode: .alwaysOriginal)
|
||||
showDrop(title: "Success", subtitle: message, duration: duration, icon: icon)
|
||||
}
|
||||
|
||||
func error(_ message: String, duration: TimeInterval = 2.0) {
|
||||
let icon = UIImage(systemName: "xmark.circle.fill")?.withTintColor(.red, renderingMode: .alwaysOriginal)
|
||||
showDrop(title: "Error", subtitle: message, duration: duration, icon: icon)
|
||||
}
|
||||
|
||||
func info(_ message: String, duration: TimeInterval = 2.0) {
|
||||
let accentColor = UIColor(Color.accentColor)
|
||||
let icon = UIImage(systemName: "info.circle.fill")?.withTintColor(accentColor, renderingMode: .alwaysOriginal)
|
||||
showDrop(title: "Info", subtitle: message, duration: duration, icon: icon)
|
||||
}
|
||||
|
||||
func downloadStarted(episodeNumber: Int) {
|
||||
let willStartImmediately = JSController.shared.willDownloadStartImmediately()
|
||||
|
||||
let message = willStartImmediately
|
||||
? "Episode \(episodeNumber) is now downloading"
|
||||
: "Episode \(episodeNumber) added to download queue"
|
||||
|
||||
showDrop(
|
||||
title: willStartImmediately ? "Download Started" : "Download Queued",
|
||||
subtitle: message,
|
||||
duration: 1.5,
|
||||
icon: UIImage(systemName: willStartImmediately ? "arrow.down.circle.fill" : "clock.arrow.circlepath")
|
||||
)
|
||||
}
|
||||
}
|
||||
343
Sora/Utlis & Misc/Extensions/JavaScriptCore+Extensions.swift
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
//
|
||||
// JSContext+Extensions.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Hamzo on 19/03/25.
|
||||
//
|
||||
|
||||
import SoraCore
|
||||
import JavaScriptCore
|
||||
|
||||
extension JSContext {
|
||||
func setupConsoleLogging() {
|
||||
let consoleObject = JSValue(newObjectIn: self)
|
||||
|
||||
let consoleLogFunction: @convention(block) (String) -> Void = { message in
|
||||
Logger.shared.log(message, type: "Debug")
|
||||
}
|
||||
consoleObject?.setObject(consoleLogFunction, forKeyedSubscript: "log" as NSString)
|
||||
|
||||
let consoleErrorFunction: @convention(block) (String) -> Void = { message in
|
||||
Logger.shared.log(message, type: "Error")
|
||||
}
|
||||
consoleObject?.setObject(consoleErrorFunction, forKeyedSubscript: "error" as NSString)
|
||||
|
||||
self.setObject(consoleObject, forKeyedSubscript: "console" as NSString)
|
||||
|
||||
let logFunction: @convention(block) (String) -> Void = { message in
|
||||
Logger.shared.log("JavaScript log: \(message)", type: "Debug")
|
||||
}
|
||||
self.setObject(logFunction, forKeyedSubscript: "log" as NSString)
|
||||
}
|
||||
|
||||
func setupNativeFetch() {
|
||||
let fetchNativeFunction: @convention(block) (String, [String: String]?, JSValue, JSValue) -> Void = { urlString, headers, resolve, reject in
|
||||
guard let url = URL(string: urlString) else {
|
||||
Logger.shared.log("Invalid URL", type: "Error")
|
||||
reject.call(withArguments: ["Invalid URL"])
|
||||
return
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
if let headers = headers {
|
||||
for (key, value) in headers {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
let task = URLSession.custom.dataTask(with: request) { data, _, error in
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)", type: "Error")
|
||||
reject.call(withArguments: [error.localizedDescription])
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
Logger.shared.log("No data in response", type: "Error")
|
||||
reject.call(withArguments: ["No data"])
|
||||
return
|
||||
}
|
||||
if let text = String(data: data, encoding: .utf8) {
|
||||
resolve.call(withArguments: [text])
|
||||
} else {
|
||||
Logger.shared.log("Unable to decode data to text", type: "Error")
|
||||
reject.call(withArguments: ["Unable to decode data"])
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
self.setObject(fetchNativeFunction, forKeyedSubscript: "fetchNative" as NSString)
|
||||
|
||||
let fetchDefinition = """
|
||||
function fetch(url, headers) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fetchNative(url, headers, resolve, reject);
|
||||
});
|
||||
}
|
||||
"""
|
||||
self.evaluateScript(fetchDefinition)
|
||||
}
|
||||
|
||||
func setupFetchV2() {
|
||||
let fetchV2NativeFunction: @convention(block) (String, Any?, String?, String?, ObjCBool, String?, JSValue, JSValue) -> Void = { urlString, headersAny, method, body, redirect, encoding, resolve, reject in
|
||||
guard let url = URL(string: urlString) else {
|
||||
Logger.shared.log("Invalid URL", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
reject.call(withArguments: ["Invalid URL"])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var headers: [String: String]? = nil
|
||||
|
||||
if let headersAny = headersAny {
|
||||
if headersAny is NSNull {
|
||||
headers = nil
|
||||
} else if let headersDict = headersAny as? [String: Any] {
|
||||
var safeHeaders: [String: String] = [:]
|
||||
for (key, value) in headersDict {
|
||||
let stringValue: String
|
||||
if let str = value as? String {
|
||||
stringValue = str
|
||||
} else if let num = value as? NSNumber {
|
||||
stringValue = num.stringValue
|
||||
} else if value is NSNull {
|
||||
continue
|
||||
} else {
|
||||
stringValue = String(describing: value)
|
||||
}
|
||||
safeHeaders[key] = stringValue
|
||||
}
|
||||
headers = safeHeaders.isEmpty ? nil : safeHeaders
|
||||
} else if let headersDict = headersAny as? [AnyHashable: Any] {
|
||||
var safeHeaders: [String: String] = [:]
|
||||
for (key, value) in headersDict {
|
||||
let stringKey = String(describing: key)
|
||||
|
||||
let stringValue: String
|
||||
if let str = value as? String {
|
||||
stringValue = str
|
||||
} else if let num = value as? NSNumber {
|
||||
stringValue = num.stringValue
|
||||
} else if value is NSNull {
|
||||
continue
|
||||
} else {
|
||||
stringValue = String(describing: value)
|
||||
}
|
||||
safeHeaders[stringKey] = stringValue
|
||||
}
|
||||
headers = safeHeaders.isEmpty ? nil : safeHeaders
|
||||
} else {
|
||||
Logger.shared.log("Headers argument is not a dictionary, type: \(type(of: headersAny))", type: "Warning")
|
||||
headers = nil
|
||||
}
|
||||
}
|
||||
|
||||
let httpMethod = method ?? "GET"
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = httpMethod
|
||||
|
||||
Logger.shared.log("FetchV2 Request: URL=\(url), Method=\(httpMethod), Body=\(body ?? "nil"), Encoding=\(encoding ?? "utf-8")", type: "Debug")
|
||||
|
||||
func getEncoding(from encodingString: String?) -> String.Encoding {
|
||||
guard let encodingString = encodingString?.lowercased() else {
|
||||
return .utf8
|
||||
}
|
||||
|
||||
switch encodingString {
|
||||
case "utf-8", "utf8":
|
||||
return .utf8
|
||||
case "windows-1251", "cp1251":
|
||||
return .windowsCP1251
|
||||
case "windows-1252", "cp1252":
|
||||
return .windowsCP1252
|
||||
case "iso-8859-1", "latin1":
|
||||
return .isoLatin1
|
||||
case "ascii":
|
||||
return .ascii
|
||||
case "utf-16", "utf16":
|
||||
return .utf16
|
||||
default:
|
||||
Logger.shared.log("Unknown encoding '\(encodingString)', defaulting to UTF-8", type: "Warning")
|
||||
return .utf8
|
||||
}
|
||||
}
|
||||
|
||||
let textEncoding = getEncoding(from: encoding)
|
||||
|
||||
let bodyIsEmpty = body == nil || (body)?.isEmpty == true || body == "null" || body == "undefined"
|
||||
|
||||
if httpMethod == "GET" && !bodyIsEmpty {
|
||||
Logger.shared.log("GET request must not have a body", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
reject.call(withArguments: ["GET request must not have a body"])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if httpMethod != "GET" && !bodyIsEmpty {
|
||||
if let bodyString = body {
|
||||
request.httpBody = bodyString.data(using: .utf8)
|
||||
} else {
|
||||
let bodyString = String(describing: body!)
|
||||
request.httpBody = bodyString.data(using: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
if let headers = headers {
|
||||
for (key, value) in headers {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.shared.log("Redirect value is \(redirect.boolValue)", type: "Debug")
|
||||
let session = URLSession.fetchData(allowRedirects: redirect.boolValue)
|
||||
|
||||
let task = session.downloadTask(with: request) { tempFileURL, response, error in
|
||||
defer { session.finishTasksAndInvalidate() }
|
||||
|
||||
let callReject: (String) -> Void = { message in
|
||||
DispatchQueue.main.async {
|
||||
reject.call(withArguments: [message])
|
||||
}
|
||||
}
|
||||
let callResolve: ([String: Any]) -> Void = { dict in
|
||||
DispatchQueue.main.async {
|
||||
resolve.call(withArguments: [dict])
|
||||
}
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: "Error")
|
||||
callReject(error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
guard let tempFileURL = tempFileURL else {
|
||||
Logger.shared.log("No data in response", type: "Error")
|
||||
callReject("No data")
|
||||
return
|
||||
}
|
||||
|
||||
var safeHeaders: [String: String] = [:]
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
for (key, value) in httpResponse.allHeaderFields {
|
||||
if let keyString = key as? String {
|
||||
let valueString: String
|
||||
if let str = value as? String {
|
||||
valueString = str
|
||||
} else {
|
||||
valueString = String(describing: value)
|
||||
}
|
||||
safeHeaders[keyString] = valueString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var responseDict: [String: Any] = [
|
||||
"status": (response as? HTTPURLResponse)?.statusCode ?? 0,
|
||||
"headers": safeHeaders,
|
||||
"body": ""
|
||||
]
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: tempFileURL)
|
||||
|
||||
if data.count > 10_000_000 {
|
||||
Logger.shared.log("Response exceeds maximum size", type: "Error")
|
||||
callReject("Response exceeds maximum size")
|
||||
return
|
||||
}
|
||||
|
||||
if let text = String(data: data, encoding: textEncoding) {
|
||||
responseDict["body"] = text
|
||||
callResolve(responseDict)
|
||||
} else {
|
||||
Logger.shared.log("Unable to decode data with encoding \(encoding ?? "utf-8"), trying UTF-8 fallback", type: "Warning")
|
||||
if let fallbackText = String(data: data, encoding: .utf8) {
|
||||
responseDict["body"] = fallbackText
|
||||
callResolve(responseDict)
|
||||
} else {
|
||||
Logger.shared.log("Unable to decode data to text with any encoding", type: "Error")
|
||||
callResolve(responseDict)
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
Logger.shared.log("Error reading downloaded file: \(error.localizedDescription)", type: "Error")
|
||||
callReject("Error reading downloaded file")
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
self.setObject(fetchV2NativeFunction, forKeyedSubscript: "fetchV2Native" as NSString)
|
||||
|
||||
let fetchv2Definition = """
|
||||
function fetchv2(url, headers = {}, method = "GET", body = null, redirect = true, encoding) {
|
||||
|
||||
var processedBody = null;
|
||||
if(method != "GET") {
|
||||
processedBody = (body && (typeof body === 'object')) ? JSON.stringify(body) : (body || null)
|
||||
}
|
||||
|
||||
var finalEncoding = encoding || "utf-8";
|
||||
|
||||
// Ensure headers is an object and not null/undefined
|
||||
var processedHeaders = {};
|
||||
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
||||
processedHeaders = headers;
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
fetchV2Native(url, processedHeaders, method, processedBody, redirect, finalEncoding, function(rawText) {
|
||||
const responseObj = {
|
||||
headers: rawText.headers,
|
||||
status: rawText.status,
|
||||
_data: rawText.body,
|
||||
text: function() {
|
||||
return Promise.resolve(this._data);
|
||||
},
|
||||
json: function() {
|
||||
try {
|
||||
return Promise.resolve(JSON.parse(this._data));
|
||||
} catch (e) {
|
||||
return Promise.reject("JSON parse error: " + e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
resolve(responseObj);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
"""
|
||||
self.evaluateScript(fetchv2Definition)
|
||||
}
|
||||
|
||||
func setupBase64Functions() {
|
||||
let btoaFunction: @convention(block) (String) -> String? = { data in
|
||||
guard let data = data.data(using: .utf8) else {
|
||||
Logger.shared.log("btoa: Failed to encode input as UTF-8", type: "Error")
|
||||
return nil
|
||||
}
|
||||
return data.base64EncodedString()
|
||||
}
|
||||
|
||||
let atobFunction: @convention(block) (String) -> String? = { base64String in
|
||||
guard let data = Data(base64Encoded: base64String) else {
|
||||
Logger.shared.log("atob: Invalid base64 input", type: "Error")
|
||||
return nil
|
||||
}
|
||||
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
self.setObject(btoaFunction, forKeyedSubscript: "btoa" as NSString)
|
||||
self.setObject(atobFunction, forKeyedSubscript: "atob" as NSString)
|
||||
}
|
||||
|
||||
func setupJavaScriptEnvironment() {
|
||||
setupWeirdCode()
|
||||
setupConsoleLogging()
|
||||
setupNativeFetch()
|
||||
setupFetchV2()
|
||||
setupBase64Functions()
|
||||
}
|
||||
}
|
||||
26
Sora/Utlis & Misc/Extensions/Notification+Name.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Notification+Name.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 17/04/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension Notification.Name {
|
||||
static let iCloudSyncDidComplete = Notification.Name("iCloudSyncDidComplete")
|
||||
static let iCloudSyncDidFail = Notification.Name("iCloudSyncDidFail")
|
||||
static let ContinueWatchingDidUpdate = Notification.Name("ContinueWatchingDidUpdate")
|
||||
static let DownloadManagerStatusUpdate = Notification.Name("DownloadManagerStatusUpdate")
|
||||
static let modulesSyncDidComplete = Notification.Name("modulesSyncDidComplete")
|
||||
static let moduleRemoved = Notification.Name("moduleRemoved")
|
||||
static let didReceiveNewModule = Notification.Name("didReceiveNewModule")
|
||||
static let didUpdateModules = Notification.Name("didUpdateModules")
|
||||
static let didUpdateDownloads = Notification.Name("didUpdateDownloads")
|
||||
static let didUpdateBookmarks = Notification.Name("didUpdateBookmarks")
|
||||
static let hideTabBar = Notification.Name("hideTabBar")
|
||||
static let showTabBar = Notification.Name("showTabBar")
|
||||
static let searchQueryChanged = Notification.Name("searchQueryChanged")
|
||||
static let tabBarSearchQueryUpdated = Notification.Name("tabBarSearchQueryUpdated")
|
||||
}
|
||||
24
Sora/Utlis & Misc/Extensions/String.swift
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// String.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 14/02/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
var strippedHTML: String {
|
||||
guard let data = self.data(using: .utf8) else { return self }
|
||||
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
|
||||
.documentType: NSAttributedString.DocumentType.html,
|
||||
.characterEncoding: String.Encoding.utf8.rawValue
|
||||
]
|
||||
let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil)
|
||||
return attributedString?.string ?? self
|
||||
}
|
||||
|
||||
var trimmed: String {
|
||||
return self.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
226
Sora/Utlis & Misc/Extensions/UIDevice+Model.swift
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
//
|
||||
// UIDevice+Model.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Hamzo on 02.03.25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UIDevice {
|
||||
|
||||
static let modelName: String = {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||
}
|
||||
|
||||
func mapToDevice(identifier: String) -> String {
|
||||
#if os(iOS)
|
||||
switch identifier {
|
||||
case "iPod5,1":
|
||||
return "iPod touch (5th generation)"
|
||||
case "iPod7,1":
|
||||
return "iPod touch (6th generation)"
|
||||
case "iPod9,1":
|
||||
return "iPod touch (7th generation)"
|
||||
case "iPhone3,1", "iPhone3,2", "iPhone3,3":
|
||||
return "iPhone 4"
|
||||
case "iPhone4,1":
|
||||
return "iPhone 4s"
|
||||
case "iPhone5,1", "iPhone5,2":
|
||||
return "iPhone 5"
|
||||
case "iPhone5,3", "iPhone5,4":
|
||||
return "iPhone 5c"
|
||||
case "iPhone6,1", "iPhone6,2":
|
||||
return "iPhone 5s"
|
||||
case "iPhone7,2":
|
||||
return "iPhone 6"
|
||||
case "iPhone7,1":
|
||||
return "iPhone 6 Plus"
|
||||
case "iPhone8,1":
|
||||
return "iPhone 6s"
|
||||
case "iPhone8,2":
|
||||
return "iPhone 6s Plus"
|
||||
case "iPhone9,1", "iPhone9,3":
|
||||
return "iPhone 7"
|
||||
case "iPhone9,2", "iPhone9,4":
|
||||
return "iPhone 7 Plus"
|
||||
case "iPhone10,1", "iPhone10,4":
|
||||
return "iPhone 8"
|
||||
case "iPhone10,2", "iPhone10,5":
|
||||
return "iPhone 8 Plus"
|
||||
case "iPhone10,3", "iPhone10,6":
|
||||
return "iPhone X"
|
||||
case "iPhone11,2":
|
||||
return "iPhone XS"
|
||||
case "iPhone11,4", "iPhone11,6":
|
||||
return "iPhone XS Max"
|
||||
case "iPhone11,8":
|
||||
return "iPhone XR"
|
||||
case "iPhone12,1":
|
||||
return "iPhone 11"
|
||||
case "iPhone12,3":
|
||||
return "iPhone 11 Pro"
|
||||
case "iPhone12,5":
|
||||
return "iPhone 11 Pro Max"
|
||||
case "iPhone13,1":
|
||||
return "iPhone 12 mini"
|
||||
case "iPhone13,2":
|
||||
return "iPhone 12"
|
||||
case "iPhone13,3":
|
||||
return "iPhone 12 Pro"
|
||||
case "iPhone13,4":
|
||||
return "iPhone 12 Pro Max"
|
||||
case "iPhone14,4":
|
||||
return "iPhone 13 mini"
|
||||
case "iPhone14,5":
|
||||
return "iPhone 13"
|
||||
case "iPhone14,2":
|
||||
return "iPhone 13 Pro"
|
||||
case "iPhone14,3":
|
||||
return "iPhone 13 Pro Max"
|
||||
case "iPhone14,7":
|
||||
return "iPhone 14"
|
||||
case "iPhone14,8":
|
||||
return "iPhone 14 Plus"
|
||||
case "iPhone15,2":
|
||||
return "iPhone 14 Pro"
|
||||
case "iPhone15,3":
|
||||
return "iPhone 14 Pro Max"
|
||||
case "iPhone15,4":
|
||||
return "iPhone 15"
|
||||
case "iPhone15,5":
|
||||
return "iPhone 15 Plus"
|
||||
case "iPhone16,1":
|
||||
return "iPhone 15 Pro"
|
||||
case "iPhone16,2":
|
||||
return "iPhone 15 Pro Max"
|
||||
case "iPhone17,3":
|
||||
return "iPhone 16"
|
||||
case "iPhone17,4":
|
||||
return "iPhone 16 Plus"
|
||||
case "iPhone17,1":
|
||||
return "iPhone 16 Pro"
|
||||
case "iPhone17,2":
|
||||
return "iPhone 16 Pro Max"
|
||||
case "iPhone8,4":
|
||||
return "iPhone SE"
|
||||
case "iPhone12,8":
|
||||
return "iPhone SE (2nd generation)"
|
||||
case "iPhone14,6":
|
||||
return "iPhone SE (3rd generation)"
|
||||
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4":
|
||||
return "iPad 2"
|
||||
case "iPad3,1", "iPad3,2", "iPad3,3":
|
||||
return "iPad (3rd generation)"
|
||||
case "iPad3,4", "iPad3,5", "iPad3,6":
|
||||
return "iPad (4th generation)"
|
||||
case "iPad6,11", "iPad6,12":
|
||||
return "iPad (5th generation)"
|
||||
case "iPad7,5", "iPad7,6":
|
||||
return "iPad (6th generation)"
|
||||
case "iPad7,11", "iPad7,12":
|
||||
return "iPad (7th generation)"
|
||||
case "iPad11,6", "iPad11,7":
|
||||
return "iPad (8th generation)"
|
||||
case "iPad12,1", "iPad12,2":
|
||||
return "iPad (9th generation)"
|
||||
case "iPad13,18", "iPad13,19":
|
||||
return "iPad (10th generation)"
|
||||
case "iPad4,1", "iPad4,2", "iPad4,3":
|
||||
return "iPad Air"
|
||||
case "iPad5,3", "iPad5,4":
|
||||
return "iPad Air 2"
|
||||
case "iPad11,3", "iPad11,4":
|
||||
return "iPad Air (3rd generation)"
|
||||
case "iPad13,1", "iPad13,2":
|
||||
return "iPad Air (4th generation)"
|
||||
case "iPad13,16", "iPad13,17":
|
||||
return "iPad Air (5th generation)"
|
||||
case "iPad14,8", "iPad14,9":
|
||||
return "iPad Air (11-inch) (M2)"
|
||||
case "iPad14,10", "iPad14,11":
|
||||
return "iPad Air (13-inch) (M2)"
|
||||
case "iPad2,5", "iPad2,6", "iPad2,7":
|
||||
return "iPad mini"
|
||||
case "iPad4,4", "iPad4,5", "iPad4,6":
|
||||
return "iPad mini 2"
|
||||
case "iPad4,7", "iPad4,8", "iPad4,9":
|
||||
return "iPad mini 3"
|
||||
case "iPad5,1", "iPad5,2":
|
||||
return "iPad mini 4"
|
||||
case "iPad11,1", "iPad11,2":
|
||||
return "iPad mini (5th generation)"
|
||||
case "iPad14,1", "iPad14,2":
|
||||
return "iPad mini (6th generation)"
|
||||
case "iPad16,1", "iPad16,2":
|
||||
return "iPad mini (A17 Pro)"
|
||||
case "iPad6,3", "iPad6,4":
|
||||
return "iPad Pro (9.7-inch)"
|
||||
case "iPad7,3", "iPad7,4":
|
||||
return "iPad Pro (10.5-inch)"
|
||||
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4":
|
||||
return "iPad Pro (11-inch) (1st generation)"
|
||||
case "iPad8,9", "iPad8,10":
|
||||
return "iPad Pro (11-inch) (2nd generation)"
|
||||
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7":
|
||||
return "iPad Pro (11-inch) (3rd generation)"
|
||||
case "iPad14,3", "iPad14,4":
|
||||
return "iPad Pro (11-inch) (4th generation)"
|
||||
case "iPad16,3", "iPad16,4":
|
||||
return "iPad Pro (11-inch) (M4)"
|
||||
case "iPad6,7", "iPad6,8":
|
||||
return "iPad Pro (12.9-inch) (1st generation)"
|
||||
case "iPad7,1", "iPad7,2":
|
||||
return "iPad Pro (12.9-inch) (2nd generation)"
|
||||
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8":
|
||||
return "iPad Pro (12.9-inch) (3rd generation)"
|
||||
case "iPad8,11", "iPad8,12":
|
||||
return "iPad Pro (12.9-inch) (4th generation)"
|
||||
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":
|
||||
return "iPad Pro (12.9-inch) (5th generation)"
|
||||
case "iPad14,5", "iPad14,6":
|
||||
return "iPad Pro (12.9-inch) (6th generation)"
|
||||
case "iPad16,5", "iPad16,6":
|
||||
return "iPad Pro (13-inch) (M4)"
|
||||
case "AppleTV5,3":
|
||||
return "Apple TV"
|
||||
case "AppleTV6,2":
|
||||
return "Apple TV 4K"
|
||||
case "AudioAccessory1,1":
|
||||
return "HomePod"
|
||||
case "AudioAccessory5,1":
|
||||
return "HomePod mini"
|
||||
case "i386", "x86_64", "arm64":
|
||||
return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
|
||||
default:
|
||||
return identifier
|
||||
}
|
||||
#elseif os(tvOS)
|
||||
switch identifier {
|
||||
case "AppleTV5,3":
|
||||
return "Apple TV 4"
|
||||
case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1":
|
||||
return "Apple TV 4K"
|
||||
case "i386", "x86_64":
|
||||
return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
|
||||
default:
|
||||
return identifier
|
||||
}
|
||||
#elseif os(visionOS)
|
||||
switch identifier {
|
||||
case "RealityDevice14,1":
|
||||
return "Apple Vision Pro"
|
||||
default:
|
||||
return identifier
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return mapToDevice(identifier: identifier)
|
||||
}()
|
||||
}
|
||||
25
Sora/Utlis & Misc/Extensions/URL.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// URL.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 23/03/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
var queryParameters: [String: String]? {
|
||||
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true),
|
||||
let queryItems = components.queryItems else { return nil }
|
||||
var params = [String: String]()
|
||||
for queryItem in queryItems {
|
||||
params[queryItem.name] = queryItem.value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
static func isValidHLSURL(string: String) -> Bool {
|
||||
guard let url = URL(string: string), url.pathExtension == "m3u8" else { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
121
Sora/Utlis & Misc/Extensions/URLSession.swift
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// URLSession.swift
|
||||
// Sora-JS
|
||||
//
|
||||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import Network
|
||||
import Foundation
|
||||
|
||||
class FetchDelegate: NSObject, URLSessionTaskDelegate {
|
||||
private let allowRedirects: Bool
|
||||
|
||||
init(allowRedirects: Bool) {
|
||||
self.allowRedirects = allowRedirects
|
||||
}
|
||||
deinit { Logger.shared.log("FetchDelegate deallocated", type: "Debug")
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
|
||||
if(allowRedirects) {
|
||||
completionHandler(request)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLSession {
|
||||
static let userAgents = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.86",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.2849.80",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.2 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15_1_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.1 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15.1; rv:132.0) Gecko/20100101 Firefox/132.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15.0; rv:131.0) Gecko/20100101 Firefox/131.0",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0",
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0",
|
||||
"Mozilla/5.0 (Linux; Android 15; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.135 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.135 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPad; CPU OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Android 15; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0",
|
||||
"Mozilla/5.0 (Android 14; Mobile; rv:131.0) Gecko/131.0 Firefox/131.0"
|
||||
]
|
||||
|
||||
static var randomUserAgent: String = {
|
||||
userAgents.randomElement() ?? userAgents[0]
|
||||
}()
|
||||
|
||||
static let custom: URLSession = {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent]
|
||||
return URLSession(configuration: configuration)
|
||||
}()
|
||||
|
||||
static func fetchData(allowRedirects:Bool) -> URLSession
|
||||
{
|
||||
let delegate = FetchDelegate(allowRedirects:allowRedirects)
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent]
|
||||
return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkType {
|
||||
case wifi
|
||||
case cellular
|
||||
case unknown
|
||||
}
|
||||
|
||||
class NetworkMonitor: ObservableObject {
|
||||
static let shared = NetworkMonitor()
|
||||
|
||||
private let monitor = NWPathMonitor()
|
||||
private let queue = DispatchQueue(label: "NetworkMonitor")
|
||||
|
||||
@Published var currentNetworkType: NetworkType = .unknown
|
||||
@Published var isConnected: Bool = false
|
||||
|
||||
private init() {
|
||||
startMonitoring()
|
||||
}
|
||||
|
||||
private func startMonitoring() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
DispatchQueue.main.async {
|
||||
self?.isConnected = path.status == .satisfied
|
||||
self?.currentNetworkType = self?.getNetworkType(from: path) ?? .unknown
|
||||
}
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
}
|
||||
|
||||
private func getNetworkType(from path: NWPath) -> NetworkType {
|
||||
if path.usesInterfaceType(.wifi) {
|
||||
return .wifi
|
||||
} else if path.usesInterfaceType(.cellular) {
|
||||
return .cellular
|
||||
} else {
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
static func getCurrentNetworkType() -> NetworkType {
|
||||
return shared.currentNetworkType
|
||||
}
|
||||
|
||||
deinit {
|
||||
monitor.cancel()
|
||||
}
|
||||
}
|
||||
105
Sora/Utlis & Misc/Extensions/UserDefaults.swift
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// UserDefaults.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 23/05/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum VideoQualityPreference: String, CaseIterable {
|
||||
case best = "Best"
|
||||
case p1080 = "1080p"
|
||||
case p720 = "720p"
|
||||
case p420 = "420p"
|
||||
case p360 = "360p"
|
||||
case worst = "Worst"
|
||||
|
||||
static let wifiDefaultKey = "videoQualityWiFi"
|
||||
static let cellularDefaultKey = "videoQualityCellular"
|
||||
|
||||
static let defaultWiFiPreference: VideoQualityPreference = .best
|
||||
static let defaultCellularPreference: VideoQualityPreference = .p720
|
||||
|
||||
static let qualityPriority: [VideoQualityPreference] = [.best, .p1080, .p720, .p420, .p360, .worst]
|
||||
|
||||
static func findClosestQuality(preferred: VideoQualityPreference, availableQualities: [(String, String)]) -> (String, String)? {
|
||||
for (name, url) in availableQualities {
|
||||
if isQualityMatch(preferred: preferred, qualityName: name) {
|
||||
return (name, url)
|
||||
}
|
||||
}
|
||||
|
||||
let preferredIndex = qualityPriority.firstIndex(of: preferred) ?? qualityPriority.count
|
||||
|
||||
for i in 0..<qualityPriority.count {
|
||||
let candidate = qualityPriority[i]
|
||||
for (name, url) in availableQualities {
|
||||
if isQualityMatch(preferred: candidate, qualityName: name) {
|
||||
return (name, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableQualities.first
|
||||
}
|
||||
|
||||
private static func isQualityMatch(preferred: VideoQualityPreference, qualityName: String) -> Bool {
|
||||
let lowercaseName = qualityName.lowercased()
|
||||
|
||||
switch preferred {
|
||||
case .best:
|
||||
return lowercaseName.contains("best") || lowercaseName.contains("highest") || lowercaseName.contains("max")
|
||||
case .p1080:
|
||||
return lowercaseName.contains("1080") || lowercaseName.contains("1920")
|
||||
case .p720:
|
||||
return lowercaseName.contains("720") || lowercaseName.contains("1280")
|
||||
case .p420:
|
||||
return lowercaseName.contains("420") || lowercaseName.contains("480")
|
||||
case .p360:
|
||||
return lowercaseName.contains("360") || lowercaseName.contains("640")
|
||||
case .worst:
|
||||
return lowercaseName.contains("worst") || lowercaseName.contains("lowest") || lowercaseName.contains("min")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UserDefaults {
|
||||
func color(forKey key: String) -> UIColor? {
|
||||
guard let colorData = data(forKey: key) else { return nil }
|
||||
do {
|
||||
return try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: colorData)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func set(_ color: UIColor?, forKey key: String) {
|
||||
guard let color = color else {
|
||||
removeObject(forKey: key)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: false)
|
||||
set(data, forKey: key)
|
||||
} catch {
|
||||
Logger.shared.log("Error archiving color: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
static func getVideoQualityPreference() -> VideoQualityPreference {
|
||||
let networkType = NetworkMonitor.getCurrentNetworkType()
|
||||
|
||||
switch networkType {
|
||||
case .wifi:
|
||||
let rawValue = UserDefaults.standard.string(forKey: VideoQualityPreference.wifiDefaultKey)
|
||||
return VideoQualityPreference(rawValue: rawValue ?? "") ?? VideoQualityPreference.defaultWiFiPreference
|
||||
case .cellular:
|
||||
let rawValue = UserDefaults.standard.string(forKey: VideoQualityPreference.cellularDefaultKey)
|
||||
return VideoQualityPreference(rawValue: rawValue ?? "") ?? VideoQualityPreference.defaultCellularPreference
|
||||
case .unknown:
|
||||
return .p720
|
||||
}
|
||||
}
|
||||
}
|
||||