Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa88d35b2b | ||
|
|
64d5c1f6aa | ||
|
|
56baa48223 | ||
|
|
ad3edd4652 | ||
|
|
81bac6ee9b | ||
|
|
a715f4e26b | ||
|
|
6115c21b44 | ||
|
|
f367260acd | ||
|
|
7f8b827c0e | ||
|
|
0a1abfcbbb | ||
|
|
f53592f32b | ||
|
|
fd1d187ede | ||
|
|
b4e78febc0 | ||
|
|
ff2928b18f | ||
|
|
8b34307975 | ||
|
|
9cca40420c | ||
|
|
3da51f9d64 | ||
|
|
2bd9e6655f | ||
|
|
d2b01100a5 | ||
|
|
47837edb01 | ||
|
|
91057c12a4 | ||
|
|
66b32468eb | ||
|
|
1e62c6c756 | ||
|
|
f8132e7bdb | ||
|
|
dec2e1c9fc | ||
|
|
ee20965218 | ||
|
|
2e429840d2 | ||
|
|
e63920e23b | ||
|
|
11d2e40e34 | ||
|
|
ddb4a91b08 | ||
|
|
157669bd76 | ||
|
|
f055759c7a | ||
|
|
34815f22ee | ||
|
|
1d3ae3e1c5 | ||
|
|
3cf1ebc09b | ||
|
|
360741674d | ||
|
|
c369f2c3d7 | ||
|
|
65e22fbbbd | ||
|
|
74b2d76fea | ||
|
|
6c61e0e4c9 | ||
|
|
a60e1e7f42 | ||
|
|
1b1c4b741b | ||
|
|
f9d8549e56 | ||
|
|
bdd9d24d32 | ||
|
|
a7ba837cb1 | ||
|
|
d04781e4b3 | ||
|
|
f87f0b0807 | ||
|
|
59346f4b67 | ||
|
|
06174af16c | ||
|
|
f1867ddeb7 | ||
|
|
79f7de1b4c | ||
|
|
15de88463c | ||
|
|
c032d97c5f | ||
|
|
2eca47ab69 | ||
|
|
caa70dd460 | ||
|
|
6b0e65b563 | ||
|
|
e0308be8bb | ||
|
|
4663c435c9 | ||
|
|
8380980abd | ||
|
|
771c431188 | ||
|
|
1e1bb9176a | ||
|
|
6eb5c847af | ||
|
|
e50fdc4eb9 | ||
|
|
4edf1a8c42 | ||
|
|
cb455d379c | ||
|
|
d093023ce7 | ||
|
|
a55650bc40 | ||
|
|
6cbf0980c5 | ||
|
|
4cb65c1bfa | ||
|
|
f30e47dbd7 | ||
|
|
7ab6b9720f | ||
|
|
2f7c87d437 | ||
|
|
bc4b4955da | ||
|
|
f7e3408c86 | ||
|
|
2126a38f39 | ||
|
|
b81f6eaa96 | ||
|
|
17eb37cb37 | ||
|
|
9208aa8d16 | ||
|
|
bd7f7894ed | ||
|
|
6dfdb62df8 | ||
|
|
3be302d69f | ||
|
|
66ebc547f0 | ||
|
|
3366b83a89 | ||
|
|
cd3db94b5d | ||
|
|
d56067e7cc | ||
|
|
7c3cee692c | ||
|
|
a948d82c24 | ||
|
|
77893bb9ba | ||
|
|
1b2ecd9de4 | ||
|
|
b92842f9ac | ||
|
|
91d191b3d8 | ||
|
|
21accaa883 | ||
|
|
c6f2e949a7 | ||
|
|
9d9d9bb311 | ||
|
|
0b20300572 | ||
|
|
f47bcb69fd | ||
|
|
c20bb93f1f | ||
|
|
497259ca16 | ||
|
|
4322ffc6e5 | ||
|
|
8b40e0993f | ||
|
|
966b6371f5 | ||
|
|
5b493a70b0 | ||
|
|
1cb793d781 | ||
|
|
7f237c9c7b | ||
|
|
d0934295b0 | ||
|
|
8d40ba3011 | ||
|
|
f926deb63b | ||
|
|
b82c612f94 | ||
|
|
e7ae108c49 | ||
|
|
08689df2f5 | ||
|
|
87c308f6ca | ||
|
|
15b22f75a8 | ||
|
|
8e7431e7e5 | ||
|
|
c034f9b66e | ||
|
|
5cf53f8cfa | ||
|
|
e4de7f1ecb | ||
|
|
bf9913ba0f | ||
|
|
6ac8c905c9 | ||
|
|
603b5b5105 | ||
|
|
7528dcec71 | ||
|
|
d96848181d | ||
|
|
16b6ea8f29 | ||
|
|
7150ea2987 | ||
|
|
bc2febf480 | ||
|
|
11ec48a506 | ||
|
|
f08989cc99 | ||
|
|
424351092d | ||
|
|
df72c3e6f4 | ||
|
|
82e605a1c6 | ||
|
|
7a91675054 | ||
|
|
edf4eb2090 | ||
|
|
de1b37b7e9 | ||
|
|
928206a179 | ||
|
|
52b8944d71 | ||
|
|
0db5cb7025 | ||
|
|
cef8f5efe2 | ||
|
|
83ff87c79f | ||
|
|
00f5357403 | ||
|
|
010ace58fa | ||
|
|
b3cf4ebf12 | ||
|
|
f707b7b5f2 | ||
|
|
29ab2ddad3 | ||
|
|
627024bd13 | ||
|
|
c4a71048f5 | ||
|
|
bd07b7f011 | ||
|
|
93496849ec | ||
|
|
9984f42176 | ||
|
|
a262c94f88 | ||
|
|
8f326e692d | ||
|
|
7b8a47aac1 | ||
|
|
b46f1cccb1 | ||
|
|
5880451401 | ||
|
|
3c59452782 | ||
|
|
7d35132509 | ||
|
|
dbb3e14c50 | ||
|
|
d1d45c4998 | ||
|
|
c9db6e528e | ||
|
|
062fd82de1 | ||
|
|
122cff359d | ||
|
|
ef96ccb623 | ||
|
|
8a66cf6d92 | ||
|
|
16dd352231 | ||
|
|
fb275ea242 | ||
|
|
533472299a | ||
|
|
da95901d39 | ||
|
|
29101c310b | ||
|
|
91db6d0e57 | ||
|
|
504da89223 | ||
|
|
4a54b897ac | ||
|
|
9565682eda | ||
|
|
17741a2f61 | ||
|
|
f5407ecac8 | ||
|
|
c57cbfb2f6 | ||
|
|
339a767304 | ||
|
|
4c534b44d4 | ||
|
|
984125a79e | ||
|
|
17f1075e64 | ||
|
|
64a5874566 | ||
|
|
b16a6a6bd5 | ||
|
|
87da5b400e | ||
|
|
3f1da5f289 | ||
|
|
45525a4f14 | ||
|
|
fc7892daed | ||
|
|
51a49cfca3 | ||
|
|
da8b583286 | ||
|
|
2c12a07257 | ||
|
|
3a82119c2a | ||
|
|
e6ea1b056a | ||
|
|
1a96d2b418 | ||
|
|
8eb716505d | ||
|
|
ddb157809a | ||
|
|
ad8faab9ea | ||
|
|
6a7644935f | ||
|
|
684196aa36 | ||
|
|
56697303f9 | ||
|
|
3306a28892 | ||
|
|
059509354d | ||
|
|
53d50dbc3b | ||
|
|
46ebda944a | ||
|
|
0bb200a785 | ||
|
|
d3602201c6 | ||
|
|
beb78fada0 | ||
|
|
a05efef3c8 | ||
|
|
36d63860e2 | ||
|
|
ad9be7f132 | ||
|
|
8e9968628a | ||
|
|
e33db4f6d7 | ||
|
|
ab5ad3c839 | ||
|
|
fa4af97fb0 | ||
|
|
db50cb4837 | ||
|
|
b4b90eec63 | ||
|
|
beff313814 | ||
|
|
0dc9cd38af | ||
|
|
8ff7676e60 | ||
|
|
bfe35220aa | ||
|
|
843faee4c5 | ||
|
|
f8bfeb0ec6 | ||
|
|
dd441fee66 | ||
|
|
2204b05d6c | ||
|
|
a1841804b6 | ||
|
|
25e3c4a543 | ||
|
|
5e31ad9c67 | ||
|
|
477b92291b | ||
|
|
fcc6186796 | ||
|
|
23179f6d17 | ||
|
|
996e8621fa | ||
|
|
8ad1cb988f | ||
|
|
4cef99dda2 | ||
|
|
4beeedd318 | ||
|
|
23043c61f4 | ||
|
|
8d56171bd2 | ||
|
|
ff0fafe938 | ||
|
|
8b7f369c16 | ||
|
|
5c0e40bddb | ||
|
|
58b0bf5306 | ||
|
|
9bd2774820 | ||
|
|
cc7da5bbc9 | ||
|
|
bd4e30e653 | ||
|
|
e759dafa87 | ||
|
|
3ae2488adf | ||
|
|
2f5b596fff | ||
|
|
472ac90fea | ||
|
|
e9dfee6870 | ||
|
|
48e57fd375 | ||
|
|
4b05ed79ea | ||
|
|
4365e70a78 | ||
|
|
e8c02d04ae | ||
|
|
0539c1d869 | ||
|
|
1980ce92d5 | ||
|
|
b5576fa6de | ||
|
|
d355e07e10 | ||
|
|
09ff47e19f | ||
|
|
4ee4a766d6 | ||
|
|
7ebbf75cb9 | ||
|
|
e95b768350 | ||
|
|
82f0d46acc | ||
|
|
bafba0b151 | ||
|
|
7a68218439 | ||
|
|
2d37d37596 | ||
|
|
d64c72f40e | ||
|
|
efe375f817 | ||
|
|
1f8dfd3587 | ||
|
|
60eae5d14f | ||
|
|
5123a5ec48 | ||
|
|
dbb3ae38b3 | ||
|
|
fbf6fc988b | ||
|
|
17da465e4e | ||
|
|
e096a4ccb8 | ||
|
|
079417ad3e | ||
|
|
edb2120f98 | ||
|
|
cc0305d84a | ||
|
|
3f78686661 | ||
|
|
13604cca01 | ||
|
|
eae4eacf00 | ||
|
|
4119824048 | ||
|
|
8bdf2b53d3 | ||
|
|
fb4f17a898 | ||
|
|
ad0623334c | ||
|
|
13dcfd7551 | ||
|
|
053884a147 | ||
|
|
4f893cb826 | ||
|
|
94aa966f46 | ||
|
|
0fcdcf3c1f | ||
|
|
4147bd0d86 | ||
|
|
3ea3455772 | ||
|
|
a82c3d2d2d | ||
|
|
04ef1b8ad2 | ||
|
|
b009dd9c80 | ||
|
|
e9f1979e6c | ||
|
|
23906dd65d | ||
|
|
630755cf7b | ||
|
|
3c2f72d15f | ||
|
|
ff5cf06e60 | ||
|
|
9798486724 | ||
|
|
db6161d84e | ||
|
|
af0de5c0a9 | ||
|
|
e0d9b4ee93 | ||
|
|
551be5bbef | ||
|
|
bbc7f0f665 | ||
|
|
8433d59432 | ||
|
|
59b3e6ce81 | ||
|
|
a5629c54f6 | ||
|
|
45726a57ce | ||
|
|
5f597ef567 | ||
|
|
bf464b257d | ||
|
|
0100b2a2ea | ||
|
|
360c3356cf | ||
|
|
5f18efcfb3 | ||
|
|
79888168ae | ||
|
|
2427a55901 | ||
|
|
ac397f32a3 | ||
|
|
d236e7fec6 | ||
|
|
6d7cdc4ef4 | ||
|
|
9a296b3da8 | ||
|
|
f27861748c | ||
|
|
c9668af2ae | ||
|
|
9c2253616f | ||
|
|
cfb8bf04d6 | ||
|
|
9efdd5a17d | ||
|
|
dd11e8d3b2 | ||
|
|
cde6e99a17 | ||
|
|
045e68ca10 | ||
|
|
4f736c555b | ||
|
|
642c49b1ea | ||
|
|
e13b16f13c | ||
|
|
1f9fa9ba4f | ||
|
|
849ef6efe5 | ||
|
|
e968a20f4c | ||
|
|
7a6b866c28 | ||
|
|
6ccbaed3fd | ||
|
|
d01628a262 | ||
|
|
675ac84486 | ||
|
|
e84dc93910 | ||
|
|
3d30ea1342 | ||
|
|
efd28d3ca3 | ||
|
|
8a28a2c398 | ||
|
|
c6fa6afa9b | ||
|
|
912ebc2f90 | ||
|
|
f04b49e260 | ||
|
|
a17458ff9b | ||
|
|
c062dd944e | ||
|
|
1a4516bdd1 | ||
|
|
5538e0627b | ||
|
|
7febfa9771 | ||
|
|
160be52f02 | ||
|
|
828d57af24 | ||
|
|
1f7fb2201c | ||
|
|
32ae51142f | ||
|
|
7014df7a0e | ||
|
|
cb0ed15c0c | ||
|
|
d4e9364d29 | ||
|
|
4681d60850 | ||
|
|
78052dc07f | ||
|
|
67c66ff0c1 | ||
|
|
d1bb99635a | ||
|
|
69092e3656 | ||
|
|
98138a4f35 | ||
|
|
25c7f325b6 | ||
|
|
5dbaf961b7 | ||
|
|
b4471fab49 | ||
|
|
307fd354d6 | ||
|
|
a3b5557185 | ||
|
|
3c491be690 | ||
|
|
580c1d82e5 | ||
|
|
4cc83006ec | ||
|
|
4954c9c569 | ||
|
|
8f1b40d143 | ||
|
|
d72153c25a | ||
|
|
666b59c293 | ||
|
|
7543a8c55b | ||
|
|
bfc7be2b80 | ||
|
|
8b2c68f3ae | ||
|
|
9aa019fe6f | ||
|
|
23a100afdc | ||
|
|
af2ac2c993 | ||
|
|
166e73cdaa | ||
|
|
73fe9e054f | ||
|
|
42e066288f | ||
|
|
682a04f047 | ||
|
|
29cb88e6e2 | ||
|
|
6daf77c890 | ||
|
|
0372840ee1 | ||
|
|
3c51760203 | ||
|
|
f3c735e3a1 | ||
|
|
7784cb7511 | ||
|
|
82b0de9223 | ||
|
|
a6852d716f | ||
|
|
92dfe5b41f | ||
|
|
16e96cdde0 | ||
|
|
61e4bda616 | ||
|
|
e69fab87d0 | ||
|
|
a3e738773b | ||
|
|
425957c804 | ||
|
|
efa1cc8bd8 | ||
|
|
0fc64cdf81 | ||
|
|
d08c4c905f | ||
|
|
43b9bd7038 | ||
|
|
2dddd00fd3 | ||
|
|
5be7b43cb8 | ||
|
|
965d9540fa | ||
|
|
65bb7bd44e | ||
|
|
a5ade78cd6 | ||
|
|
57420e51cf | ||
|
|
bec8b593be | ||
|
|
c62e406749 | ||
|
|
d503f6c6ce | ||
|
|
ff6e3ee92a | ||
|
|
16676bca49 | ||
|
|
9e2e1f72ce | ||
|
|
f85afbe576 | ||
|
|
6fa6b34b59 | ||
|
|
7a1f677c75 | ||
|
|
de28691481 | ||
|
|
686a61ac74 | ||
|
|
0b1f64ef8a | ||
|
|
5b910fd3d7 | ||
|
|
c56e73eefe | ||
|
|
7cfa18e1e9 | ||
|
|
06034bb8e7 | ||
|
|
7b20543fa1 | ||
|
|
d207185130 | ||
|
|
66628e1bca | ||
|
|
beff4f9ca0 | ||
|
|
1318c54c0c | ||
|
|
b05d69b10f | ||
|
|
7825490b0f | ||
|
|
c06b0efedc | ||
|
|
43f4736936 | ||
|
|
cf8f1849aa | ||
|
|
c9a1bc97c4 | ||
|
|
581f665f58 | ||
|
|
7b3a8ad642 | ||
|
|
c1e2bc36ea | ||
|
|
72aed76a1c | ||
|
|
7ae293631a | ||
|
|
f3d24866b7 | ||
|
|
985ba5d2c7 | ||
|
|
f7ca80de77 | ||
|
|
a9c7130d55 | ||
|
|
9730ffd541 | ||
|
|
232f018ae7 | ||
|
|
8571ee74c4 | ||
|
|
9566e0b897 | ||
|
|
863f90f65d | ||
|
|
22a733280a | ||
|
|
a4008d3d6b | ||
|
|
819d7b0f8f | ||
|
|
c785ce1f88 | ||
|
|
0c3588210a | ||
|
|
db57fa73ce | ||
|
|
0269825179 | ||
|
|
588b413223 | ||
|
|
a2cd3397c9 | ||
|
|
268f293704 | ||
|
|
317d5dbdd9 | ||
|
|
7f2d548d32 | ||
|
|
5877673755 | ||
|
|
3b351d7634 | ||
|
|
e6ee83d9c2 | ||
|
|
8b57e810ab | ||
|
|
4121d568c8 | ||
|
|
551bd83ffe | ||
|
|
5f4e4bfdd3 | ||
|
|
8cd48f3af5 | ||
|
|
13b5d7bae8 | ||
|
|
3a76754af9 | ||
|
|
73eeac35a8 | ||
|
|
18abcef8a2 | ||
|
|
85800abf9f | ||
|
|
2b72327013 | ||
|
|
cc86afb31d | ||
|
|
efac1a9f28 | ||
|
|
a9e4091500 | ||
|
|
891c4f8c32 | ||
|
|
96f7bd431c | ||
|
|
cd2ab9d9cd | ||
|
|
8f3d52142d | ||
|
|
cb7de47aed | ||
|
|
174c78bf67 | ||
|
|
19c36ca340 | ||
|
|
b0432ece86 | ||
|
|
a009c6db93 | ||
|
|
e60b8d13b9 | ||
|
|
1b673743b2 | ||
|
|
34e8c179fe | ||
|
|
33d5760930 | ||
|
|
77ec4cc5c8 | ||
|
|
c8e6cac00c | ||
|
|
35fc0334bc | ||
|
|
c6ace690cb | ||
|
|
7a979bed2f | ||
|
|
e46bd71fa7 | ||
|
|
c98010234f | ||
|
|
02e175e4d3 | ||
|
|
013bcfbece | ||
|
|
ebf2d1e516 | ||
|
|
4bce668df6 | ||
|
|
c64c7defc3 | ||
|
|
1629801074 | ||
|
|
cd36e3acb0 | ||
|
|
7001032e51 | ||
|
|
3a0bac4446 | ||
|
|
1c31f0effa | ||
|
|
01e61c590a | ||
|
|
a00e2b78d6 | ||
|
|
b5904bc477 | ||
|
|
47f2afb665 | ||
|
|
3b2e278868 | ||
|
|
8baa84c06f | ||
|
|
92b589213f | ||
|
|
94395feaba | ||
|
|
c372920965 | ||
|
|
5ab6af1b02 | ||
|
|
34252b9ba7 | ||
|
|
8d3f448fdf | ||
|
|
7ebbfb9ef7 | ||
|
|
a008f86ee2 | ||
|
|
5f1c51ff6c | ||
|
|
229d6b24e1 | ||
|
|
9dc0654225 | ||
|
|
36277076bd | ||
|
|
f96ef0740d | ||
|
|
6bee1a6c38 | ||
|
|
0dbba6439d | ||
|
|
7ac888cd46 | ||
|
|
88f4ae9987 | ||
|
|
938291dbf6 | ||
|
|
f5715987fc | ||
|
|
7c0e86c3cf |
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
if [ "${SKIP_HOOKS:-}" = "1" ]; then
|
||||
echo "[pre-commit] skipped (SKIP_HOOKS=1)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
GIT_DIR="$(git rev-parse --git-dir)"
|
||||
if [ -f "$GIT_DIR/MERGE_MSG" ] || [ -f "$GIT_DIR/MERGE_HEAD" ] || [ -f "$GIT_DIR/REVERT_HEAD" ]; then
|
||||
echo "[pre-commit] skipped (merge/revert in progress)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "[pre-commit] nothing staged — skipping checks"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
exec "$ROOT/scripts/ci_checks.sh"
|
||||
@@ -1 +1,2 @@
|
||||
github: edde746
|
||||
liberapay: edde746
|
||||
|
||||
+22
-12
@@ -3,6 +3,12 @@ name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SENTRY_DART_DEFINE: ${{ github.repository == 'edde746/plezy' && '--dart-define=ENABLE_SENTRY=true' || '' }}
|
||||
GIT_COMMIT_DART_DEFINE: --dart-define=GIT_COMMIT=${{ github.sha }}
|
||||
SENTRY_ENV_DART_DEFINE: --dart-define=SENTRY_ENVIRONMENT=github
|
||||
DONATIONS_DART_DEFINE: --dart-define=ENABLE_DONATIONS=true
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,7 +64,7 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Build APKs
|
||||
run: flutter build apk --release --split-per-abi --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
run: flutter build apk --release --split-per-abi --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
|
||||
|
||||
- name: Clean up keystore
|
||||
if: always()
|
||||
@@ -126,7 +132,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build iOS (no codesign)
|
||||
run: flutter build ios --release --no-codesign --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
run: flutter build ios --release --no-codesign --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
|
||||
|
||||
- name: Create IPA
|
||||
run: |
|
||||
@@ -184,7 +190,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build macOS
|
||||
run: flutter build macos --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
run: flutter build macos --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
|
||||
|
||||
- name: Import Code Signing Certificate
|
||||
env:
|
||||
@@ -335,7 +341,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Windows x64
|
||||
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
|
||||
|
||||
- name: Upload x64 build
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -372,7 +378,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Windows arm64
|
||||
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
|
||||
|
||||
- name: Upload arm64 build
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -490,13 +496,13 @@ jobs:
|
||||
- name: Cache APT packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
|
||||
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
|
||||
version: 1.1
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
|
||||
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
|
||||
|
||||
- name: Cache libmpv build
|
||||
id: libmpv-cache
|
||||
@@ -516,7 +522,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Linux x64
|
||||
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
|
||||
env:
|
||||
PKG_CONFIG_PATH: ${{ github.workspace }}/libmpv-prefix/lib/pkgconfig:${{ github.workspace }}/libmpv-prefix/lib/x86_64-linux-gnu/pkgconfig
|
||||
|
||||
@@ -598,13 +604,13 @@ jobs:
|
||||
- name: Cache APT packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
|
||||
packages: clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
|
||||
version: 1.1
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick
|
||||
sudo apt-get install -y clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
|
||||
|
||||
- name: Cache libmpv build
|
||||
id: libmpv-cache
|
||||
@@ -624,7 +630,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Linux arm64
|
||||
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true ${{ env.SENTRY_DART_DEFINE }} ${{ env.GIT_COMMIT_DART_DEFINE }} ${{ env.SENTRY_ENV_DART_DEFINE }} ${{ env.DONATIONS_DART_DEFINE }}
|
||||
env:
|
||||
PKG_CONFIG_PATH: ${{ github.workspace }}/libmpv-prefix/lib/pkgconfig:${{ github.workspace }}/libmpv-prefix/lib/aarch64-linux-gnu/pkgconfig
|
||||
|
||||
@@ -712,11 +718,14 @@ jobs:
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//')
|
||||
BUILD_NUMBER=$(grep '^version:' pubspec.yaml | sed 's/.*+//')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate appcast.xml
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
BUILD_NUMBER="${{ steps.version.outputs.build_number }}"
|
||||
|
||||
# Read macOS signature metadata (may be empty if secrets not configured)
|
||||
MACOS_ED_SIG=""
|
||||
@@ -745,7 +754,8 @@ jobs:
|
||||
<title>Plezy Updates</title>
|
||||
<item>
|
||||
<title>Version ${VERSION}</title>
|
||||
<sparkle:version>${VERSION}</sparkle:version>
|
||||
<sparkle:version>${BUILD_NUMBER}</sparkle:version>
|
||||
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
|
||||
<sparkle:releaseNotesLink>https://gh-html.edde746.dev/edde746/plezy/${VERSION}</sparkle:releaseNotesLink>
|
||||
XMLEOF
|
||||
|
||||
|
||||
@@ -52,3 +52,27 @@ jobs:
|
||||
identifier: edde746.Plezy
|
||||
installers-regex: 'plezy-windows-installer\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
update-appcast-branch:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download appcast.xml from release
|
||||
run: |
|
||||
gh release download "${{ github.event.release.tag_name }}" \
|
||||
--pattern "appcast.xml"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Push to appcast branch
|
||||
run: |
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
BLOB=$(git hash-object -w appcast.xml)
|
||||
TREE=$(printf "100644 blob %s\tappcast.xml\n" "$BLOB" | git mktree)
|
||||
COMMIT=$(git commit-tree "$TREE" -m "Update appcast for $VERSION")
|
||||
git push origin "$COMMIT:refs/heads/appcast" --force
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
cask "plezy" do
|
||||
version "1.22.0"
|
||||
sha256 "94524bb6b7c4779f5416cef7d3a876e081f7ad6e737e828f790e52920b7086f4"
|
||||
version "1.33.1"
|
||||
sha256 "155722eb653c0efa939e47c38d1fb340c693f9039424effcb9c2bf4aea80720e"
|
||||
|
||||
url "https://github.com/edde746/plezy/releases/download/#{version}/plezy-macos.dmg"
|
||||
name "Plezy"
|
||||
|
||||
@@ -20,12 +20,20 @@ A modern Plex client for desktop and mobile. Built with Flutter for native perfo
|
||||
- [Windows (x64, arm64)](https://github.com/edde746/plezy/releases/latest/download/plezy-windows-installer.exe)
|
||||
- [macOS (x64, arm64)](https://github.com/edde746/plezy/releases/latest/download/plezy-macos.dmg)
|
||||
- [Linux (x64, arm64)](https://github.com/edde746/plezy/releases/latest) - .deb, .rpm, .pkg.tar.zst, and portable tar.gz available
|
||||
- [NixOS/Nix](https://github.com/mio-19/nurpkgs/tree/main/pkgs/plezy) - Community package by [@mio-19](https://github.com/mio-19)
|
||||
- [Nix](https://search.nixos.org/packages?channel=unstable&query=plezy) - Community package by [@mio-19](https://github.com/mio-19) and [@MiniHarinn](https://github.com/MiniHarinn)
|
||||
- **Homebrew** (macOS):
|
||||
```bash
|
||||
brew tap edde746/plezy https://github.com/edde746/plezy
|
||||
brew install --cask plezy
|
||||
```
|
||||
- [AUR](https://aur.archlinux.org/packages/plezy-bin) (Arch Linux) - Community maintained by [@jianglai](https://github.com/jianglai):
|
||||
```bash
|
||||
yay -S plezy-bin
|
||||
```
|
||||
- **WinGet** (Windows):
|
||||
```bash
|
||||
winget install edde746.plezy
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -8,6 +8,66 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val mpvVersion = "v1.0.6"
|
||||
val mpvDir = layout.buildDirectory.dir("libmpv").get().asFile
|
||||
val mpvAar = "libmpv-release.aar"
|
||||
|
||||
val downloadLibmpv by tasks.registering {
|
||||
val stamp = File(mpvDir, ".version")
|
||||
outputs.upToDateWhen { stamp.exists() && stamp.readText().trim() == mpvVersion }
|
||||
doLast {
|
||||
mpvDir.mkdirs()
|
||||
val url = "https://github.com/edde746/libmpv-android/releases/download/$mpvVersion/$mpvAar"
|
||||
exec { commandLine("curl", "-sfL", url, "-o", File(mpvDir, mpvAar).absolutePath) }
|
||||
stamp.writeText(mpvVersion)
|
||||
}
|
||||
}
|
||||
|
||||
val assVersion = "fp-2"
|
||||
val assDir = layout.buildDirectory.dir("libass").get().asFile
|
||||
val assAars = listOf("lib_ass-release.aar", "lib_ass_kt-release.aar", "lib_ass_media-release.aar")
|
||||
|
||||
val downloadLibass by tasks.registering {
|
||||
val stamp = File(assDir, ".version")
|
||||
outputs.upToDateWhen { stamp.exists() && stamp.readText().trim() == assVersion }
|
||||
doLast {
|
||||
assDir.mkdirs()
|
||||
val baseUrl = "https://github.com/edde746/libass-android/releases/download/$assVersion"
|
||||
assAars.forEach { name ->
|
||||
val dest = File(assDir, name)
|
||||
exec { commandLine("curl", "-sfL", "$baseUrl/$name", "-o", dest.absolutePath) }
|
||||
}
|
||||
stamp.writeText(assVersion)
|
||||
}
|
||||
}
|
||||
|
||||
val doviVersion = "2.3.1"
|
||||
val doviDir = layout.buildDirectory.dir("libdovi").get().asFile
|
||||
val doviAbis = mapOf(
|
||||
"arm64-v8a" to "aarch64-linux-android",
|
||||
"armeabi-v7a" to "armv7-linux-androideabi",
|
||||
"x86" to "i686-linux-android",
|
||||
"x86_64" to "x86_64-linux-android",
|
||||
)
|
||||
|
||||
val downloadLibdovi by tasks.registering {
|
||||
val stamp = File(doviDir, ".version")
|
||||
outputs.upToDateWhen { stamp.exists() && stamp.readText().trim() == doviVersion }
|
||||
doLast {
|
||||
doviDir.mkdirs()
|
||||
val baseUrl = "https://github.com/edde746/libdovi-builds/releases/download/v$doviVersion"
|
||||
doviAbis.forEach { (abi, triple) ->
|
||||
val archive = File(doviDir, "$triple.tar.gz")
|
||||
exec { commandLine("curl", "-sfL", "$baseUrl/libdovi-$triple.tar.gz", "-o", archive.absolutePath) }
|
||||
val outDir = File(doviDir, "$abi/lib")
|
||||
outDir.mkdirs()
|
||||
exec { commandLine("tar", "-xzf", archive.absolutePath, "-C", outDir.absolutePath) }
|
||||
archive.delete()
|
||||
}
|
||||
stamp.writeText(doviVersion)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.edde746.plezy"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -30,6 +90,28 @@ android {
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += listOf(
|
||||
"-DDOVI_ENABLE_LIBDOVI=ON",
|
||||
"-DDOVI_LIBDOVI_PREBUILT_ROOT=${doviDir.absolutePath}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (System.getenv("AMAZON") != null) {
|
||||
versionCode = (flutter.versionCode ?: 0) + 3000
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -73,22 +155,38 @@ flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
// Download libdovi before any CMake/native build task
|
||||
tasks.matching { it.name.contains("CMake") || it.name.contains("externalNative") }.configureEach {
|
||||
dependsOn(downloadLibdovi)
|
||||
}
|
||||
|
||||
// Download libmpv and libass AARs before compilation
|
||||
tasks.matching { it.name.startsWith("pre") && it.name.endsWith("Build") }.configureEach {
|
||||
dependsOn(downloadLibmpv)
|
||||
dependsOn(downloadLibass)
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation("dev.jdtech.mpv:libmpv:0.5.1")
|
||||
implementation(files(File(mpvDir, mpvAar)))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
|
||||
|
||||
// Android TV Watch Next integration
|
||||
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
||||
|
||||
// Media3 ExoPlayer for Android
|
||||
implementation("androidx.media3:media3-exoplayer:1.9.2")
|
||||
implementation("androidx.media3:media3-exoplayer-hls:1.9.2")
|
||||
implementation("androidx.media3:media3-ui:1.9.2")
|
||||
implementation("androidx.media3:media3-common:1.9.2")
|
||||
|
||||
// Cronet for HTTP/2 multiplexing + better connection management
|
||||
implementation("androidx.media3:media3-datasource-cronet:1.9.2")
|
||||
implementation("org.chromium.net:cronet-embedded:143.7445.0")
|
||||
|
||||
// FFmpeg audio decoder for unsupported codecs (ALAC, DTS, TrueHD, etc.)
|
||||
implementation("org.jellyfin.media3:media3-ffmpeg-decoder:1.9.0+1")
|
||||
|
||||
// libass-android for ASS/SSA subtitle rendering (fork with font buffering fix)
|
||||
implementation(files("libs/ass-media.aar"))
|
||||
// ass-kt core library (needed for AssRender.setFontScale)
|
||||
implementation("io.github.peerless2012:ass-kt:0.4.0-beta01")
|
||||
// libass-android for ASS/SSA subtitle rendering
|
||||
assAars.forEach { implementation(files(File(assDir, it))) }
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -6,14 +6,8 @@
|
||||
<!-- Internet access permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- Camera permission for QR code scanning (companion remote pairing) -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<!-- PIP Permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
|
||||
<!-- Media session permissions for OS media controls -->
|
||||
<!-- PIP and media session foreground service permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
|
||||
@@ -27,6 +21,7 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||
|
||||
|
||||
<!-- Android TV support (not required, but allow detection) -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<!-- Android TV Watch Next integration -->
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(dovi_bridge)
|
||||
|
||||
option(DOVI_ENABLE_LIBDOVI "Link real libdovi" ON)
|
||||
set(DOVI_LIBDOVI_PREBUILT_ROOT "" CACHE PATH "Path to prebuilt libdovi")
|
||||
|
||||
add_library(dovi_bridge SHARED dovi_bridge.cpp)
|
||||
|
||||
if(DOVI_ENABLE_LIBDOVI)
|
||||
add_library(dovi_static STATIC IMPORTED)
|
||||
set_target_properties(dovi_static PROPERTIES
|
||||
IMPORTED_LOCATION "${DOVI_LIBDOVI_PREBUILT_ROOT}/${ANDROID_ABI}/lib/libdovi.a")
|
||||
target_compile_definitions(dovi_bridge PRIVATE DOVI_REAL_LINKED=1)
|
||||
target_include_directories(dovi_bridge PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
target_link_libraries(dovi_bridge dovi_static log)
|
||||
else()
|
||||
target_compile_definitions(dovi_bridge PRIVATE DOVI_REAL_LINKED=0)
|
||||
target_link_libraries(dovi_bridge log)
|
||||
endif()
|
||||
@@ -0,0 +1,126 @@
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#define TAG "DoviBridge"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
|
||||
|
||||
#if DOVI_REAL_LINKED
|
||||
#include "include/libdovi/rpu_parser.h"
|
||||
#endif
|
||||
|
||||
static const char* BRIDGE_VERSION = "1.0.0";
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
|
||||
JNIEnv *env, jclass, jbyteArray payload, jint mode) {
|
||||
#if !DOVI_REAL_LINKED
|
||||
return nullptr;
|
||||
#else
|
||||
if (payload == nullptr) return nullptr;
|
||||
|
||||
jsize len = env->GetArrayLength(payload);
|
||||
if (len <= 0) return nullptr;
|
||||
|
||||
// Valid RPU NALs are typically <2 KiB; reject unreasonable sizes
|
||||
if (len > 8192) {
|
||||
LOGW("RPU payload too large (%d bytes), skipping", len);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Copy to native heap so libdovi never touches JVM heap memory.
|
||||
// GetByteArrayElements on ART may return a direct heap pointer; any
|
||||
// out-of-bounds access by libdovi would corrupt adjacent JVM objects.
|
||||
auto *buf = new (std::nothrow) uint8_t[static_cast<size_t>(len)];
|
||||
if (buf == nullptr) return nullptr;
|
||||
|
||||
env->GetByteArrayRegion(payload, 0, len, reinterpret_cast<jbyte *>(buf));
|
||||
if (env->ExceptionCheck()) {
|
||||
delete[] buf;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Try dovi_parse_unspec62_nalu first (handles escaped NALs), fallback to dovi_parse_rpu
|
||||
DoviRpuOpaque *rpu = dovi_parse_unspec62_nalu(buf, static_cast<size_t>(len));
|
||||
|
||||
if (rpu == nullptr) {
|
||||
delete[] buf;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *err = dovi_rpu_get_error(rpu);
|
||||
if (err != nullptr) {
|
||||
// Fallback: try dovi_parse_rpu (raw RPU without NAL framing)
|
||||
dovi_rpu_free(rpu);
|
||||
rpu = dovi_parse_rpu(buf, static_cast<size_t>(len));
|
||||
if (rpu == nullptr) {
|
||||
delete[] buf;
|
||||
return nullptr;
|
||||
}
|
||||
err = dovi_rpu_get_error(rpu);
|
||||
if (err != nullptr) {
|
||||
LOGW("RPU parse failed: %s", err);
|
||||
dovi_rpu_free(rpu);
|
||||
delete[] buf;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
delete[] buf;
|
||||
|
||||
// Convert to target profile (mode 2 = P8.1 with no-op curves)
|
||||
int32_t ret = dovi_convert_rpu_with_mode(rpu, static_cast<uint8_t>(mode));
|
||||
if (ret != 0) {
|
||||
err = dovi_rpu_get_error(rpu);
|
||||
LOGW("RPU conversion failed (mode %d): %s", mode, err ? err : "unknown");
|
||||
dovi_rpu_free(rpu);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Write back as UNSPEC62 NAL
|
||||
const DoviData *out = dovi_write_unspec62_nalu(rpu);
|
||||
if (out == nullptr || out->data == nullptr || out->len == 0) {
|
||||
err = dovi_rpu_get_error(rpu);
|
||||
LOGW("RPU write failed: %s", err ? err : "unknown");
|
||||
if (out != nullptr) dovi_data_free(out);
|
||||
dovi_rpu_free(rpu);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (out->len > 16384) {
|
||||
LOGW("RPU output unexpectedly large (%zu bytes), discarding", out->len);
|
||||
dovi_data_free(out);
|
||||
dovi_rpu_free(rpu);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jbyteArray result = env->NewByteArray(static_cast<jsize>(out->len));
|
||||
if (result != nullptr) {
|
||||
env->SetByteArrayRegion(result, 0, static_cast<jsize>(out->len),
|
||||
reinterpret_cast<const jbyte *>(out->data));
|
||||
}
|
||||
|
||||
dovi_data_free(out);
|
||||
dovi_rpu_free(rpu);
|
||||
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeIsConversionPathReady(
|
||||
JNIEnv *, jclass) {
|
||||
#if DOVI_REAL_LINKED
|
||||
return JNI_TRUE;
|
||||
#else
|
||||
return JNI_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeGetBridgeVersion(
|
||||
JNIEnv *env, jclass) {
|
||||
return env->NewStringUTF(BRIDGE_VERSION);
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef DOVI_H
|
||||
#define DOVI_H
|
||||
|
||||
|
||||
#define RPU_PARSER_MAJOR 3
|
||||
#define RPU_PARSER_MINOR 3
|
||||
#define RPU_PARSER_PATCH 2
|
||||
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define DoviNUM_COMPONENTS 3
|
||||
|
||||
/**
|
||||
* Opaque Dolby Vision RPU.
|
||||
*
|
||||
* Use dovi_rpu_free to free.
|
||||
* It should be freed regardless of whether or not an error occurred.
|
||||
*/
|
||||
typedef struct DoviRpuOpaque DoviRpuOpaque;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer
|
||||
*/
|
||||
const uint8_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviData;
|
||||
|
||||
/**
|
||||
* C struct for rpu_data_header()
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Profile guessed from the values in the header
|
||||
*/
|
||||
uint8_t guessed_profile;
|
||||
/**
|
||||
* Enhancement layer type (FEL or MEL) if the RPU is profile 7
|
||||
* null pointer if not profile 7
|
||||
*/
|
||||
const char *el_type;
|
||||
/**
|
||||
* Deprecated since 3.2.0
|
||||
* The field is not actually part of the RPU header
|
||||
*/
|
||||
uint8_t rpu_nal_prefix;
|
||||
uint8_t rpu_type;
|
||||
uint16_t rpu_format;
|
||||
uint8_t vdr_rpu_profile;
|
||||
uint8_t vdr_rpu_level;
|
||||
bool vdr_seq_info_present_flag;
|
||||
bool chroma_resampling_explicit_filter_flag;
|
||||
uint8_t coefficient_data_type;
|
||||
uint64_t coefficient_log2_denom;
|
||||
uint8_t vdr_rpu_normalized_idc;
|
||||
bool bl_video_full_range_flag;
|
||||
uint64_t bl_bit_depth_minus8;
|
||||
uint64_t el_bit_depth_minus8;
|
||||
uint64_t vdr_bit_depth_minus8;
|
||||
bool spatial_resampling_filter_flag;
|
||||
uint8_t reserved_zero_3bits;
|
||||
bool el_spatial_resampling_filter_flag;
|
||||
bool disable_residual_flag;
|
||||
bool vdr_dm_metadata_present_flag;
|
||||
bool use_prev_vdr_rpu_flag;
|
||||
uint64_t prev_vdr_rpu_id;
|
||||
} DoviRpuDataHeader;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer. Can be null if length is zero.
|
||||
*/
|
||||
const uint16_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU16Data;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer. Can be null if length is zero.
|
||||
*/
|
||||
const uint64_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU64Data;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer
|
||||
*/
|
||||
const int64_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviI64Data;
|
||||
|
||||
/**
|
||||
* Struct representing a 2D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of Data structs
|
||||
*/
|
||||
const DoviI64Data *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviI64Data2D;
|
||||
|
||||
/**
|
||||
* Struct representing a 2D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of Data structs
|
||||
*/
|
||||
const DoviU64Data *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU64Data2D;
|
||||
|
||||
typedef struct {
|
||||
DoviU64Data poly_order_minus1;
|
||||
DoviData linear_interp_flag;
|
||||
DoviI64Data2D poly_coef_int;
|
||||
DoviU64Data2D poly_coef;
|
||||
} DoviPolynomialCurve;
|
||||
|
||||
/**
|
||||
* Struct representing a 3D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of I64Data2D structs
|
||||
*/
|
||||
const DoviI64Data2D *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviI64Data3D;
|
||||
|
||||
/**
|
||||
* Struct representing a 3D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of U64Data2D structs
|
||||
*/
|
||||
const DoviU64Data2D *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU64Data3D;
|
||||
|
||||
typedef struct {
|
||||
DoviData mmr_order_minus1;
|
||||
DoviI64Data mmr_constant_int;
|
||||
DoviU64Data mmr_constant;
|
||||
DoviI64Data3D mmr_coef_int;
|
||||
DoviU64Data3D mmr_coef;
|
||||
} DoviMMRCurve;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* [2, 9]
|
||||
*/
|
||||
uint64_t num_pivots_minus2;
|
||||
DoviU16Data pivots;
|
||||
/**
|
||||
* Consistent for a component
|
||||
* Luma (component 0): Polynomial = 0
|
||||
* Chroma (components 1 and 2): MMR = 1
|
||||
*/
|
||||
uint8_t mapping_idc;
|
||||
/**
|
||||
* mapping_idc = 0, null pointer otherwise
|
||||
*/
|
||||
const DoviPolynomialCurve *polynomial;
|
||||
/**
|
||||
* mapping_idc = 1, null pointer otherwise
|
||||
*/
|
||||
const DoviMMRCurve *mmr;
|
||||
} DoviReshapingCurve;
|
||||
|
||||
/**
|
||||
* C struct for rpu_data_nlq()
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t nlq_offset[DoviNUM_COMPONENTS];
|
||||
uint64_t vdr_in_max_int[DoviNUM_COMPONENTS];
|
||||
uint64_t vdr_in_max[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_slope_int[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_slope[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_threshold_int[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_threshold[DoviNUM_COMPONENTS];
|
||||
} DoviRpuDataNlq;
|
||||
|
||||
/**
|
||||
* C struct for rpu_data_mapping()
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t vdr_rpu_id;
|
||||
uint64_t mapping_color_space;
|
||||
uint64_t mapping_chroma_format_idc;
|
||||
uint64_t num_x_partitions_minus1;
|
||||
uint64_t num_y_partitions_minus1;
|
||||
DoviReshapingCurve curves[DoviNUM_COMPONENTS];
|
||||
/**
|
||||
* Set to -1 to represent Option::None
|
||||
*/
|
||||
int32_t nlq_method_idc;
|
||||
/**
|
||||
* Set to -1 to represent Option::None
|
||||
*/
|
||||
int32_t nlq_num_pivots_minus2;
|
||||
/**
|
||||
* Length of zero when not present. Only present in profile 4 and 7.
|
||||
*/
|
||||
DoviU16Data nlq_pred_pivot_value;
|
||||
/**
|
||||
* Pointer to `RpuDataNlq` struct, null if not dual layer profile
|
||||
*/
|
||||
const DoviRpuDataNlq *nlq;
|
||||
} DoviRpuDataMapping;
|
||||
|
||||
/**
|
||||
* Statistical analysis of the frame: min, max, avg brightness.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t min_pq;
|
||||
uint16_t max_pq;
|
||||
uint16_t avg_pq;
|
||||
} DoviExtMetadataBlockLevel1;
|
||||
|
||||
/**
|
||||
* Creative intent trim passes per target display peak brightness
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t target_max_pq;
|
||||
uint16_t trim_slope;
|
||||
uint16_t trim_offset;
|
||||
uint16_t trim_power;
|
||||
uint16_t trim_chroma_weight;
|
||||
uint16_t trim_saturation_gain;
|
||||
int16_t ms_weight;
|
||||
} DoviExtMetadataBlockLevel2;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of ExtMetadataBlockLevel2 structs
|
||||
*/
|
||||
const DoviExtMetadataBlockLevel2 *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviLevel2BlockList;
|
||||
|
||||
/**
|
||||
* Level 1 offsets.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t min_pq_offset;
|
||||
uint16_t max_pq_offset;
|
||||
uint16_t avg_pq_offset;
|
||||
} DoviExtMetadataBlockLevel3;
|
||||
|
||||
/**
|
||||
* Something about temporal stability
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t anchor_pq;
|
||||
uint16_t anchor_power;
|
||||
} DoviExtMetadataBlockLevel4;
|
||||
|
||||
/**
|
||||
* Active area of the picture (letterbox, aspect ratio)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t active_area_left_offset;
|
||||
uint16_t active_area_right_offset;
|
||||
uint16_t active_area_top_offset;
|
||||
uint16_t active_area_bottom_offset;
|
||||
} DoviExtMetadataBlockLevel5;
|
||||
|
||||
/**
|
||||
* ST2086/HDR10 metadata fallback
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t max_display_mastering_luminance;
|
||||
uint16_t min_display_mastering_luminance;
|
||||
uint16_t max_content_light_level;
|
||||
uint16_t max_frame_average_light_level;
|
||||
} DoviExtMetadataBlockLevel6;
|
||||
|
||||
/**
|
||||
* Creative intent trim passes per target display peak brightness
|
||||
* For CM v4.0, L8 metadata only is present and used to compute L2
|
||||
*
|
||||
* This block can have varying byte lengths: 10, 12, 13, 19, 25
|
||||
* Depending on the length, the fields parsed default to zero and may not be set.
|
||||
* Up to (including):
|
||||
* - 10: ms_weight
|
||||
* - 12: target_mid_contrast
|
||||
* - 13: clip_trim
|
||||
* - 19: saturation_vector_field[0-5]
|
||||
* - 25: hue_vector_field[0-5]
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t length;
|
||||
uint8_t target_display_index;
|
||||
uint16_t trim_slope;
|
||||
uint16_t trim_offset;
|
||||
uint16_t trim_power;
|
||||
uint16_t trim_chroma_weight;
|
||||
uint16_t trim_saturation_gain;
|
||||
uint16_t ms_weight;
|
||||
uint16_t target_mid_contrast;
|
||||
uint16_t clip_trim;
|
||||
uint8_t saturation_vector_field0;
|
||||
uint8_t saturation_vector_field1;
|
||||
uint8_t saturation_vector_field2;
|
||||
uint8_t saturation_vector_field3;
|
||||
uint8_t saturation_vector_field4;
|
||||
uint8_t saturation_vector_field5;
|
||||
uint8_t hue_vector_field0;
|
||||
uint8_t hue_vector_field1;
|
||||
uint8_t hue_vector_field2;
|
||||
uint8_t hue_vector_field3;
|
||||
uint8_t hue_vector_field4;
|
||||
uint8_t hue_vector_field5;
|
||||
} DoviExtMetadataBlockLevel8;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of ExtMetadataBlockLevel8 structs
|
||||
*/
|
||||
const DoviExtMetadataBlockLevel8 *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviLevel8BlockList;
|
||||
|
||||
/**
|
||||
* Source/mastering display color primaries
|
||||
*
|
||||
* This block can have varying byte lengths: 1 or 17
|
||||
* Depending on the length, the fields parsed default to zero and may not be set.
|
||||
* Up to (including):
|
||||
* - 1: source_primary_index
|
||||
* - 17: source_primary_{red,green,blue,white}_{x,y}
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t length;
|
||||
uint8_t source_primary_index;
|
||||
uint16_t source_primary_red_x;
|
||||
uint16_t source_primary_red_y;
|
||||
uint16_t source_primary_green_x;
|
||||
uint16_t source_primary_green_y;
|
||||
uint16_t source_primary_blue_x;
|
||||
uint16_t source_primary_blue_y;
|
||||
uint16_t source_primary_white_x;
|
||||
uint16_t source_primary_white_y;
|
||||
} DoviExtMetadataBlockLevel9;
|
||||
|
||||
/**
|
||||
* Custom target display information
|
||||
*
|
||||
* This block can have varying byte lengths: 5 or 21
|
||||
* Depending on the length, the fields parsed default to zero and may not be set.
|
||||
* Up to (including):
|
||||
* - 5: target_primary_index
|
||||
* - 21: target_primary_{red,green,blue,white}_{x,y}
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t length;
|
||||
uint8_t target_display_index;
|
||||
uint16_t target_max_pq;
|
||||
uint16_t target_min_pq;
|
||||
uint8_t target_primary_index;
|
||||
uint16_t target_primary_red_x;
|
||||
uint16_t target_primary_red_y;
|
||||
uint16_t target_primary_green_x;
|
||||
uint16_t target_primary_green_y;
|
||||
uint16_t target_primary_blue_x;
|
||||
uint16_t target_primary_blue_y;
|
||||
uint16_t target_primary_white_x;
|
||||
uint16_t target_primary_white_y;
|
||||
} DoviExtMetadataBlockLevel10;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of ExtMetadataBlockLevel10 structs
|
||||
*/
|
||||
const DoviExtMetadataBlockLevel10 *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviLevel10BlockList;
|
||||
|
||||
/**
|
||||
* Content type metadata level
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t content_type;
|
||||
uint8_t whitepoint;
|
||||
bool reference_mode_flag;
|
||||
uint8_t reserved_byte2;
|
||||
uint8_t reserved_byte3;
|
||||
} DoviExtMetadataBlockLevel11;
|
||||
|
||||
/**
|
||||
* Metadata level present in CM v4.0
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t dm_mode;
|
||||
uint8_t dm_version_index;
|
||||
} DoviExtMetadataBlockLevel254;
|
||||
|
||||
/**
|
||||
* Metadata level optionally present in CM v2.9.
|
||||
* Different display modes (calibration/verify/bypass), debugging
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t dm_run_mode;
|
||||
uint8_t dm_run_version;
|
||||
uint8_t dm_debug0;
|
||||
uint8_t dm_debug1;
|
||||
uint8_t dm_debug2;
|
||||
uint8_t dm_debug3;
|
||||
} DoviExtMetadataBlockLevel255;
|
||||
|
||||
/**
|
||||
* C struct for the list of ext_metadata_block()
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Number of metadata blocks
|
||||
*/
|
||||
uint64_t num_ext_blocks;
|
||||
const DoviExtMetadataBlockLevel1 *level1;
|
||||
DoviLevel2BlockList level2;
|
||||
const DoviExtMetadataBlockLevel3 *level3;
|
||||
const DoviExtMetadataBlockLevel4 *level4;
|
||||
const DoviExtMetadataBlockLevel5 *level5;
|
||||
const DoviExtMetadataBlockLevel6 *level6;
|
||||
DoviLevel8BlockList level8;
|
||||
const DoviExtMetadataBlockLevel9 *level9;
|
||||
DoviLevel10BlockList level10;
|
||||
const DoviExtMetadataBlockLevel11 *level11;
|
||||
const DoviExtMetadataBlockLevel254 *level254;
|
||||
const DoviExtMetadataBlockLevel255 *level255;
|
||||
} DoviDmData;
|
||||
|
||||
/**
|
||||
* C struct for vdr_dm_data()
|
||||
*/
|
||||
typedef struct {
|
||||
bool compressed;
|
||||
uint64_t affected_dm_metadata_id;
|
||||
uint64_t current_dm_metadata_id;
|
||||
uint64_t scene_refresh_flag;
|
||||
int16_t ycc_to_rgb_coef0;
|
||||
int16_t ycc_to_rgb_coef1;
|
||||
int16_t ycc_to_rgb_coef2;
|
||||
int16_t ycc_to_rgb_coef3;
|
||||
int16_t ycc_to_rgb_coef4;
|
||||
int16_t ycc_to_rgb_coef5;
|
||||
int16_t ycc_to_rgb_coef6;
|
||||
int16_t ycc_to_rgb_coef7;
|
||||
int16_t ycc_to_rgb_coef8;
|
||||
uint32_t ycc_to_rgb_offset0;
|
||||
uint32_t ycc_to_rgb_offset1;
|
||||
uint32_t ycc_to_rgb_offset2;
|
||||
int16_t rgb_to_lms_coef0;
|
||||
int16_t rgb_to_lms_coef1;
|
||||
int16_t rgb_to_lms_coef2;
|
||||
int16_t rgb_to_lms_coef3;
|
||||
int16_t rgb_to_lms_coef4;
|
||||
int16_t rgb_to_lms_coef5;
|
||||
int16_t rgb_to_lms_coef6;
|
||||
int16_t rgb_to_lms_coef7;
|
||||
int16_t rgb_to_lms_coef8;
|
||||
uint16_t signal_eotf;
|
||||
uint16_t signal_eotf_param0;
|
||||
uint16_t signal_eotf_param1;
|
||||
uint32_t signal_eotf_param2;
|
||||
uint8_t signal_bit_depth;
|
||||
uint8_t signal_color_space;
|
||||
uint8_t signal_chroma_format;
|
||||
uint8_t signal_full_range_flag;
|
||||
uint16_t source_min_pq;
|
||||
uint16_t source_max_pq;
|
||||
uint16_t source_diagonal;
|
||||
DoviDmData dm_data;
|
||||
} DoviVdrDmData;
|
||||
|
||||
/**
|
||||
* Heap allocated list of valid RPU pointers
|
||||
*/
|
||||
typedef struct {
|
||||
DoviRpuOpaque *const *list;
|
||||
size_t len;
|
||||
const char *error;
|
||||
} DoviRpuOpaqueList;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the data must be valid.
|
||||
*
|
||||
* Parse a Dolby Vision RPU from unescaped byte buffer.
|
||||
* Adds an error if the parsing fails.
|
||||
*/
|
||||
DoviRpuOpaque *dovi_parse_rpu(const uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the data must be valid.
|
||||
*
|
||||
* Parse a Dolby Vision from a AV1 ITU-T T.35 metadata OBU byte buffer.
|
||||
* Adds an error if the parsing fails.
|
||||
*/
|
||||
DoviRpuOpaque *dovi_parse_itu_t35_dovi_metadata_obu(const uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the data must be valid.
|
||||
*
|
||||
* Parse a Dolby Vision from a (possibly) escaped HEVC UNSPEC 62 NAL unit byte buffer.
|
||||
* Adds an error if the parsing fails.
|
||||
*/
|
||||
DoviRpuOpaque *dovi_parse_unspec62_nalu(const uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
* Avoid using on opaque pointers obtained through `dovi_parse_rpu_bin_file`.
|
||||
*
|
||||
* Free the RpuOpaque
|
||||
*/
|
||||
void dovi_rpu_free(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the last logged error for the RpuOpaque operations.
|
||||
*
|
||||
* On invalid parsing, an error is added.
|
||||
* The user should manually verify if there is an error, as the parsing does not return an error code.
|
||||
*/
|
||||
const char *dovi_rpu_get_error(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The data pointer should exist, and be allocated by Rust.
|
||||
*
|
||||
* Free a Data buffer
|
||||
*/
|
||||
void dovi_data_free(const DoviData *data);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU as a byte buffer.
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_rpu(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU, escapes the bytes for HEVC and prepends the buffer with 0x7C01.
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_unspec62_nalu(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
* The mode must be between 0 and 4.
|
||||
*
|
||||
* Converts the RPU to be compatible with a different Dolby Vision profile.
|
||||
* Possible modes:
|
||||
* - 0: Don't modify the RPU
|
||||
* - 1: Converts the RPU to be MEL compatible
|
||||
* - 2: Converts the RPU to be profile 8.1 compatible. Both luma and chroma mapping curves are set to no-op.
|
||||
* This mode handles source profiles 5, 7 and 8.
|
||||
* - 3: Converts to static profile 8.4
|
||||
* - 4: Converts to profile 8.1 preserving luma and chroma mapping. Old mode 2 behaviour.
|
||||
*
|
||||
* If an error occurs, it is logged to RpuOpaque.error.
|
||||
* Returns 0 if successful, -1 otherwise.
|
||||
*/
|
||||
int32_t dovi_convert_rpu_with_mode(DoviRpuOpaque *ptr,
|
||||
uint8_t mode);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the DoVi RPU header struct.
|
||||
*/
|
||||
const DoviRpuDataHeader *dovi_rpu_get_header(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the RPU header.
|
||||
*/
|
||||
void dovi_rpu_free_header(const DoviRpuDataHeader *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the DoVi RpuDataMapping struct.
|
||||
*/
|
||||
const DoviRpuDataMapping *dovi_rpu_get_data_mapping(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the RpuDataMapping.
|
||||
*/
|
||||
void dovi_rpu_free_data_mapping(const DoviRpuDataMapping *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the DoVi VdrDmData struct.
|
||||
*/
|
||||
const DoviVdrDmData *dovi_rpu_get_vdr_dm_data(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the VdrDmData struct.
|
||||
*/
|
||||
void dovi_rpu_free_vdr_dm_data(const DoviVdrDmData *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the file path must be valid.
|
||||
*
|
||||
* Parses an existing RPU binary file.
|
||||
*
|
||||
* Returns the heap allocated `DoviRpuList` as a pointer.
|
||||
* The returned pointer may be null, or the list could be empty if an error occurred.
|
||||
*/
|
||||
const DoviRpuOpaqueList *dovi_parse_rpu_bin_file(const char *path);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the DoviRpuOpaqueList struct.
|
||||
*/
|
||||
void dovi_rpu_list_free(const DoviRpuOpaqueList *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Sets the L5 metadata active area offsets.
|
||||
* If there is no L5 block present, it is created with the offsets.
|
||||
*/
|
||||
int32_t dovi_rpu_set_active_area_offsets(DoviRpuOpaque *ptr,
|
||||
uint16_t left,
|
||||
uint16_t right,
|
||||
uint16_t top,
|
||||
uint16_t bottom);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Converts the existing reshaping/mapping to become no-op.
|
||||
*/
|
||||
int32_t dovi_rpu_remove_mapping(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU as `itu_t_t35_payload_bytes` for AV1 ITU-T T.35 metadata OBU
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_av1_rpu_metadata_obu_t35_payload(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU a complete AV1 `metadata_itut_t35()` OBU
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_av1_rpu_metadata_obu_t35_complete(DoviRpuOpaque *ptr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif /* DOVI_H */
|
||||
@@ -1,20 +1,27 @@
|
||||
package com.edde746.plezy
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.app.AppOpsManager
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.view.KeyEvent
|
||||
import android.view.TextureView
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.FileProvider
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterShellArgs
|
||||
import io.flutter.embedding.android.FlutterTextureView
|
||||
import io.flutter.embedding.android.RenderMode
|
||||
import io.flutter.embedding.android.TransparencyMode
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
@@ -27,9 +34,14 @@ import java.io.File
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val PIP_CHANNEL = "app.plezy/pip"
|
||||
private val EXTERNAL_PLAYER_CHANNEL = "app.plezy/external_player"
|
||||
private val THEME_CHANNEL = "app.plezy/theme"
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
var usingSkia = false
|
||||
}
|
||||
|
||||
private val PIP_CHANNEL = "com.plezy/pip"
|
||||
private val EXTERNAL_PLAYER_CHANNEL = "com.plezy/external_player"
|
||||
private val THEME_CHANNEL = "com.plezy/theme"
|
||||
private var watchNextPlugin: WatchNextPlugin? = null
|
||||
|
||||
// Auto PiP state
|
||||
@@ -37,6 +49,10 @@ class MainActivity : FlutterActivity() {
|
||||
private var autoPipWidth: Int = 16
|
||||
private var autoPipHeight: Int = 9
|
||||
|
||||
private fun isAndroidTvDevice(): Boolean {
|
||||
return packageManager.hasSystemFeature("android.software.leanback")
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Apply persisted theme color to the window background before anything
|
||||
// else renders. This prevents a white flash between the native splash
|
||||
@@ -108,6 +124,31 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFlutterShellArgs(): FlutterShellArgs {
|
||||
val args = super.getFlutterShellArgs()
|
||||
usingSkia = shouldDisableImpeller()
|
||||
if (usingSkia) args.add("--enable-impeller=false")
|
||||
return args
|
||||
}
|
||||
|
||||
private fun shouldDisableImpeller(): Boolean {
|
||||
// Android TV devices — weaker GPUs, less Impeller testing
|
||||
if (packageManager.hasSystemFeature("android.software.leanback")) return true
|
||||
// Google Tensor SoC (Mali GPU) — Pixel 6+
|
||||
// SOC_MODEL may return marketing name ("Tensor G2") or internal ID ("GS201")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val soc = Build.SOC_MODEL
|
||||
if (soc.startsWith("Tensor", ignoreCase = true) ||
|
||||
soc.startsWith("GS", ignoreCase = true)) return true
|
||||
}
|
||||
// NVIDIA Tegra (Shield TV)
|
||||
if (Build.MANUFACTURER.equals("NVIDIA", ignoreCase = true)) return true
|
||||
// Huawei/HONOR Kirin SoCs use Mali GPUs
|
||||
if (Build.MANUFACTURER.equals("Huawei", ignoreCase = true) ||
|
||||
Build.MANUFACTURER.equals("HONOR", ignoreCase = true)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getRenderMode(): RenderMode {
|
||||
// Use TextureView so Flutter doesn't occupy a SurfaceView layer.
|
||||
// This allows the libass subtitle SurfaceView to sit between video and Flutter UI.
|
||||
@@ -119,6 +160,40 @@ class MainActivity : FlutterActivity() {
|
||||
return TransparencyMode.transparent
|
||||
}
|
||||
|
||||
override fun onFlutterTextureViewCreated(flutterTextureView: FlutterTextureView) {
|
||||
val original = flutterTextureView.surfaceTextureListener ?: return
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var pendingResize: Runnable? = null
|
||||
var lastWidth = 0
|
||||
var lastHeight = 0
|
||||
|
||||
flutterTextureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, w: Int, h: Int) {
|
||||
original.onSurfaceTextureAvailable(surface, w, h)
|
||||
}
|
||||
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, w: Int, h: Int) {
|
||||
if (w == lastWidth && h == lastHeight) return
|
||||
lastWidth = w; lastHeight = h
|
||||
pendingResize?.let { handler.removeCallbacks(it) }
|
||||
pendingResize = Runnable {
|
||||
if (flutterTextureView.isAvailable) {
|
||||
original.onSurfaceTextureSizeChanged(surface, w, h)
|
||||
}
|
||||
}
|
||||
handler.postDelayed(pendingResize!!, 100)
|
||||
}
|
||||
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
|
||||
original.onSurfaceTextureUpdated(surface)
|
||||
}
|
||||
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
||||
pendingResize?.let { handler.removeCallbacks(it) }
|
||||
pendingResize = null
|
||||
lastWidth = 0; lastHeight = 0
|
||||
return original.onSurfaceTextureDestroyed(surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(MpvPlayerPlugin())
|
||||
@@ -177,6 +252,7 @@ class MainActivity : FlutterActivity() {
|
||||
// Splash screen theme: persist user's chosen theme for next launch (API 31+)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, THEME_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getRenderer" -> result.success(if (usingSkia) "Skia" else "Impeller")
|
||||
"setSplashTheme" -> {
|
||||
val mode = call.argument<String>("mode")
|
||||
|
||||
@@ -208,7 +284,7 @@ class MainActivity : FlutterActivity() {
|
||||
MethodChannel( flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL ).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"isSupported" -> {
|
||||
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAndroidTvDevice())
|
||||
}
|
||||
"enter" -> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@@ -216,6 +292,11 @@ class MainActivity : FlutterActivity() {
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
|
||||
if (isAndroidTvDevice()) {
|
||||
result.success(mapOf("success" to false, "errorCode" to "not_supported"))
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
|
||||
if (!isPipPermissionGranted()) {
|
||||
result.success(mapOf("success" to false, "errorCode" to "permission_disabled"))
|
||||
return@setMethodCallHandler
|
||||
@@ -224,11 +305,7 @@ class MainActivity : FlutterActivity() {
|
||||
try {
|
||||
val width = call.argument<Int>("width") ?: 16
|
||||
val height = call.argument<Int>("height") ?: 9
|
||||
val clamped = clampAspectRatio(width, height)
|
||||
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(clamped.first, clamped.second))
|
||||
.build()
|
||||
val params = buildPipParams(width, height)
|
||||
val success = enterPictureInPictureMode(params)
|
||||
if (success) {
|
||||
result.success(mapOf("success" to true))
|
||||
@@ -242,19 +319,23 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
"setAutoPipReady" -> {
|
||||
if (isAndroidTvDevice()) {
|
||||
autoPipReady = false
|
||||
result.success(true)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
|
||||
autoPipReady = call.argument<Boolean>("ready") ?: false
|
||||
autoPipWidth = call.argument<Int>("width") ?: 16
|
||||
autoPipHeight = call.argument<Int>("height") ?: 9
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
try {
|
||||
val clamped = clampAspectRatio(autoPipWidth, autoPipHeight)
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(clamped.first, clamped.second))
|
||||
.setAutoEnterEnabled(autoPipReady)
|
||||
.build()
|
||||
val params = buildPipParams(autoPipWidth, autoPipHeight, autoEnterEnabled = autoPipReady)
|
||||
setPictureInPictureParams(params)
|
||||
} catch (_: Exception) {}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to set auto-PiP params", e)
|
||||
}
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
@@ -265,18 +346,19 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
MethodChannel( flutterEngine!!.dartExecutor.binaryMessenger, PIP_CHANNEL ).invokeMethod( "onPipChanged" , isInPictureInPictureMode)
|
||||
|
||||
// Notify ExoPlayer plugin to resize video surface for PiP
|
||||
flutterEngine?.plugins?.get(ExoPlayerPlugin::class.java)?.let { plugin ->
|
||||
(plugin as? ExoPlayerPlugin)?.onPipModeChanged(isInPictureInPictureMode)
|
||||
flutterEngine?.let { engine ->
|
||||
MethodChannel(engine.dartExecutor.binaryMessenger, PIP_CHANNEL).invokeMethod("onPipChanged", isInPictureInPictureMode)
|
||||
engine.plugins.get(ExoPlayerPlugin::class.java)?.let { plugin ->
|
||||
(plugin as? ExoPlayerPlugin)?.onPipModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
// Auto PiP for API 26-30 (API 31+ uses setAutoEnterEnabled)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
if (!isAndroidTvDevice() &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
|
||||
autoPipReady && isPipPermissionGranted()) {
|
||||
try {
|
||||
@@ -284,12 +366,11 @@ class MainActivity : FlutterActivity() {
|
||||
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
|
||||
MethodChannel(messenger, PIP_CHANNEL).invokeMethod("onAutoPipEntering", null)
|
||||
}
|
||||
val clamped = clampAspectRatio(autoPipWidth, autoPipHeight)
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(clamped.first, clamped.second))
|
||||
.build()
|
||||
val params = buildPipParams(autoPipWidth, autoPipHeight)
|
||||
enterPictureInPictureMode(params)
|
||||
} catch (_: Exception) {}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to enter auto-PiP", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,12 +383,22 @@ class MainActivity : FlutterActivity() {
|
||||
) == AppOpsManager.MODE_ALLOWED
|
||||
}
|
||||
|
||||
private fun clampAspectRatio(width: Int, height: Int): Pair<Int, Int> {
|
||||
val ratio = width.toFloat() / height.toFloat()
|
||||
return when {
|
||||
ratio < 0.42f -> Pair(5, 12)
|
||||
ratio > 2.39f -> Pair(12, 5)
|
||||
else -> Pair(width, height)
|
||||
private fun buildPipParams(width: Int, height: Int, autoEnterEnabled: Boolean? = null): PictureInPictureParams {
|
||||
val (w, h) = if (width <= 0 || height <= 0) {
|
||||
Pair(16, 9)
|
||||
} else {
|
||||
val ratio = width.toFloat() / height.toFloat()
|
||||
when {
|
||||
ratio < 1f / 2.39f -> Pair(100, 239)
|
||||
ratio > 2.39f -> Pair(239, 100)
|
||||
else -> Pair(width, height)
|
||||
}
|
||||
}
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(w, h))
|
||||
if (autoEnterEnabled != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(autoEnterEnabled)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.extractor.Extractor
|
||||
import androidx.media3.extractor.ExtractorInput
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.PositionHolder
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.SeekPoint
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
|
||||
/**
|
||||
* Extractor wrapper that enables approximate seeking for MKV files without Cues.
|
||||
*
|
||||
* When the underlying extractor reports an [SeekMap.Unseekable] seek map (i.e. the MKV
|
||||
* has no Cues element), this wrapper replaces it with a proportional byte-position
|
||||
* estimate and scans for the nearest Cluster boundary after seeking.
|
||||
*/
|
||||
@androidx.media3.common.util.UnstableApi
|
||||
class CuelessSeekExtractorWrapper(
|
||||
private val delegate: Extractor,
|
||||
) : Extractor {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CuelessSeek"
|
||||
// MKV Cluster element ID: 0x1F43B675 (4-byte EBML Class-D ID)
|
||||
private val CLUSTER_ID = byteArrayOf(0x1F, 0x43, 0xB6.toByte(), 0x75)
|
||||
private const val SCAN_BUFFER_SIZE = 8192
|
||||
// Max bytes to scan for a Cluster boundary before giving up
|
||||
private const val MAX_SCAN_BYTES = 1024 * 1024 // 1 MB
|
||||
}
|
||||
|
||||
private var inputLength: Long = C.LENGTH_UNSET.toLong()
|
||||
private var needsClusterResync = false
|
||||
private var isApproximateSeeking = false
|
||||
private var pendingSeekTimeUs: Long = C.TIME_UNSET
|
||||
|
||||
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
|
||||
|
||||
override fun init(output: ExtractorOutput) {
|
||||
delegate.init(SeekInterceptingOutput(output))
|
||||
}
|
||||
|
||||
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int {
|
||||
if (inputLength == C.LENGTH_UNSET.toLong()) {
|
||||
inputLength = input.length
|
||||
}
|
||||
if (needsClusterResync) {
|
||||
needsClusterResync = false
|
||||
return scanForCluster(input, seekPosition)
|
||||
}
|
||||
return delegate.read(input, seekPosition)
|
||||
}
|
||||
|
||||
override fun seek(position: Long, timeUs: Long) {
|
||||
if (isApproximateSeeking && position > 0) {
|
||||
needsClusterResync = true
|
||||
pendingSeekTimeUs = timeUs
|
||||
}
|
||||
delegate.seek(position, timeUs)
|
||||
}
|
||||
|
||||
override fun release() = delegate.release()
|
||||
|
||||
/**
|
||||
* Scan forward from the current input position to find the next MKV Cluster
|
||||
* element ID (0x1F43B675). Returns [Extractor.RESULT_SEEK] with the Cluster's
|
||||
* byte position so ExoPlayer repositions the DataSource there.
|
||||
*/
|
||||
private fun scanForCluster(input: ExtractorInput, seekPosition: PositionHolder): Int {
|
||||
val buffer = ByteArray(SCAN_BUFFER_SIZE)
|
||||
var totalScanned = 0L
|
||||
// Carry over last 3 bytes across buffer boundaries to detect split IDs
|
||||
var carry = ByteArray(0)
|
||||
|
||||
while (totalScanned < MAX_SCAN_BYTES) {
|
||||
val toRead = minOf(SCAN_BUFFER_SIZE, (MAX_SCAN_BYTES - totalScanned).toInt())
|
||||
val bytesRead: Int
|
||||
try {
|
||||
bytesRead = input.read(buffer, 0, toRead)
|
||||
} catch (_: Exception) {
|
||||
break
|
||||
}
|
||||
if (bytesRead == C.RESULT_END_OF_INPUT) break
|
||||
|
||||
// Combine carry + new data for scanning
|
||||
val scanData = if (carry.isNotEmpty()) carry + buffer.copyOf(bytesRead) else buffer.copyOf(bytesRead)
|
||||
|
||||
for (i in 0..scanData.size - 4) {
|
||||
if (scanData[i] == CLUSTER_ID[0] &&
|
||||
scanData[i + 1] == CLUSTER_ID[1] &&
|
||||
scanData[i + 2] == CLUSTER_ID[2] &&
|
||||
scanData[i + 3] == CLUSTER_ID[3]
|
||||
) {
|
||||
// Compute the absolute byte position of this Cluster
|
||||
val clusterPosition = input.position - bytesRead - carry.size + i
|
||||
Log.d(TAG, "Found Cluster at byte $clusterPosition (scanned ${totalScanned + i} bytes)")
|
||||
seekPosition.position = clusterPosition
|
||||
delegate.seek(clusterPosition, pendingSeekTimeUs)
|
||||
pendingSeekTimeUs = C.TIME_UNSET
|
||||
return Extractor.RESULT_SEEK
|
||||
}
|
||||
}
|
||||
|
||||
// Keep last 3 bytes as carry for next iteration
|
||||
carry = if (scanData.size >= 3) scanData.copyOfRange(scanData.size - 3, scanData.size) else scanData.copyOf()
|
||||
totalScanned += bytesRead
|
||||
}
|
||||
|
||||
// Failed to find a Cluster — fall back to position 0
|
||||
Log.w(TAG, "No Cluster found after scanning $totalScanned bytes, resetting to start")
|
||||
seekPosition.position = 0
|
||||
delegate.seek(0, 0)
|
||||
return Extractor.RESULT_SEEK
|
||||
}
|
||||
|
||||
/**
|
||||
* ExtractorOutput wrapper that intercepts [seekMap] calls to replace
|
||||
* [SeekMap.Unseekable] with an approximate proportional SeekMap.
|
||||
*/
|
||||
private inner class SeekInterceptingOutput(
|
||||
private val delegate: ExtractorOutput,
|
||||
) : ExtractorOutput {
|
||||
|
||||
override fun track(id: Int, type: Int): TrackOutput = delegate.track(id, type)
|
||||
override fun endTracks() = delegate.endTracks()
|
||||
|
||||
override fun seekMap(seekMap: SeekMap) {
|
||||
if (seekMap is SeekMap.Unseekable) {
|
||||
val durationUs = seekMap.durationUs
|
||||
if (durationUs != C.TIME_UNSET && durationUs > 0) {
|
||||
Log.i(TAG, "Replacing Unseekable with approximate SeekMap (duration=${durationUs / 1_000_000}s)")
|
||||
isApproximateSeeking = true
|
||||
delegate.seekMap(ApproximateSeekMap(durationUs))
|
||||
return
|
||||
}
|
||||
}
|
||||
// File has real Cues or unknown duration — pass through
|
||||
isApproximateSeeking = false
|
||||
delegate.seekMap(seekMap)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate SeekMap that estimates byte positions proportionally.
|
||||
* Used when the MKV has no Cues but has a known duration.
|
||||
*/
|
||||
private inner class ApproximateSeekMap(
|
||||
private val durationUs: Long,
|
||||
) : SeekMap {
|
||||
|
||||
override fun isSeekable(): Boolean = true
|
||||
|
||||
override fun getDurationUs(): Long = durationUs
|
||||
|
||||
override fun getSeekPoints(timeUs: Long): SeekMap.SeekPoints {
|
||||
val length = inputLength
|
||||
if (length == C.LENGTH_UNSET.toLong() || durationUs <= 0) {
|
||||
return SeekMap.SeekPoints(SeekPoint(0, 0))
|
||||
}
|
||||
val clampedTimeUs = timeUs.coerceIn(0, durationUs)
|
||||
val position = (clampedTimeUs.toDouble() / durationUs * length).toLong().coerceIn(0, length)
|
||||
return SeekMap.SeekPoints(SeekPoint(clampedTimeUs, position))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecList
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
|
||||
enum class DvConversionMode { DISABLED, DV81, HEVC_STRIP }
|
||||
|
||||
object DoviBridge {
|
||||
private const val TAG = "DoviBridge"
|
||||
|
||||
private val nativeLoaded: Boolean by lazy {
|
||||
try {
|
||||
System.loadLibrary("dovi_bridge")
|
||||
true
|
||||
} catch (_: UnsatisfiedLinkError) {
|
||||
Log.w(TAG, "Native lib not found")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isAvailable(): Boolean = nativeLoaded &&
|
||||
runCatching { nativeIsConversionPathReady() }.getOrDefault(false)
|
||||
|
||||
private fun deviceSupportsDvProfile(profile: Int, minApi: Int = 0): Boolean {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < minApi) return false
|
||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
return codecList.codecInfos.any { info ->
|
||||
!info.isEncoder && info.supportedTypes.any { type ->
|
||||
type.equals("video/dolby-vision", ignoreCase = true) &&
|
||||
info.getCapabilitiesForType(type).profileLevels.any { it.profile == profile }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to query DV profile $profile support", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val deviceSupportsDvProfile7: Boolean by lazy {
|
||||
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr)
|
||||
.also { Log.i(TAG, "Device DV Profile 7 support: $it") }
|
||||
}
|
||||
|
||||
val deviceSupportsDvProfile8: Boolean by lazy {
|
||||
deviceSupportsDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, minApi = 27)
|
||||
.also { Log.i(TAG, "Device DV Profile 8 support: $it") }
|
||||
}
|
||||
|
||||
fun getConversionMode(): DvConversionMode = when {
|
||||
!isAvailable() -> DvConversionMode.DISABLED
|
||||
deviceSupportsDvProfile7 -> DvConversionMode.DISABLED // try native first; ExoPlayerCore retries with conversion on failure
|
||||
deviceSupportsDvProfile8 -> DvConversionMode.DV81
|
||||
else -> DvConversionMode.HEVC_STRIP
|
||||
}
|
||||
|
||||
/** Get the fallback mode when native DV7 decoding fails. */
|
||||
fun getDv7FallbackMode(): DvConversionMode = when {
|
||||
deviceSupportsDvProfile8 -> DvConversionMode.DV81
|
||||
else -> DvConversionMode.HEVC_STRIP
|
||||
}
|
||||
|
||||
fun convertRpuNalu(payload: ByteArray, mode: Int = 2): ByteArray? {
|
||||
if (!isAvailable() || payload.isEmpty()) return null
|
||||
return runCatching { nativeConvertDv7RpuToDv81(payload, mode) }
|
||||
.onFailure { Log.w(TAG, "RPU conversion failed: ${it.message}") }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
fun getVersion(): String? {
|
||||
if (!nativeLoaded) return null
|
||||
return runCatching { nativeGetBridgeVersion() }.getOrNull()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeConvertDv7RpuToDv81(payload: ByteArray, mode: Int): ByteArray?
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeIsConversionPathReady(): Boolean
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeGetBridgeVersion(): String
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.common.DataReader
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.ParsableByteArray
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
|
||||
/**
|
||||
* TrackOutput wrapper that processes DV Profile 7 HEVC samples based on conversion mode:
|
||||
*
|
||||
* - DV81: Convert RPU NALs via libdovi to Profile 8.1, present as video/dolby-vision
|
||||
* with dvhe.08.XX codec string. Preserves dynamic tone mapping metadata.
|
||||
* - HEVC_STRIP: Strip all DV enhancement layers, present as plain video/hevc.
|
||||
*
|
||||
* Two modes of NAL framing (auto-detected):
|
||||
* - Annex B (MKV path): MatroskaExtractor outputs 00 00 00 01 start codes
|
||||
* - Length-prefixed (MP4 path): Mp4Extractor outputs 4-byte big-endian lengths
|
||||
*
|
||||
* NAL processing:
|
||||
* - Type 62 (UNSPEC62): DV RPU → convert (DV81) or strip (HEVC_STRIP)
|
||||
* - Type 63 (UNSPEC63): DV Enhancement Layer → strip
|
||||
* - nuh_layer_id > 0: Enhancement layer NAL → strip
|
||||
* - All retained NALs: normalize nuh_layer_id to 0
|
||||
*
|
||||
* All buffers are reused across samples to minimize GC pressure on the hot path.
|
||||
*/
|
||||
class DoviConvertingTrackOutput(
|
||||
private val delegate: TrackOutput,
|
||||
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP,
|
||||
) : TrackOutput {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DoviConvertTrack"
|
||||
private const val NAL_TYPE_UNSPEC62 = 62
|
||||
private const val NAL_TYPE_UNSPEC63 = 63
|
||||
private const val LIBDOVI_MODE_TO_81 = 2
|
||||
private const val INITIAL_BUFFER_SIZE = 256 * 1024
|
||||
private const val READ_CHUNK = 64 * 1024
|
||||
private val ANNEX_B_START_CODE = byteArrayOf(0, 0, 0, 1)
|
||||
}
|
||||
|
||||
var conversionActive = false
|
||||
private set
|
||||
var strippedNalCount = 0L
|
||||
private set
|
||||
var convertedRpuCount = 0L
|
||||
private set
|
||||
|
||||
// Reusable buffers — grown as needed, never shrunk
|
||||
private var sampleBuf = ByteArray(INITIAL_BUFFER_SIZE)
|
||||
private var sampleLen = 0
|
||||
private var outputBuf = ByteArray(INITIAL_BUFFER_SIZE)
|
||||
private var outputLen = 0
|
||||
private var readBuf = ByteArray(READ_CHUNK)
|
||||
private val outputParsable = ParsableByteArray()
|
||||
private var buffering = false
|
||||
|
||||
// Sample counter for periodic logging
|
||||
private var sampleCount = 0L
|
||||
|
||||
override fun format(format: Format) {
|
||||
if (!conversionActive) {
|
||||
val codecs = format.codecs
|
||||
if (codecs != null && codecs.startsWith("dvhe.07")) {
|
||||
conversionActive = true
|
||||
Log.i(TAG, "DV Profile 7 detected ($codecs), mode=$dvMode")
|
||||
Log.i(TAG, "Original format: mime=${format.sampleMimeType}, codecs=$codecs, " +
|
||||
"initData=${format.initializationData.size} entries " +
|
||||
"(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})")
|
||||
|
||||
val newFormat = when (dvMode) {
|
||||
DvConversionMode.DV81 -> {
|
||||
// Parse DV level from codec string: "dvhe.07.06" → 6
|
||||
val level = codecs.split('.').getOrNull(2)?.toIntOrNull() ?: 6
|
||||
val newCodecs = "dvhe.08.%02d".format(level)
|
||||
val dvConfigRecord = buildDv81ConfigRecord(level)
|
||||
Log.i(TAG, "DV81: rewriting to $newCodecs, config=${dvConfigRecord.size}B")
|
||||
|
||||
format.buildUpon()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
|
||||
.setCodecs(newCodecs)
|
||||
.setInitializationData(
|
||||
if (format.initializationData.isNotEmpty())
|
||||
listOf(format.initializationData[0], dvConfigRecord)
|
||||
else
|
||||
listOf(ByteArray(0), dvConfigRecord)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
else -> {
|
||||
// HEVC_STRIP: present as plain HEVC
|
||||
Log.i(TAG, "HEVC_STRIP: rewriting to video/hevc")
|
||||
format.buildUpon()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_H265)
|
||||
.setCodecs(null)
|
||||
.setInitializationData(
|
||||
if (format.initializationData.isNotEmpty())
|
||||
listOf(format.initializationData[0])
|
||||
else
|
||||
emptyList()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Rewritten format: mime=${newFormat.sampleMimeType}, " +
|
||||
"codecs=${newFormat.codecs}, initData=${newFormat.initializationData.size} entries")
|
||||
delegate.format(newFormat)
|
||||
return
|
||||
}
|
||||
}
|
||||
delegate.format(format)
|
||||
}
|
||||
|
||||
override fun sampleData(
|
||||
input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int
|
||||
): Int {
|
||||
if (!conversionActive) {
|
||||
return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
|
||||
}
|
||||
|
||||
buffering = true
|
||||
if (readBuf.size < length) readBuf = ByteArray(length)
|
||||
val bytesRead = input.read(readBuf, 0, length)
|
||||
if (bytesRead > 0) {
|
||||
ensureSampleCapacity(sampleLen + bytesRead)
|
||||
System.arraycopy(readBuf, 0, sampleBuf, sampleLen, bytesRead)
|
||||
sampleLen += bytesRead
|
||||
}
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
|
||||
if (!conversionActive) {
|
||||
delegate.sampleData(data, length, sampleDataPart)
|
||||
return
|
||||
}
|
||||
|
||||
buffering = true
|
||||
ensureSampleCapacity(sampleLen + length)
|
||||
data.readBytes(sampleBuf, sampleLen, length)
|
||||
sampleLen += length
|
||||
}
|
||||
|
||||
override fun sampleMetadata(
|
||||
timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?
|
||||
) {
|
||||
if (!conversionActive || !buffering) {
|
||||
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
|
||||
return
|
||||
}
|
||||
|
||||
buffering = false
|
||||
val srcLen = sampleLen
|
||||
sampleLen = 0
|
||||
|
||||
val outLen: Int
|
||||
val outBuf: ByteArray
|
||||
val success = try {
|
||||
processNalUnits(srcLen)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "NAL processing failed, passing raw sample", e)
|
||||
false
|
||||
}
|
||||
if (success) {
|
||||
outLen = outputLen
|
||||
outBuf = outputBuf
|
||||
} else {
|
||||
outLen = srcLen
|
||||
outBuf = sampleBuf
|
||||
}
|
||||
|
||||
// Skip empty samples (all NALs were DV layers) — don't confuse the decoder
|
||||
if (outLen == 0) return
|
||||
|
||||
outputParsable.reset(outBuf, outLen)
|
||||
delegate.sampleData(outputParsable, outLen, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
delegate.sampleMetadata(timeUs, flags, outLen, 0, cryptoData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process NAL units in sampleBuf[0..dataLen). Auto-detects format:
|
||||
* - Annex B (00 00 00 01 / 00 00 01 start codes) — used by MatroskaExtractor
|
||||
* - Length-prefixed (4-byte big-endian length) — used by Mp4Extractor
|
||||
*
|
||||
* Result is written to outputBuf[0..outputLen).
|
||||
*/
|
||||
private fun processNalUnits(dataLen: Int) {
|
||||
outputLen = 0
|
||||
if (dataLen < 4) {
|
||||
ensureOutputCapacity(dataLen)
|
||||
System.arraycopy(sampleBuf, 0, outputBuf, 0, dataLen)
|
||||
outputLen = dataLen
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-detect: Annex B starts with 00 00 00 01 or 00 00 01
|
||||
val isAnnexB = (dataLen >= 4 && sampleBuf[0] == 0.toByte() && sampleBuf[1] == 0.toByte() &&
|
||||
sampleBuf[2] == 0.toByte() && sampleBuf[3] == 1.toByte()) ||
|
||||
(dataLen >= 3 && sampleBuf[0] == 0.toByte() && sampleBuf[1] == 0.toByte() &&
|
||||
sampleBuf[2] == 1.toByte())
|
||||
|
||||
if (sampleCount == 0L) {
|
||||
Log.d(TAG, "NAL format detected: ${if (isAnnexB) "Annex B" else "length-prefixed"}, " +
|
||||
"first bytes: ${sampleBuf.take(8).joinToString(" ") { "%02X".format(it) }}")
|
||||
}
|
||||
|
||||
if (isAnnexB) processAnnexBNals(dataLen) else processLengthPrefixedNals(dataLen)
|
||||
}
|
||||
|
||||
/** Process Annex B formatted NAL units (MKV path). Scans inline, no list allocation. */
|
||||
private fun processAnnexBNals(dataLen: Int) {
|
||||
ensureOutputCapacity(dataLen)
|
||||
var kept = 0
|
||||
var stripped = 0
|
||||
|
||||
// Find first start code
|
||||
var scEnd = -1
|
||||
var i = 0
|
||||
while (i < dataLen - 2) {
|
||||
if (sampleBuf[i] == 0.toByte() && sampleBuf[i + 1] == 0.toByte()) {
|
||||
if (i + 3 < dataLen && sampleBuf[i + 2] == 0.toByte() && sampleBuf[i + 3] == 1.toByte()) {
|
||||
scEnd = i + 4
|
||||
break
|
||||
} else if (sampleBuf[i + 2] == 1.toByte()) {
|
||||
scEnd = i + 3
|
||||
break
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if (scEnd < 0) {
|
||||
// No start codes found — pass through
|
||||
System.arraycopy(sampleBuf, 0, outputBuf, 0, dataLen)
|
||||
outputLen = dataLen
|
||||
sampleCount++
|
||||
return
|
||||
}
|
||||
|
||||
var nalStart = scEnd
|
||||
|
||||
while (nalStart < dataLen) {
|
||||
// Find next start code to determine end of current NAL
|
||||
var nalEnd = dataLen
|
||||
i = nalStart
|
||||
while (i < dataLen - 2) {
|
||||
if (sampleBuf[i] == 0.toByte() && sampleBuf[i + 1] == 0.toByte()) {
|
||||
if (i + 3 < dataLen && sampleBuf[i + 2] == 0.toByte() && sampleBuf[i + 3] == 1.toByte()) {
|
||||
nalEnd = i
|
||||
break
|
||||
} else if (sampleBuf[i + 2] == 1.toByte()) {
|
||||
nalEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
val nalLen = nalEnd - nalStart
|
||||
if (nalLen > 0) {
|
||||
val action = processNalInline(nalStart, nalLen)
|
||||
if (action == NalAction.KEEP) {
|
||||
ensureOutputCapacity(outputLen + 4 + nalLen)
|
||||
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
|
||||
outputLen += 4
|
||||
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
|
||||
normalizeLayerId(outputBuf, outputLen)
|
||||
outputLen += nalLen
|
||||
kept++
|
||||
} else if (action == NalAction.CONVERT) {
|
||||
val converted = DoviBridge.convertRpuNalu(
|
||||
sampleBuf.copyOfRange(nalStart, nalStart + nalLen), LIBDOVI_MODE_TO_81
|
||||
)
|
||||
if (converted != null) {
|
||||
normalizeLayerId(converted, 0)
|
||||
ensureOutputCapacity(outputLen + 4 + converted.size)
|
||||
System.arraycopy(ANNEX_B_START_CODE, 0, outputBuf, outputLen, 4)
|
||||
outputLen += 4
|
||||
System.arraycopy(converted, 0, outputBuf, outputLen, converted.size)
|
||||
outputLen += converted.size
|
||||
convertedRpuCount++
|
||||
kept++
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
}
|
||||
|
||||
// Advance past the next start code
|
||||
if (nalEnd >= dataLen) break
|
||||
nalStart = if (nalEnd + 3 < dataLen && sampleBuf[nalEnd + 2] == 0.toByte() && sampleBuf[nalEnd + 3] == 1.toByte()) {
|
||||
nalEnd + 4
|
||||
} else {
|
||||
nalEnd + 3
|
||||
}
|
||||
}
|
||||
|
||||
sampleCount++
|
||||
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
|
||||
Log.d(TAG, "Sample #$sampleCount (AnnexB): ${dataLen}B -> ${outputLen}B, " +
|
||||
"kept=$kept stripped=$stripped NALs")
|
||||
}
|
||||
}
|
||||
|
||||
/** Process length-prefixed NAL units (MP4 path). */
|
||||
private fun processLengthPrefixedNals(dataLen: Int) {
|
||||
ensureOutputCapacity(dataLen)
|
||||
var pos = 0
|
||||
var kept = 0
|
||||
var stripped = 0
|
||||
|
||||
while (pos + 4 <= dataLen) {
|
||||
val nalLen = ((sampleBuf[pos].toInt() and 0xFF) shl 24) or
|
||||
((sampleBuf[pos + 1].toInt() and 0xFF) shl 16) or
|
||||
((sampleBuf[pos + 2].toInt() and 0xFF) shl 8) or
|
||||
(sampleBuf[pos + 3].toInt() and 0xFF)
|
||||
|
||||
if (nalLen <= 0 || pos + 4 + nalLen > dataLen) {
|
||||
if (sampleCount < 5) {
|
||||
Log.w(TAG, "Bad NAL length $nalLen at pos $pos (data.size=$dataLen)")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
val nalStart = pos + 4
|
||||
val action = processNalInline(nalStart, nalLen)
|
||||
if (action == NalAction.KEEP) {
|
||||
ensureOutputCapacity(outputLen + 4 + nalLen)
|
||||
writeInt32BE(outputBuf, outputLen, nalLen)
|
||||
outputLen += 4
|
||||
System.arraycopy(sampleBuf, nalStart, outputBuf, outputLen, nalLen)
|
||||
normalizeLayerId(outputBuf, outputLen)
|
||||
outputLen += nalLen
|
||||
kept++
|
||||
} else if (action == NalAction.CONVERT) {
|
||||
val converted = DoviBridge.convertRpuNalu(
|
||||
sampleBuf.copyOfRange(nalStart, nalStart + nalLen), LIBDOVI_MODE_TO_81
|
||||
)
|
||||
if (converted != null) {
|
||||
normalizeLayerId(converted, 0)
|
||||
ensureOutputCapacity(outputLen + 4 + converted.size)
|
||||
writeInt32BE(outputBuf, outputLen, converted.size)
|
||||
outputLen += 4
|
||||
System.arraycopy(converted, 0, outputBuf, outputLen, converted.size)
|
||||
outputLen += converted.size
|
||||
convertedRpuCount++
|
||||
kept++
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
|
||||
pos += 4 + nalLen
|
||||
}
|
||||
|
||||
sampleCount++
|
||||
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
|
||||
Log.d(TAG, "Sample #$sampleCount (LenPrefix): ${dataLen}B -> ${outputLen}B, " +
|
||||
"kept=$kept stripped=$stripped NALs")
|
||||
}
|
||||
}
|
||||
|
||||
private enum class NalAction { KEEP, STRIP, CONVERT }
|
||||
|
||||
/** Classify a NAL at sampleBuf[offset..offset+len) without copying. */
|
||||
private fun processNalInline(offset: Int, len: Int): NalAction {
|
||||
if (len < 2) return NalAction.KEEP
|
||||
val nalType = (sampleBuf[offset].toInt() ushr 1) and 0x3F
|
||||
val nuhLayerId = ((sampleBuf[offset].toInt() and 1) shl 5) or
|
||||
((sampleBuf[offset + 1].toInt() ushr 3) and 0x1F)
|
||||
return when {
|
||||
nalType == NAL_TYPE_UNSPEC62 && dvMode == DvConversionMode.DV81 -> NalAction.CONVERT
|
||||
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> NalAction.STRIP
|
||||
else -> NalAction.KEEP
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeLayerId(data: ByteArray, offset: Int) {
|
||||
if (data.size - offset >= 2) {
|
||||
data[offset] = (data[offset].toInt() and 0xFE).toByte()
|
||||
data[offset + 1] = (data[offset + 1].toInt() and 0x07).toByte()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeInt32BE(buf: ByteArray, offset: Int, value: Int) {
|
||||
buf[offset] = ((value ushr 24) and 0xFF).toByte()
|
||||
buf[offset + 1] = ((value ushr 16) and 0xFF).toByte()
|
||||
buf[offset + 2] = ((value ushr 8) and 0xFF).toByte()
|
||||
buf[offset + 3] = (value and 0xFF).toByte()
|
||||
}
|
||||
|
||||
private fun ensureSampleCapacity(needed: Int) {
|
||||
if (sampleBuf.size < needed) {
|
||||
sampleBuf = sampleBuf.copyOf(maxOf(needed, sampleBuf.size * 2))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureOutputCapacity(needed: Int) {
|
||||
if (outputBuf.size < needed) {
|
||||
outputBuf = outputBuf.copyOf(maxOf(needed, outputBuf.size * 2))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a 24-byte DOVIDecoderConfigurationRecord for DV Profile 8.1.
|
||||
*
|
||||
* Binary layout (from Dolby Vision spec):
|
||||
* byte[0]: dv_version_major = 1
|
||||
* byte[1]: dv_version_minor = 0
|
||||
* byte[2]: dv_profile (7 bits) | dv_level MSB (1 bit)
|
||||
* byte[3]: dv_level low 5 bits (5 bits) | rpu_present (1) | el_present (1) | bl_present (1)
|
||||
* byte[4]: bl_compatibility_id (4 bits) | md_compression (2 bits) | reserved (2 bits)
|
||||
* byte[5-23]: reserved (zeros)
|
||||
*/
|
||||
private fun buildDv81ConfigRecord(level: Int): ByteArray {
|
||||
val record = ByteArray(24)
|
||||
record[0] = 0x01 // dv_version_major = 1
|
||||
record[1] = 0x00 // dv_version_minor = 0
|
||||
record[2] = ((8 shl 1) or ((level ushr 5) and 0x01)).toByte() // profile=8 | level MSB
|
||||
record[3] = (((level and 0x1F) shl 3) or 0x05).toByte() // level low 5 | rpu=1 el=0 bl=1
|
||||
record[4] = (1 shl 4).toByte() // bl_compatibility_id=1 (HDR10)
|
||||
// bytes 5-23 remain 0 (reserved)
|
||||
return record
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.extractor.Extractor
|
||||
import androidx.media3.extractor.ExtractorInput
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.PositionHolder
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
|
||||
/**
|
||||
* ExtractorOutput wrapper that intercepts video track creation
|
||||
* to insert DoviConvertingTrackOutput for DV processing.
|
||||
* Shared by DoviExtractorWrapper for both MP4 and MKV containers.
|
||||
*/
|
||||
class DoviExtractorOutputWrapper(
|
||||
private val delegate: ExtractorOutput,
|
||||
private val dvMode: DvConversionMode,
|
||||
private val onVideoTrackWrapped: (DoviConvertingTrackOutput) -> Unit,
|
||||
) : ExtractorOutput {
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = delegate.track(id, type)
|
||||
if (type == C.TRACK_TYPE_VIDEO) {
|
||||
val wrapper = DoviConvertingTrackOutput(original, dvMode)
|
||||
onVideoTrackWrapped(wrapper)
|
||||
return wrapper
|
||||
}
|
||||
return original
|
||||
}
|
||||
|
||||
override fun endTracks() = delegate.endTracks()
|
||||
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor decorator for Mp4/FragmentedMp4 containers.
|
||||
* Wraps the video TrackOutput with DoviConvertingTrackOutput to perform
|
||||
* DV Profile 7 → 8.1 conversion via inline NAL processing.
|
||||
*/
|
||||
class DoviExtractorWrapper(
|
||||
private val delegate: Extractor,
|
||||
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP,
|
||||
) : Extractor {
|
||||
|
||||
@Volatile var doviTrackOutput: DoviConvertingTrackOutput? = null
|
||||
private set
|
||||
|
||||
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
|
||||
|
||||
override fun init(output: ExtractorOutput) {
|
||||
delegate.init(DoviExtractorOutputWrapper(output, dvMode) { doviTrackOutput = it })
|
||||
}
|
||||
|
||||
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int =
|
||||
delegate.read(input, seekPosition)
|
||||
|
||||
override fun seek(position: Long, timeUs: Long) = delegate.seek(position, timeUs)
|
||||
|
||||
override fun release() = delegate.release()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,13 @@ package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.edde746.plezy.mpv.MpvPlayerCore
|
||||
import com.edde746.plezy.mpv.MpvPlayerDelegate
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
@@ -15,7 +17,7 @@ import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
EventChannel.StreamHandler, ActivityAware, ExoPlayerDelegate, MpvPlayerDelegate {
|
||||
EventChannel.StreamHandler, ActivityAware, ExoPlayerDelegate {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ExoPlayerPlugin"
|
||||
@@ -33,9 +35,12 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private var activity: Activity? = null
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
private val nameToId = mutableMapOf<String, Int>()
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var configuredBufferSizeBytes: Int? = null
|
||||
private var configuredTunnelingEnabled: Boolean = true
|
||||
|
||||
private var sessionGeneration = 0
|
||||
private var debugLoggingEnabled: Boolean = false
|
||||
private val pendingMpvProperties = mutableListOf<Pair<String, String>>()
|
||||
|
||||
// FlutterPlugin
|
||||
|
||||
@@ -64,6 +69,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
sessionGeneration++
|
||||
playerCore?.dispose()
|
||||
playerCore = null
|
||||
mpvCore?.dispose()
|
||||
@@ -82,6 +88,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
sessionGeneration++
|
||||
fallbackInProgress = false
|
||||
activity = null
|
||||
activityBinding = null
|
||||
Log.d(TAG, "Detached from activity for config changes")
|
||||
@@ -116,6 +124,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
"selectSubtitleTrack" -> handleSelectSubtitleTrack(call, result)
|
||||
"addSubtitleTrack" -> handleAddSubtitleTrack(call, result)
|
||||
"setVisible" -> handleSetVisible(call, result)
|
||||
"updateFrame" -> handleUpdateFrame(result)
|
||||
"setVideoFrameRate" -> handleSetVideoFrameRate(call, result)
|
||||
"clearVideoFrameRate" -> handleClearVideoFrameRate(result)
|
||||
"requestAudioFocus" -> handleRequestAudioFocus(result)
|
||||
@@ -132,12 +141,17 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
|
||||
"observeProperty" -> handleObserveProperty(call, result)
|
||||
"setMpvProperty" -> handleSetMpvProperty(call, result)
|
||||
"setLogLevel" -> {
|
||||
val level = call.argument<String>("level") ?: "warn"
|
||||
debugLoggingEnabled = (level == "v" || level == "debug" || level == "trace")
|
||||
playerCore?.debugLoggingEnabled = debugLoggingEnabled
|
||||
result.success(null)
|
||||
}
|
||||
"triggerFallback" -> {
|
||||
playerCore?.triggerFallback()
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
@@ -158,9 +172,17 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
|
||||
val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: true
|
||||
configuredBufferSizeBytes = bufferSizeBytes
|
||||
configuredTunnelingEnabled = tunnelingEnabled
|
||||
|
||||
currentActivity.runOnUiThread {
|
||||
sessionGeneration++
|
||||
|
||||
if (mpvCore != null || fallbackInProgress) {
|
||||
mpvCore?.dispose()
|
||||
mpvCore = null
|
||||
usingMpvFallback = false
|
||||
fallbackInProgress = false
|
||||
}
|
||||
|
||||
try {
|
||||
playerCore = ExoPlayerCore(currentActivity).apply {
|
||||
delegate = this@ExoPlayerPlugin
|
||||
@@ -185,6 +207,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
private fun handleDispose(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
sessionGeneration++
|
||||
playerCore?.dispose()
|
||||
playerCore = null
|
||||
mpvCore?.dispose()
|
||||
@@ -196,18 +219,26 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
} ?: result.success(null)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun handleOpen(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")
|
||||
val headers = call.argument<Map<String, String>>("headers")
|
||||
val startPositionMs = call.argument<Number>("startPositionMs")?.toLong() ?: 0L
|
||||
val autoPlay = call.argument<Boolean>("autoPlay") ?: true
|
||||
val isLive = call.argument<Boolean>("isLive") ?: false
|
||||
val externalSubtitles = call.argument<List<Map<String, String?>>>("externalSubtitles")
|
||||
|
||||
if (uri == null) {
|
||||
result.error("INVALID_ARGS", "Missing 'uri'", null)
|
||||
return
|
||||
}
|
||||
|
||||
// Only clear pending MPV properties when MPV is the active backend.
|
||||
// When ExoPlayer is active, keep them for potential ExoPlayer→MPV fallback.
|
||||
if (usingMpvFallback) {
|
||||
pendingMpvProperties.clear()
|
||||
}
|
||||
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
// MPV: Build loadfile command with options
|
||||
@@ -223,7 +254,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
|
||||
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
|
||||
} else {
|
||||
playerCore?.open(uri, headers, startPositionMs, autoPlay, isLive)
|
||||
playerCore?.open(uri, headers, startPositionMs, autoPlay, isLive, externalSubtitles)
|
||||
}
|
||||
result.success(null)
|
||||
} ?: result.error("NO_ACTIVITY", "Activity not available", null)
|
||||
@@ -328,9 +359,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
// MPV uses numeric track IDs - extract from string format
|
||||
val numericId = trackId.split("_").lastOrNull()?.toIntOrNull() ?: 1
|
||||
mpvCore?.setProperty("aid", numericId.toString())
|
||||
// After fallback, track IDs come from mpv's track-list (already 1-indexed)
|
||||
mpvCore?.setProperty("aid", trackId)
|
||||
} else {
|
||||
playerCore?.selectAudioTrack(trackId)
|
||||
}
|
||||
@@ -344,12 +374,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
// trackId can be null or "no" to disable subtitles
|
||||
activity?.runOnUiThread {
|
||||
if (usingMpvFallback) {
|
||||
if (trackId == null || trackId == "no") {
|
||||
mpvCore?.setProperty("sid", "no")
|
||||
} else {
|
||||
val numericId = trackId.split("_").lastOrNull()?.toIntOrNull() ?: 1
|
||||
mpvCore?.setProperty("sid", numericId.toString())
|
||||
}
|
||||
mpvCore?.setProperty("sid", trackId ?: "no")
|
||||
} else {
|
||||
playerCore?.selectSubtitleTrack(trackId)
|
||||
}
|
||||
@@ -396,6 +421,15 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleUpdateFrame(result: MethodChannel.Result) {
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.updateFrame()
|
||||
} else {
|
||||
playerCore?.updateFrame()
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetVideoFrameRate(call: MethodCall, result: MethodChannel.Result) {
|
||||
val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
|
||||
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
|
||||
@@ -460,6 +494,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
val bgColor = call.argument<String>("bgColor") ?: "#000000"
|
||||
val bgOpacity = call.argument<Number>("bgOpacity")?.toInt() ?: 0
|
||||
val subtitlePosition = call.argument<Number>("subtitlePosition")?.toInt() ?: 100
|
||||
val bold = call.argument<Boolean>("bold") ?: false
|
||||
val italic = call.argument<Boolean>("italic") ?: false
|
||||
|
||||
if (usingMpvFallback) {
|
||||
// MPV fallback handles styling via setProperty, no-op here
|
||||
@@ -467,21 +503,48 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition)
|
||||
playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition, bold, italic)
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetMpvProperty(call: MethodCall, result: MethodChannel.Result) {
|
||||
val name = call.argument<String>("name")
|
||||
val value = call.argument<String>("value")
|
||||
|
||||
if (name == null || value == null) {
|
||||
result.error("INVALID_ARGS", "Missing 'name' or 'value'", null)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply sync offsets to ExoPlayer when active
|
||||
if (!usingMpvFallback) {
|
||||
when (name) {
|
||||
"audio-delay" -> playerCore?.setAudioDelay(value.toDoubleOrNull() ?: 0.0)
|
||||
"sub-delay" -> playerCore?.setSubtitleDelay(value.toDoubleOrNull() ?: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
if (usingMpvFallback) {
|
||||
mpvCore?.setProperty(name, value)
|
||||
} else {
|
||||
// Store for later application if ExoPlayer falls back to MPV
|
||||
pendingMpvProperties.add(Pair(name, value))
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleGetStats(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
val stats = if (usingMpvFallback) {
|
||||
// For MPV fallback, query MPV properties directly
|
||||
getMpvStats()
|
||||
} else {
|
||||
if (usingMpvFallback) {
|
||||
Thread {
|
||||
val stats = getMpvStats()
|
||||
activity?.runOnUiThread { result.success(stats) }
|
||||
}.start()
|
||||
} else {
|
||||
activity?.runOnUiThread {
|
||||
val coreStats = playerCore?.getStats() ?: emptyMap()
|
||||
coreStats + mapOf("playerType" to "exoplayer")
|
||||
}
|
||||
result.success(stats)
|
||||
} ?: result.success(mapOf("playerType" to "unknown"))
|
||||
result.success(coreStats + mapOf("playerType" to "exoplayer"))
|
||||
} ?: result.success(mapOf("playerType" to "unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -500,6 +563,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
"video-codec" to mpv.getProperty("video-codec"),
|
||||
"video-params/w" to mpv.getProperty("video-params/w"),
|
||||
"video-params/h" to mpv.getProperty("video-params/h"),
|
||||
"videoWidth" to mpv.getProperty("dwidth"),
|
||||
"videoHeight" to mpv.getProperty("dheight"),
|
||||
"container-fps" to mpv.getProperty("container-fps"),
|
||||
"estimated-vf-fps" to mpv.getProperty("estimated-vf-fps"),
|
||||
"video-bitrate" to mpv.getProperty("video-bitrate"),
|
||||
@@ -555,7 +620,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
override fun onPropertyChange(name: String, value: Any?) {
|
||||
val propId = nameToId[name] ?: return
|
||||
eventSink?.success(listOf(propId, value))
|
||||
mainHandler.post { eventSink?.success(listOf(propId, value)) }
|
||||
}
|
||||
|
||||
override fun onEvent(name: String, data: Map<String, Any>?) {
|
||||
@@ -564,7 +629,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
"name" to name
|
||||
)
|
||||
data?.let { event["data"] = it }
|
||||
eventSink?.success(event)
|
||||
mainHandler.post { eventSink?.success(event) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -572,11 +637,14 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
* or null if the URI is not a content:// scheme or opening fails.
|
||||
* The returned FD is detached so MPV can own and close it via fdclose://.
|
||||
*/
|
||||
private fun openContentFd(uriString: String): Int? {
|
||||
private fun openContentFd(
|
||||
uriString: String,
|
||||
resolver: ContentResolver? = activity?.contentResolver
|
||||
): Int? {
|
||||
if (!uriString.startsWith("content://")) return null
|
||||
return try {
|
||||
val uri = Uri.parse(uriString)
|
||||
val pfd = activity?.contentResolver?.openFileDescriptor(uri, "r") ?: return null
|
||||
val pfd = resolver?.openFileDescriptor(uri, "r") ?: return null
|
||||
val fd = pfd.detachFd()
|
||||
Log.d(TAG, "Opened content FD $fd for $uriString")
|
||||
fd
|
||||
@@ -615,69 +683,136 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
playerCore = null
|
||||
mpvCore?.dispose()
|
||||
mpvCore = null
|
||||
usingMpvFallback = false // Clear before handoff
|
||||
|
||||
// Create and initialize MPV
|
||||
mpvCore = MpvPlayerCore(currentActivity).apply {
|
||||
delegate = this@ExoPlayerPlugin
|
||||
}
|
||||
mpvCore?.initialize { success ->
|
||||
if (!success) {
|
||||
val generation = sessionGeneration
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (generation != sessionGeneration) {
|
||||
fallbackInProgress = false
|
||||
Log.e(TAG, "Failed to initialize MPV fallback")
|
||||
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: $errorMessage"))
|
||||
return@initialize
|
||||
return@post
|
||||
}
|
||||
val act = activity
|
||||
if (act == null) {
|
||||
fallbackInProgress = false
|
||||
return@post
|
||||
}
|
||||
|
||||
usingMpvFallback = true
|
||||
fallbackInProgress = false
|
||||
|
||||
// Configure basic MPV properties for Plex playback
|
||||
mpvCore?.setProperty("hwdec", "mediacodec,mediacodec-copy")
|
||||
mpvCore?.setProperty("vo", "gpu")
|
||||
mpvCore?.setProperty("ao", "audiotrack")
|
||||
|
||||
// Forward user's buffer config to MPV fallback
|
||||
configuredBufferSizeBytes?.let { bytes ->
|
||||
if (bytes > 0) {
|
||||
mpvCore?.setProperty("demuxer-max-bytes", bytes.toString())
|
||||
try {
|
||||
val core = MpvPlayerCore(act).apply {
|
||||
delegate = this@ExoPlayerPlugin
|
||||
}
|
||||
mpvCore = core // publish so dispose/init can reach it
|
||||
|
||||
core.initialize { success ->
|
||||
if (generation != sessionGeneration) {
|
||||
if (mpvCore === core) {
|
||||
core.dispose()
|
||||
mpvCore = null
|
||||
}
|
||||
fallbackInProgress = false
|
||||
return@initialize
|
||||
}
|
||||
if (!success) {
|
||||
if (mpvCore === core) {
|
||||
core.dispose()
|
||||
mpvCore = null
|
||||
}
|
||||
fallbackInProgress = false
|
||||
Log.e(TAG, "Failed to initialize MPV fallback")
|
||||
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: $errorMessage"))
|
||||
return@initialize
|
||||
}
|
||||
|
||||
usingMpvFallback = true
|
||||
fallbackInProgress = false
|
||||
|
||||
// Snapshot pending properties on main thread before clearing
|
||||
val pendingProps = pendingMpvProperties.toList()
|
||||
pendingMpvProperties.clear()
|
||||
|
||||
// Compute content FD on main thread (needs contentResolver)
|
||||
val mpvUri = openContentFd(uri, act.contentResolver)
|
||||
?.let { "fdclose://$it" } ?: uri
|
||||
|
||||
// Buffer size for closure
|
||||
val bufferSize = configuredBufferSizeBytes
|
||||
|
||||
if (mpvCore !== core) {
|
||||
core.dispose()
|
||||
fallbackInProgress = false
|
||||
return@initialize
|
||||
}
|
||||
// Configure basic MPV properties for Plex playback
|
||||
core.setProperty("hwdec", "mediacodec,mediacodec-copy")
|
||||
core.setProperty("vo", "gpu")
|
||||
core.setProperty("ao", "audiotrack")
|
||||
|
||||
// Forward user's buffer config to MPV fallback
|
||||
if (bufferSize != null && bufferSize > 0) {
|
||||
core.setProperty("demuxer-max-bytes", bufferSize.toString())
|
||||
}
|
||||
|
||||
// Apply pending MPV properties from Dart
|
||||
for ((propName, propValue) in pendingProps) {
|
||||
core.setProperty(propName, propValue)
|
||||
}
|
||||
|
||||
// Setup property observers
|
||||
core.observeProperty("time-pos", "double")
|
||||
core.observeProperty("duration", "double")
|
||||
core.observeProperty("seekable", "flag")
|
||||
core.observeProperty("pause", "flag")
|
||||
core.observeProperty("paused-for-cache", "flag")
|
||||
core.observeProperty("demuxer-cache-time", "double")
|
||||
core.observeProperty("eof-reached", "flag")
|
||||
core.observeProperty("track-list", "string")
|
||||
core.observeProperty("aid", "string")
|
||||
core.observeProperty("sid", "string")
|
||||
core.observeProperty("volume", "double")
|
||||
core.observeProperty("speed", "double")
|
||||
|
||||
// Show the MPV surface (internally posts to UI)
|
||||
core.setVisible(true)
|
||||
|
||||
// Load media at the same position
|
||||
val startSeconds = positionMs / 1000.0
|
||||
val options = mutableListOf<String>()
|
||||
options.add("start=$startSeconds")
|
||||
headers?.forEach { (key, value) ->
|
||||
options.add("http-header-fields-append=$key: $value")
|
||||
}
|
||||
val optionsStr = options.joinToString(",")
|
||||
core.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
|
||||
|
||||
// On GPUs without compute shaders, MPV can't do dynamic peak detection
|
||||
// and spline tone-mapping produces dim/washed-out results with extreme
|
||||
// static HDR peak metadata. Use reinhard which handles this better.
|
||||
Thread {
|
||||
val peakDetection = core.getProperty("hdr-compute-peak")
|
||||
if (peakDetection == "no") {
|
||||
Log.i(TAG, "No compute shaders — overriding tone-mapping to reinhard")
|
||||
core.setProperty("tone-mapping", "reinhard")
|
||||
core.setProperty("tone-mapping-param", "0.7")
|
||||
core.setProperty("tone-mapping-mode", "luma")
|
||||
}
|
||||
}.start()
|
||||
|
||||
// Request audio focus
|
||||
core.requestAudioFocus()
|
||||
|
||||
// Emit backend-switched event on main thread
|
||||
activity?.runOnUiThread {
|
||||
onEvent("backend-switched", null)
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully switched to MPV fallback")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
fallbackInProgress = false
|
||||
Log.e(TAG, "Failed to switch to MPV fallback", e)
|
||||
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: ${e.message}"))
|
||||
}
|
||||
|
||||
// Setup property observers
|
||||
mpvCore?.observeProperty("time-pos", "double")
|
||||
mpvCore?.observeProperty("duration", "double")
|
||||
mpvCore?.observeProperty("pause", "flag")
|
||||
mpvCore?.observeProperty("paused-for-cache", "flag")
|
||||
mpvCore?.observeProperty("demuxer-cache-time", "double")
|
||||
mpvCore?.observeProperty("eof-reached", "flag")
|
||||
mpvCore?.observeProperty("track-list", "string")
|
||||
mpvCore?.observeProperty("aid", "string")
|
||||
mpvCore?.observeProperty("sid", "string")
|
||||
mpvCore?.observeProperty("volume", "double")
|
||||
mpvCore?.observeProperty("speed", "double")
|
||||
|
||||
// Show the MPV surface
|
||||
mpvCore?.setVisible(true)
|
||||
|
||||
// Load media at the same position
|
||||
val startSeconds = positionMs / 1000.0
|
||||
val options = mutableListOf<String>()
|
||||
options.add("start=$startSeconds")
|
||||
headers?.forEach { (key, value) ->
|
||||
options.add("http-header-fields-append=$key: $value")
|
||||
}
|
||||
val optionsStr = options.joinToString(",")
|
||||
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
|
||||
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
|
||||
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
|
||||
|
||||
// Request audio focus
|
||||
mpvCore?.requestAudioFocus()
|
||||
|
||||
// Emit backend-switched event so Flutter can show notification
|
||||
onEvent("backend-switched", null)
|
||||
|
||||
Log.i(TAG, "Successfully switched to MPV fallback")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
fallbackInProgress = false
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.analytics.PlayerId
|
||||
import androidx.media3.exoplayer.audio.AudioOutput
|
||||
import androidx.media3.exoplayer.audio.AudioOutputProvider
|
||||
import androidx.media3.exoplayer.audio.AudioSink
|
||||
import androidx.media3.exoplayer.audio.AudioTrackAudioOutputProvider
|
||||
import androidx.media3.exoplayer.audio.DefaultAudioSink
|
||||
import androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider
|
||||
import androidx.media3.exoplayer.audio.ForwardingAudioSink
|
||||
import androidx.media3.exoplayer.Renderer
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.abs
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context) {
|
||||
|
||||
/** Audio delay in microseconds. Shared with PositionFixAudioSink for live updates. */
|
||||
val audioDelayUs = AtomicLong(0L)
|
||||
|
||||
override fun buildAudioSink(
|
||||
context: Context,
|
||||
enableFloatOutput: Boolean,
|
||||
enableAudioOutputPlaybackParams: Boolean
|
||||
): AudioSink {
|
||||
AudioTrackAudioOutputProvider.failOnSpuriousAudioTimestamp = false
|
||||
|
||||
val bufferSizeProvider = DefaultAudioTrackBufferSizeProvider.Builder()
|
||||
.setMinPcmBufferDurationUs(500_000)
|
||||
.setMaxPcmBufferDurationUs(1_000_000)
|
||||
.setPcmBufferMultiplicationFactor(4)
|
||||
.build()
|
||||
|
||||
val realProvider = AudioTrackAudioOutputProvider.Builder(context)
|
||||
.setAudioTrackBufferSizeProvider(bufferSizeProvider)
|
||||
.build()
|
||||
|
||||
// Shared position: RawPositionAudioOutput writes the raw AudioTrack position,
|
||||
// PositionFixAudioSink reads it to bypass DefaultAudioSink's writtenDuration clamp.
|
||||
val rawPositionUs = AtomicLong(Long.MIN_VALUE)
|
||||
|
||||
val defaultSink = DefaultAudioSink.Builder(context)
|
||||
.setEnableFloatOutput(enableFloatOutput)
|
||||
.setEnableAudioOutputPlaybackParameters(enableAudioOutputPlaybackParams)
|
||||
.setAudioOutputProvider(RawPositionOutputProvider(realProvider, rawPositionUs))
|
||||
.build()
|
||||
|
||||
return PositionFixAudioSink(defaultSink, rawPositionUs, audioDelayUs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes video stutter with large audio frames (e.g. 120ms Opus).
|
||||
*
|
||||
* DefaultAudioSink.getCurrentPositionUs() clamps the playback position to
|
||||
* writtenDuration via Math.min(positionUs, framesToDurationUs(writtenFrames)).
|
||||
* With large audio frames, data arrives in bursts and writtenDuration lags behind
|
||||
* the real AudioTrack playback position between bursts, creating a sawtooth
|
||||
* position pattern (plateau → jump) that makes the video renderer drop frames.
|
||||
*
|
||||
* Fix: when DefaultAudioSink's position falls behind the raw AudioTrack position,
|
||||
* return startMediaTimeUs + rawPosition instead, giving the video renderer a
|
||||
* smooth clock to sync against.
|
||||
*
|
||||
* Also suppresses Amlogic AudioTrack timestamp discontinuity errors.
|
||||
*
|
||||
* The raw AudioTrack position advances at real-time rate regardless of speed
|
||||
* (AudioTrackPositionTracker reports post-time-stretch output frames). The
|
||||
* bypass multiplies by speed to convert to media time, matching what
|
||||
* DefaultAudioSink.applyMediaPositionParameters() does internally. Reference
|
||||
* points are captured on speed changes for correct mid-playback transitions.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
private class PositionFixAudioSink(
|
||||
sink: AudioSink,
|
||||
private val rawPositionUs: AtomicLong,
|
||||
private val audioDelayUs: AtomicLong
|
||||
) : ForwardingAudioSink(sink) {
|
||||
|
||||
private var startMediaTimeUs = Long.MIN_VALUE
|
||||
private var suppressedErrorCount = 0
|
||||
|
||||
// Speed tracking for position bypass
|
||||
private var currentSpeed = 1.0f
|
||||
private var refMediaTimeUs = Long.MIN_VALUE
|
||||
private var refRawPositionUs = 0L
|
||||
|
||||
// Transient counter-offset: after seek/flush, ramp offset from 0 to full
|
||||
// over recoveryDurationUs to prevent video frame drops.
|
||||
private var recoveryDurationUs = 0L
|
||||
|
||||
override fun handleBuffer(
|
||||
buffer: ByteBuffer,
|
||||
presentationTimeUs: Long,
|
||||
encodedAccessUnitCount: Int
|
||||
): Boolean {
|
||||
if (startMediaTimeUs == Long.MIN_VALUE) {
|
||||
startMediaTimeUs = presentationTimeUs
|
||||
refMediaTimeUs = presentationTimeUs
|
||||
refRawPositionUs = 0
|
||||
val delayUs = audioDelayUs.get()
|
||||
recoveryDurationUs = if (delayUs == 0L) 0L else maxOf(200_000L, 2L * abs(delayUs))
|
||||
}
|
||||
return super.handleBuffer(buffer, presentationTimeUs, encodedAccessUnitCount)
|
||||
}
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
// Capture reference point before speed changes
|
||||
val rawPos = rawPositionUs.get()
|
||||
if (rawPos > 0 && refMediaTimeUs != Long.MIN_VALUE) {
|
||||
refMediaTimeUs = refMediaTimeUs + ((rawPos - refRawPositionUs) * currentSpeed).toLong()
|
||||
refRawPositionUs = rawPos
|
||||
}
|
||||
currentSpeed = playbackParameters.speed
|
||||
super.setPlaybackParameters(playbackParameters)
|
||||
}
|
||||
|
||||
override fun getCurrentPositionUs(sourceEnded: Boolean): Long {
|
||||
val delegatePos = super.getCurrentPositionUs(sourceEnded)
|
||||
if (delegatePos == Long.MIN_VALUE || refMediaTimeUs == Long.MIN_VALUE) {
|
||||
return delegatePos
|
||||
}
|
||||
|
||||
val rawPos = rawPositionUs.get()
|
||||
val basePos = if (rawPos <= 0) {
|
||||
delegatePos
|
||||
} else {
|
||||
val expectedPos = refMediaTimeUs + ((rawPos - refRawPositionUs) * currentSpeed).toLong()
|
||||
if (expectedPos > delegatePos + 30_000) expectedPos else delegatePos
|
||||
}
|
||||
|
||||
// Audio delay with transient counter-offset to prevent frame drops after seeks.
|
||||
// After flush, offset ramps linearly from 0 to full over recoveryDurationUs.
|
||||
val delayUs = audioDelayUs.get()
|
||||
if (delayUs == 0L) return basePos
|
||||
|
||||
val elapsedUs = basePos - startMediaTimeUs
|
||||
val netOffsetUs = if (recoveryDurationUs <= 0L || elapsedUs >= recoveryDurationUs) {
|
||||
delayUs
|
||||
} else {
|
||||
(delayUs * elapsedUs) / recoveryDurationUs
|
||||
}
|
||||
return basePos + netOffsetUs
|
||||
}
|
||||
|
||||
// --- Suppress timestamp discontinuity errors ---
|
||||
|
||||
override fun setListener(listener: AudioSink.Listener) {
|
||||
super.setListener(@OptIn(UnstableApi::class) object : AudioSink.Listener {
|
||||
override fun onPositionDiscontinuity() = listener.onPositionDiscontinuity()
|
||||
override fun onPositionAdvancing(playoutStartSystemTimeUs: Long) = listener.onPositionAdvancing(playoutStartSystemTimeUs)
|
||||
override fun onUnderrun(bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long) = listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)
|
||||
override fun onSkipSilenceEnabledChanged(skipSilenceEnabled: Boolean) = listener.onSkipSilenceEnabledChanged(skipSilenceEnabled)
|
||||
override fun onOffloadBufferEmptying() = listener.onOffloadBufferEmptying()
|
||||
override fun onOffloadBufferFull() = listener.onOffloadBufferFull()
|
||||
override fun onAudioCapabilitiesChanged() = listener.onAudioCapabilitiesChanged()
|
||||
override fun onAudioTrackInitialized(audioTrackConfig: AudioSink.AudioTrackConfig) = listener.onAudioTrackInitialized(audioTrackConfig)
|
||||
override fun onAudioTrackReleased(audioTrackConfig: AudioSink.AudioTrackConfig) = listener.onAudioTrackReleased(audioTrackConfig)
|
||||
override fun onSilenceSkipped() = listener.onSilenceSkipped()
|
||||
override fun onAudioSessionIdChanged(audioSessionId: Int) = listener.onAudioSessionIdChanged(audioSessionId)
|
||||
|
||||
override fun onAudioSinkError(audioSinkError: Exception) {
|
||||
if (isTimestampDiscontinuity(audioSinkError)) {
|
||||
suppressedErrorCount++
|
||||
return
|
||||
}
|
||||
listener.onAudioSinkError(audioSinkError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
startMediaTimeUs = Long.MIN_VALUE
|
||||
refMediaTimeUs = Long.MIN_VALUE
|
||||
refRawPositionUs = 0
|
||||
recoveryDurationUs = 0L
|
||||
rawPositionUs.set(Long.MIN_VALUE)
|
||||
super.flush()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
startMediaTimeUs = Long.MIN_VALUE
|
||||
refMediaTimeUs = Long.MIN_VALUE
|
||||
refRawPositionUs = 0
|
||||
recoveryDurationUs = 0L
|
||||
currentSpeed = 1.0f
|
||||
rawPositionUs.set(Long.MIN_VALUE)
|
||||
suppressedErrorCount = 0
|
||||
super.reset()
|
||||
}
|
||||
|
||||
private fun isTimestampDiscontinuity(e: Exception): Boolean {
|
||||
val name = e.javaClass.simpleName
|
||||
val msg = e.message ?: ""
|
||||
return name == "InvalidAudioTrackTimestampException" ||
|
||||
name == "UnexpectedDiscontinuityException" ||
|
||||
msg.contains("timestamp discontinuity", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a text renderer to shift subtitle timing by [delayUs] microseconds.
|
||||
* Positive delay → subtitles appear later, negative → earlier.
|
||||
* Only render() needs the offset — the text renderer uses positionUs to decide
|
||||
* which cues are active; other Renderer methods are timing-independent.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
internal class SubtitleDelayRenderer(
|
||||
private val delegate: Renderer,
|
||||
private val delayUs: AtomicLong
|
||||
) : Renderer by delegate {
|
||||
override fun render(positionUs: Long, elapsedRealtimeUs: Long) {
|
||||
delegate.render(positionUs - delayUs.get(), elapsedRealtimeUs)
|
||||
}
|
||||
}
|
||||
|
||||
// --- AudioOutput wrapping: shares raw position with PositionFixAudioSink ---
|
||||
// Also implements AudioTrack reuse across seeks to avoid expensive teardown/recreation.
|
||||
// DefaultAudioSink releases the AudioOutput on every flush (seek), which destroys the
|
||||
// AudioTrack and creates a new one. On Android TV with tunneled playback, this causes
|
||||
// 7-10s audio dropout while the hardware pipeline reinitializes (Sony Bravia, etc).
|
||||
// By flushing instead of releasing and caching the output, we skip the teardown cycle.
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private class RawPositionOutputProvider(
|
||||
private val delegate: AudioOutputProvider,
|
||||
private val rawPositionUs: AtomicLong
|
||||
) : AudioOutputProvider {
|
||||
|
||||
private var cachedOutput: RawPositionAudioOutput? = null
|
||||
private var cachedConfig: AudioOutputProvider.OutputConfig? = null
|
||||
|
||||
override fun getFormatSupport(config: AudioOutputProvider.FormatConfig) =
|
||||
delegate.getFormatSupport(config)
|
||||
|
||||
override fun getOutputConfig(config: AudioOutputProvider.FormatConfig) =
|
||||
delegate.getOutputConfig(config)
|
||||
|
||||
override fun getAudioOutput(config: AudioOutputProvider.OutputConfig): AudioOutput {
|
||||
val cached = cachedOutput
|
||||
if (cached != null && cachedConfig == config) {
|
||||
cachedOutput = null
|
||||
return cached
|
||||
}
|
||||
cached?.forceRelease()
|
||||
cachedOutput = null
|
||||
|
||||
val realOutput = delegate.getAudioOutput(config)
|
||||
cachedConfig = config
|
||||
return RawPositionAudioOutput(realOutput, rawPositionUs, this)
|
||||
}
|
||||
|
||||
fun returnToCache(output: RawPositionAudioOutput) {
|
||||
val existing = cachedOutput
|
||||
if (existing != null && existing !== output) {
|
||||
existing.forceRelease()
|
||||
}
|
||||
cachedOutput = output
|
||||
}
|
||||
|
||||
override fun addListener(listener: AudioOutputProvider.Listener) =
|
||||
delegate.addListener(listener)
|
||||
|
||||
override fun removeListener(listener: AudioOutputProvider.Listener) =
|
||||
delegate.removeListener(listener)
|
||||
|
||||
override fun setClock(clock: Clock) = delegate.setClock(clock)
|
||||
|
||||
override fun release() {
|
||||
cachedOutput?.forceRelease()
|
||||
cachedOutput = null
|
||||
cachedConfig = null
|
||||
delegate.release()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private class RawPositionAudioOutput(
|
||||
private val delegate: AudioOutput,
|
||||
private val rawPositionUs: AtomicLong,
|
||||
private val provider: RawPositionOutputProvider
|
||||
) : AudioOutput {
|
||||
|
||||
override fun getPositionUs(): Long {
|
||||
val pos = delegate.getPositionUs()
|
||||
rawPositionUs.set(pos)
|
||||
return pos
|
||||
}
|
||||
|
||||
override fun play() = delegate.play()
|
||||
override fun pause() = delegate.pause()
|
||||
|
||||
@Throws(AudioOutput.WriteException::class)
|
||||
override fun write(buffer: ByteBuffer, size: Int, presentationTimeUs: Long) =
|
||||
delegate.write(buffer, size, presentationTimeUs)
|
||||
|
||||
override fun flush() {
|
||||
rawPositionUs.set(Long.MIN_VALUE)
|
||||
delegate.flush()
|
||||
}
|
||||
|
||||
override fun stop() = delegate.stop()
|
||||
|
||||
override fun release() {
|
||||
rawPositionUs.set(Long.MIN_VALUE)
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
delegate.stop()
|
||||
delegate.flush()
|
||||
provider.returnToCache(this)
|
||||
} else {
|
||||
delegate.release()
|
||||
}
|
||||
}
|
||||
|
||||
fun forceRelease() {
|
||||
rawPositionUs.set(Long.MIN_VALUE)
|
||||
delegate.release()
|
||||
}
|
||||
|
||||
override fun setVolume(volume: Float) = delegate.setVolume(volume)
|
||||
override fun isOffloadedPlayback() = delegate.isOffloadedPlayback()
|
||||
override fun getAudioSessionId() = delegate.getAudioSessionId()
|
||||
override fun getSampleRate() = delegate.getSampleRate()
|
||||
override fun getBufferSizeInFrames() = delegate.getBufferSizeInFrames()
|
||||
override fun getPlaybackParameters() = delegate.getPlaybackParameters()
|
||||
override fun isStalled() = delegate.isStalled()
|
||||
override fun addListener(listener: AudioOutput.Listener) = delegate.addListener(listener)
|
||||
override fun removeListener(listener: AudioOutput.Listener) = delegate.removeListener(listener)
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) =
|
||||
delegate.setPlaybackParameters(playbackParameters)
|
||||
override fun setOffloadDelayPadding(delayInFrames: Int, paddingInFrames: Int) =
|
||||
delegate.setOffloadDelayPadding(delayInFrames, paddingInFrames)
|
||||
override fun setOffloadEndOfStream() = delegate.setOffloadEndOfStream()
|
||||
override fun setPlayerId(playerId: PlayerId) = delegate.setPlayerId(playerId)
|
||||
override fun attachAuxEffect(effectId: Int) = delegate.attachAuxEffect(effectId)
|
||||
override fun setAuxEffectSendLevel(level: Float) = delegate.setAuxEffectSendLevel(level)
|
||||
override fun setPreferredDevice(preferredDevice: AudioDeviceInfo?) =
|
||||
delegate.setPreferredDevice(preferredDevice)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.common.DataReader
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.util.ParsableByteArray
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import java.util.zip.DataFormatException
|
||||
import java.util.zip.Inflater
|
||||
|
||||
/**
|
||||
* TrackOutput wrapper that inflates zlib-compressed sample data (MKV ContentCompAlgo 0).
|
||||
* Each MKV block is independently zlib-compressed; this wrapper decompresses per-sample
|
||||
* between sampleData() and sampleMetadata() calls.
|
||||
*
|
||||
* All buffers are reused across samples to minimize GC pressure on the hot path.
|
||||
*/
|
||||
class ZlibInflatingTrackOutput(
|
||||
private val delegate: TrackOutput,
|
||||
) : TrackOutput {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ZlibTrackOutput"
|
||||
private const val INITIAL_BUFFER_SIZE = 256 * 1024
|
||||
private const val INFLATE_CHUNK = 64 * 1024
|
||||
}
|
||||
|
||||
var active = false
|
||||
|
||||
private val inflater = Inflater()
|
||||
|
||||
// Reusable buffers — grown as needed, never shrunk
|
||||
private var compressedBuf = ByteArray(INITIAL_BUFFER_SIZE)
|
||||
private var compressedLen = 0
|
||||
private var inflateBuf = ByteArray(INITIAL_BUFFER_SIZE)
|
||||
private var readBuf = ByteArray(INFLATE_CHUNK)
|
||||
private val outputParsable = ParsableByteArray()
|
||||
private var buffering = false
|
||||
|
||||
override fun format(format: Format) = delegate.format(format)
|
||||
|
||||
override fun sampleData(
|
||||
input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int
|
||||
): Int {
|
||||
if (!active) return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
|
||||
|
||||
buffering = true
|
||||
if (readBuf.size < length) readBuf = ByteArray(length)
|
||||
val bytesRead = input.read(readBuf, 0, length)
|
||||
if (bytesRead > 0) appendCompressed(readBuf, 0, bytesRead)
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
|
||||
if (!active) {
|
||||
delegate.sampleData(data, length, sampleDataPart)
|
||||
return
|
||||
}
|
||||
|
||||
buffering = true
|
||||
ensureCompressedCapacity(compressedLen + length)
|
||||
data.readBytes(compressedBuf, compressedLen, length)
|
||||
compressedLen += length
|
||||
}
|
||||
|
||||
override fun sampleMetadata(
|
||||
timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?
|
||||
) {
|
||||
if (!active || !buffering) {
|
||||
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
|
||||
return
|
||||
}
|
||||
|
||||
buffering = false
|
||||
val srcLen = compressedLen
|
||||
compressedLen = 0
|
||||
|
||||
val inflatedLen = try {
|
||||
inflater.reset()
|
||||
inflater.setInput(compressedBuf, 0, srcLen)
|
||||
var written = 0
|
||||
while (!inflater.finished()) {
|
||||
if (written == inflateBuf.size) growInflateBuf()
|
||||
val count = inflater.inflate(inflateBuf, written, inflateBuf.size - written)
|
||||
if (count == 0 && !inflater.finished()) break
|
||||
written += count
|
||||
}
|
||||
written
|
||||
} catch (e: DataFormatException) {
|
||||
Log.e(TAG, "Zlib inflate failed (${srcLen}B), passing raw", e)
|
||||
// Fall back to raw compressed data
|
||||
ensureInflateCapacity(srcLen)
|
||||
System.arraycopy(compressedBuf, 0, inflateBuf, 0, srcLen)
|
||||
srcLen
|
||||
}
|
||||
|
||||
outputParsable.reset(inflateBuf, inflatedLen)
|
||||
delegate.sampleData(outputParsable, inflatedLen, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
delegate.sampleMetadata(timeUs, flags, inflatedLen, 0, cryptoData)
|
||||
}
|
||||
|
||||
private fun appendCompressed(src: ByteArray, offset: Int, length: Int) {
|
||||
ensureCompressedCapacity(compressedLen + length)
|
||||
System.arraycopy(src, offset, compressedBuf, compressedLen, length)
|
||||
compressedLen += length
|
||||
}
|
||||
|
||||
private fun ensureCompressedCapacity(needed: Int) {
|
||||
if (compressedBuf.size < needed) {
|
||||
compressedBuf = compressedBuf.copyOf(maxOf(needed, compressedBuf.size * 2))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInflateCapacity(needed: Int) {
|
||||
if (inflateBuf.size < needed) {
|
||||
inflateBuf = ByteArray(maxOf(needed, inflateBuf.size * 2))
|
||||
}
|
||||
}
|
||||
|
||||
private fun growInflateBuf() {
|
||||
inflateBuf = inflateBuf.copyOf(inflateBuf.size * 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.extractor.ExtractorInput
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import androidx.media3.extractor.mkv.MatroskaExtractor
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
import io.github.peerless2012.ass.media.extractor.AssMatroskaExtractor
|
||||
import androidx.media3.extractor.text.SubtitleParser
|
||||
|
||||
/**
|
||||
* Extends AssMatroskaExtractor to add support for MKV ContentCompAlgo 0 (zlib).
|
||||
*
|
||||
* Media3's MatroskaExtractor only supports ContentCompAlgo 3 (header stripping).
|
||||
* This subclass intercepts the compression algorithm during track header parsing:
|
||||
* - Tells the parent it's header stripping (algo 3) to avoid the ParserException
|
||||
* - Wraps TrackOutputs with ZlibInflatingTrackOutput to decompress per-sample data
|
||||
* - Skips ContentCompSettings for zlib tracks (not applicable)
|
||||
*/
|
||||
class ZlibMatroskaExtractor(
|
||||
subtitleParserFactory: SubtitleParser.Factory,
|
||||
assHandler: AssHandler,
|
||||
) : AssMatroskaExtractor(subtitleParserFactory, assHandler) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ZlibMkvExtractor"
|
||||
|
||||
// Matroska EBML element IDs
|
||||
private const val ID_SEGMENT = 0x18538067
|
||||
private const val ID_TRACK_ENTRY = 0xAE
|
||||
private const val ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254
|
||||
private const val ID_CONTENT_COMPRESSION_SETTINGS = 0x4255
|
||||
|
||||
private val extractorOutputField by lazy {
|
||||
MatroskaExtractor::class.java.getDeclaredField("extractorOutput").apply {
|
||||
isAccessible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var zlibOutput: ZlibExtractorOutputWrapper? = null
|
||||
private var currentTrackUsesZlib = false
|
||||
|
||||
override fun startMasterElement(id: Int, contentPosition: Long, contentSize: Long) {
|
||||
super.startMasterElement(id, contentPosition, contentSize)
|
||||
|
||||
// After super installs AssSubtitleExtractorOutput, wrap it with our zlib layer
|
||||
if (id == ID_SEGMENT && zlibOutput == null) {
|
||||
val currentOutput = extractorOutputField.get(this) as ExtractorOutput
|
||||
val wrapper = ZlibExtractorOutputWrapper(currentOutput)
|
||||
zlibOutput = wrapper
|
||||
extractorOutputField.set(this, wrapper)
|
||||
Log.d(TAG, "Installed zlib ExtractorOutput wrapper")
|
||||
}
|
||||
}
|
||||
|
||||
override fun integerElement(id: Int, value: Long) {
|
||||
if (id == ID_CONTENT_COMPRESSION_ALGORITHM && value == 0L) {
|
||||
currentTrackUsesZlib = true
|
||||
Log.i(TAG, "Track uses ContentCompAlgo 0 (zlib), will inflate samples")
|
||||
// Tell parent it's header stripping (algo 3) to avoid ParserException
|
||||
super.integerElement(id, 3)
|
||||
return
|
||||
}
|
||||
super.integerElement(id, value)
|
||||
}
|
||||
|
||||
override fun binaryElement(id: Int, contentSize: Int, input: ExtractorInput) {
|
||||
if (id == ID_CONTENT_COMPRESSION_SETTINGS && currentTrackUsesZlib) {
|
||||
// Skip ContentCompSettings for zlib tracks — parent would store these as
|
||||
// sampleStrippedBytes and prepend them to every sample, corrupting output.
|
||||
input.skipFully(contentSize)
|
||||
return
|
||||
}
|
||||
super.binaryElement(id, contentSize, input)
|
||||
}
|
||||
|
||||
override fun endMasterElement(id: Int) {
|
||||
val wasZlib = currentTrackUsesZlib
|
||||
super.endMasterElement(id)
|
||||
|
||||
if (id == ID_TRACK_ENTRY && wasZlib) {
|
||||
zlibOutput?.activateLast()
|
||||
currentTrackUsesZlib = false
|
||||
Log.i(TAG, "Activated zlib inflation for track")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ExtractorOutput wrapper that wraps all TrackOutputs with ZlibInflatingTrackOutput.
|
||||
* Tracks are created inactive; activateLast() enables inflation for the most recently
|
||||
* created track (called when we know a track uses zlib compression).
|
||||
*/
|
||||
private class ZlibExtractorOutputWrapper(
|
||||
private val delegate: ExtractorOutput,
|
||||
) : ExtractorOutput {
|
||||
|
||||
private var lastCreatedWrapper: ZlibInflatingTrackOutput? = null
|
||||
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = delegate.track(id, type)
|
||||
val wrapper = ZlibInflatingTrackOutput(original)
|
||||
lastCreatedWrapper = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
fun activateLast() {
|
||||
lastCreatedWrapper?.active = true
|
||||
}
|
||||
|
||||
override fun endTracks() = delegate.endTracks()
|
||||
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
EventChannel.StreamHandler, ActivityAware, MpvPlayerDelegate {
|
||||
EventChannel.StreamHandler, ActivityAware, com.edde746.plezy.shared.PlayerDelegate {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MpvPlayerPlugin"
|
||||
@@ -26,6 +26,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private var activity: Activity? = null
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
private val nameToId = mutableMapOf<String, Int>()
|
||||
private var sessionGeneration = 0
|
||||
|
||||
// FlutterPlugin
|
||||
|
||||
@@ -54,6 +55,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
++sessionGeneration
|
||||
playerCore?.dispose()
|
||||
playerCore = null
|
||||
activity = null
|
||||
@@ -96,6 +98,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
"observeProperty" -> handleObserveProperty(call, result)
|
||||
"command" -> handleCommand(call, result)
|
||||
"setVisible" -> handleSetVisible(call, result)
|
||||
"updateFrame" -> handleUpdateFrame(result)
|
||||
"setVideoFrameRate" -> handleSetVideoFrameRate(call, result)
|
||||
"clearVideoFrameRate" -> handleClearVideoFrameRate(result)
|
||||
"requestAudioFocus" -> handleRequestAudioFocus(result)
|
||||
@@ -122,17 +125,28 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
currentActivity.runOnUiThread {
|
||||
try {
|
||||
playerCore = MpvPlayerCore(currentActivity).apply {
|
||||
// Dispose stale core idempotently
|
||||
playerCore?.dispose()
|
||||
playerCore = null
|
||||
|
||||
val gen = ++sessionGeneration
|
||||
val core = MpvPlayerCore(currentActivity).apply {
|
||||
delegate = this@MpvPlayerPlugin
|
||||
}
|
||||
playerCore = core
|
||||
|
||||
playerCore?.initialize { success ->
|
||||
core.initialize { success ->
|
||||
if (gen != sessionGeneration || playerCore !== core) {
|
||||
Log.d(TAG, "Stale init callback (gen=$gen, current=$sessionGeneration)")
|
||||
result.success(false)
|
||||
return@initialize
|
||||
}
|
||||
// Start hidden - now safe because setVisible operates on the container,
|
||||
// not the SurfaceView directly (matching ExoPlayer's approach)
|
||||
playerCore?.setVisible(false)
|
||||
core.setVisible(false)
|
||||
Log.d(TAG, "Initialized: $success")
|
||||
result.success(success)
|
||||
} ?: result.success(false)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize: ${e.message}", e)
|
||||
result.error("INIT_FAILED", e.message, null)
|
||||
@@ -142,10 +156,14 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
private fun handleDispose(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
playerCore?.dispose()
|
||||
val core = playerCore
|
||||
++sessionGeneration
|
||||
playerCore = null
|
||||
Log.d(TAG, "Disposed")
|
||||
result.success(null)
|
||||
|
||||
core?.dispose {
|
||||
Log.d(TAG, "Disposed")
|
||||
result.success(null)
|
||||
} ?: result.success(null)
|
||||
} ?: result.success(null)
|
||||
}
|
||||
|
||||
@@ -197,10 +215,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
return
|
||||
}
|
||||
|
||||
// Use async command to prevent ANR - command executes off UI thread
|
||||
// and result is called back when complete
|
||||
playerCore?.commandAsync(args.toTypedArray(), result)
|
||||
?: result.success(null)
|
||||
playerCore?.command(args.toTypedArray())
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetVisible(call: MethodCall, result: MethodChannel.Result) {
|
||||
@@ -215,6 +231,11 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleUpdateFrame(result: MethodChannel.Result) {
|
||||
playerCore?.updateFrame()
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetVideoFrameRate(call: MethodCall, result: MethodChannel.Result) {
|
||||
val fps = call.argument<Double>("fps")?.toFloat() ?: 0f
|
||||
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
|
||||
@@ -249,31 +270,35 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val uri = Uri.parse(uriString)
|
||||
val contentResolver = activity?.contentResolver
|
||||
if (contentResolver == null) {
|
||||
result.error("NO_ACTIVITY", "Activity not available", null)
|
||||
return
|
||||
}
|
||||
|
||||
val pfd = contentResolver.openFileDescriptor(uri, "r")
|
||||
if (pfd == null) {
|
||||
result.error("OPEN_FAILED", "Failed to open file descriptor for $uriString", null)
|
||||
return
|
||||
}
|
||||
|
||||
// detachFd() transfers ownership of the FD to the caller (MPV via fdclose://)
|
||||
val fd = pfd.detachFd()
|
||||
Log.d(TAG, "Opened content FD $fd for $uriString")
|
||||
result.success(fd)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to open content FD: ${e.message}", e)
|
||||
result.error("OPEN_FAILED", e.message, null)
|
||||
val contentResolver = activity?.contentResolver
|
||||
if (contentResolver == null) {
|
||||
result.error("NO_ACTIVITY", "Activity not available", null)
|
||||
return
|
||||
}
|
||||
|
||||
// Open file descriptor off UI thread to prevent ANR on slow storage
|
||||
Thread {
|
||||
try {
|
||||
val uri = Uri.parse(uriString)
|
||||
val pfd = contentResolver.openFileDescriptor(uri, "r")
|
||||
if (pfd == null) {
|
||||
activity?.runOnUiThread {
|
||||
result.error("OPEN_FAILED", "Failed to open file descriptor for $uriString", null)
|
||||
}
|
||||
return@Thread
|
||||
}
|
||||
|
||||
val fd = pfd.detachFd()
|
||||
Log.d(TAG, "Opened content FD $fd for $uriString")
|
||||
activity?.runOnUiThread { result.success(fd) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to open content FD: ${e.message}", e)
|
||||
activity?.runOnUiThread { result.error("OPEN_FAILED", e.message, null) }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// MpvPlayerDelegate
|
||||
// PlayerDelegate
|
||||
|
||||
override fun onPropertyChange(name: String, value: Any?) {
|
||||
val propId = nameToId[name] ?: return
|
||||
|
||||
@@ -37,16 +37,19 @@ class FrameRateManager(
|
||||
return
|
||||
}
|
||||
|
||||
if (surface == null) {
|
||||
Log.d(TAG, "setVideoFrameRate: Surface not available")
|
||||
return
|
||||
}
|
||||
|
||||
log("fps=$fps, duration=${videoDurationMs}ms, API=${Build.VERSION.SDK_INT}")
|
||||
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> setFrameRateS(fps, surface, videoDurationMs)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> setFrameRateR(fps, surface)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (surface == null) {
|
||||
Log.d(TAG, "setVideoFrameRate: Surface not available")
|
||||
return
|
||||
}
|
||||
setFrameRateS(fps, surface, videoDurationMs)
|
||||
}
|
||||
// API R's Surface.setFrameRate() only supports seamless switching (no
|
||||
// CHANGE_FRAME_RATE_ALWAYS), so 60→24Hz won't switch. Fall through to
|
||||
// preferredDisplayModeId which directly sets the display mode.
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> setFrameRateM(fps)
|
||||
}
|
||||
}
|
||||
@@ -58,6 +61,13 @@ class FrameRateManager(
|
||||
getDisplayManager().unregisterDisplayListener(it)
|
||||
displayListener = null
|
||||
}
|
||||
// Restore default display mode on API M (preferredDisplayModeId persists)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
activity.window?.attributes?.let { attrs ->
|
||||
attrs.preferredDisplayModeId = 0
|
||||
activity.window?.attributes = attrs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDisplayListener() {
|
||||
@@ -79,13 +89,6 @@ class FrameRateManager(
|
||||
getDisplayManager().registerDisplayListener(displayListener, handler)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun setFrameRateR(fps: Float, surface: Surface) {
|
||||
Log.d(TAG, "setFrameRateR: Setting frame rate to $fps")
|
||||
surface.setFrameRate(fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
|
||||
registerDisplayListener()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private fun setFrameRateS(fps: Float, surface: Surface, videoDurationMs: Long) {
|
||||
Log.d(TAG, "setFrameRateS: fps=$fps, duration=${videoDurationMs}ms")
|
||||
@@ -162,7 +165,7 @@ class FrameRateManager(
|
||||
BigDecimal(mode.refreshRate.toString()).setScale(1, RoundingMode.FLOOR)) {
|
||||
modeToUse = mode
|
||||
break
|
||||
} else if (mode.refreshRate % fps == 0f) {
|
||||
} else if ((mode.refreshRate % fps).let { it < 0.1f || (fps - it) < 0.1f }) {
|
||||
modeToUse = mode
|
||||
break
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.edde746.plezy.shared
|
||||
|
||||
interface PlayerDelegate {
|
||||
fun onPropertyChange(name: String, value: Any?)
|
||||
fun onEvent(name: String, data: Map<String, Any>?)
|
||||
}
|
||||
@@ -3,11 +3,14 @@ package com.edde746.plezy.watchnext
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.tvprovider.media.tv.TvContractCompat
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Flutter plugin for Android TV Watch Next integration.
|
||||
@@ -17,7 +20,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WatchNextPlugin"
|
||||
private const val METHOD_CHANNEL = "app.plezy/watch_next"
|
||||
private const val METHOD_CHANNEL = "com.plezy/watch_next"
|
||||
|
||||
private var pendingDeepLink: String? = null
|
||||
|
||||
@@ -37,6 +40,8 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
private var applicationContext: Context? = null
|
||||
private var watchNextProvider: WatchNextProvider? = null
|
||||
private val ioExecutor by lazy { Executors.newSingleThreadExecutor() }
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
applicationContext = binding.applicationContext
|
||||
@@ -49,6 +54,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
applicationContext = null
|
||||
watchNextProvider = null
|
||||
ioExecutor.shutdown()
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
@@ -85,7 +91,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
val items = itemsData.mapNotNull { parseWatchNextItem(it) }
|
||||
result.success(provider.syncWatchNextPrograms(items))
|
||||
executeOnIo(result) { provider.syncWatchNextPrograms(items) }
|
||||
}
|
||||
|
||||
private fun handleClear(result: MethodChannel.Result) {
|
||||
@@ -94,7 +100,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.error("NOT_INITIALIZED", "WatchNextProvider not initialized", null)
|
||||
return
|
||||
}
|
||||
result.success(provider.clearAll())
|
||||
executeOnIo(result) { provider.clearAll() }
|
||||
}
|
||||
|
||||
private fun handleRemove(call: MethodCall, result: MethodChannel.Result) {
|
||||
@@ -109,7 +115,23 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.error("INVALID_ARGS", "Missing 'contentId' argument", null)
|
||||
return
|
||||
}
|
||||
result.success(provider.removeItem(contentId))
|
||||
executeOnIo(result) { provider.removeItem(contentId) }
|
||||
}
|
||||
|
||||
private fun executeOnIo(result: MethodChannel.Result, block: () -> Any?) {
|
||||
try {
|
||||
ioExecutor.execute {
|
||||
try {
|
||||
val value = block()
|
||||
mainHandler.post { result.success(value) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "IO operation failed: ${e.message}", e)
|
||||
mainHandler.post { result.error("IO_ERROR", e.message, null) }
|
||||
}
|
||||
}
|
||||
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
||||
result.error("SHUTDOWN", "Plugin is shutting down", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGetInitialDeepLink(result: MethodChannel.Result) {
|
||||
|
||||
@@ -20,9 +20,10 @@ platform :android do
|
||||
end
|
||||
|
||||
debug_info_dir = "./debug-info/#{version_name}+#{version_code}"
|
||||
git_commit = `git rev-parse --short HEAD`.strip
|
||||
|
||||
# Build the Flutter app
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true --obfuscate --split-debug-info=#{debug_info_dir}/aab")
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=play-store --obfuscate --split-debug-info=#{debug_info_dir}/aab")
|
||||
|
||||
upload_to_play_store(
|
||||
track: "production",
|
||||
@@ -31,8 +32,8 @@ platform :android do
|
||||
aab: "../build/app/outputs/bundle/release/app-release.aab"
|
||||
)
|
||||
|
||||
# Build universal APK for Amazon Appstore (doesn't accept AABs, doesn't auto-target by ABI)
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build apk --release --target-platform android-arm,android-arm64 --obfuscate --split-debug-info=#{debug_info_dir}/apk")
|
||||
# Build universal APK for Amazon Appstore (doesn't accept AABs, excludes x86 via env var)
|
||||
sh("cd #{ENV['PWD']}/.. && AMAZON=1 flutter build apk --release --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=amazon --obfuscate --split-debug-info=#{debug_info_dir}/apk")
|
||||
|
||||
upload_to_amazon_appstore(
|
||||
apk_paths: [
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="66.7" x2="185.04" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><linearGradient id="linear-gradient" y1="66.7" x2="185.04" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path fill="url(#linear-gradient)" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><circle cx="24" cy="24" r="24" fill="transparent"/><path fill="currentColor" d="M45.99 14.39 30.17 30.22l-1.46-1.46 16.31-16.32c-.15-.3-.32-.58-.5-.87l-17.2 17.2 2.16 2.16-1.46 1.46-3.62-3.62L43.34 9.82c-.18-.27-.38-.53-.6-.78L23.01 28.78l4.31 4.31-1.46 1.46-14.4-14.4 1.46-1.46 8.62 8.62L41.37 7.46c-.12-.14-.26-.28-.4-.42C36.62 2.69 30.62 0 24 0S11.38 2.69 7.03 7.03C2.69 11.37 0 17.37 0 24c0 13.25 10.75 24 24 24 6.62 0 12.62-2.69 16.97-7.03C45.31 36.64 48 30.64 48 24c0-3.42-.71-6.67-2.01-9.61ZM21.54 25.9l-7.93-7.92 1.46-1.46L23 24.44l-1.46 1.46Zm2.16-2.16-7.93-7.92 1.46-1.46 7.93 7.92-1.46 1.46Zm.3 19.65c-10.7 0-19.39-8.7-19.39-19.39S13.3 4.61 24 4.61c2.91 0 5.72.63 8.33 1.88l-.89 1.88c-2.33-1.11-4.84-1.68-7.43-1.68-9.55 0-17.33 7.77-17.33 17.32s7.78 17.33 17.33 17.33c6.82 0 13.03-4.03 15.82-10.25l1.89.84C38.6 38.9 31.65 43.41 24.01 43.41Z"/></svg>
|
||||
|
After Width: | Height: | Size: 921 B |
+28
-98
@@ -3,94 +3,50 @@ PODS:
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- cupertino_http (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- file_picker (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleMLKit/BarcodeScanning (7.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitBarcodeScanning (~> 6.0.0)
|
||||
- GoogleMLKit/MLKitCore (7.0.0):
|
||||
- MLKitCommon (~> 12.0.0)
|
||||
- GoogleToolboxForMac/Defines (4.2.1)
|
||||
- GoogleToolboxForMac/Logger (4.2.1):
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- MLImage (1.0.0-beta6)
|
||||
- MLKitBarcodeScanning (6.0.0):
|
||||
- MLKitCommon (~> 12.0)
|
||||
- MLKitVision (~> 8.0)
|
||||
- MLKitCommon (12.0.0):
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLKitVision (8.0.0):
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLImage (= 1.0.0-beta6)
|
||||
- MLKitCommon (~> 12.0)
|
||||
- mobile_scanner (6.0.2):
|
||||
- Flutter
|
||||
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- os_media_controls (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Sentry/HybridSDK (8.58.0)
|
||||
- sentry_flutter (9.16.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- Sentry/HybridSDK (= 8.58.0)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (3.51.1):
|
||||
- sqlite3/common (= 3.51.1)
|
||||
- sqlite3/common (3.51.1)
|
||||
- sqlite3/dbstatvtab (3.51.1):
|
||||
- sqlite3 (3.52.0):
|
||||
- sqlite3/common (= 3.52.0)
|
||||
- sqlite3/common (3.52.0)
|
||||
- sqlite3/dbstatvtab (3.52.0):
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.51.1):
|
||||
- sqlite3/fts5 (3.52.0):
|
||||
- sqlite3/common
|
||||
- sqlite3/math (3.51.1):
|
||||
- sqlite3/math (3.52.0):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.51.1):
|
||||
- sqlite3/perf-threadsafe (3.52.0):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.51.1):
|
||||
- sqlite3/rtree (3.52.0):
|
||||
- sqlite3/common
|
||||
- sqlite3/session (3.51.1):
|
||||
- sqlite3/session (3.52.0):
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.51.1)
|
||||
- sqlite3 (~> 3.52.0)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/math
|
||||
@@ -103,41 +59,28 @@ PODS:
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- workmanager_apple (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
||||
- os_media_controls (from `.symlinks/plugins/os_media_controls/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- universal_gamepad (from `.symlinks/plugins/universal_gamepad/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- GoogleDataTransport
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
- GoogleUtilities
|
||||
- GTMSessionFetcher
|
||||
- MLImage
|
||||
- MLKitBarcodeScanning
|
||||
- MLKitCommon
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- Sentry
|
||||
- sqlite3
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -145,6 +88,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/background_downloader/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
cupertino_http:
|
||||
:path: ".symlinks/plugins/cupertino_http/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
@@ -153,14 +98,12 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
||||
os_media_controls:
|
||||
:path: ".symlinks/plugins/os_media_controls/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
@@ -173,39 +116,26 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
workmanager_apple:
|
||||
:path: ".symlinks/plugins/workmanager_apple/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
|
||||
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
||||
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
||||
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
||||
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
||||
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
|
||||
sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
|
||||
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
|
||||
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
|
||||
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
|
||||
universal_gamepad: e10172778a8a399cce234494968f38724974919e
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||
|
||||
PODFILE CHECKSUM: 16bb7f67e16d8aaa2bfb97666803fbfb291b6559
|
||||
|
||||
|
||||
@@ -10,15 +10,18 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
50DEEA8E23D1B433DE5341D6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C12D873DE8572579684A82C /* Pods_Runner.framework */; };
|
||||
69E1B4BB161DC9D84D1DFC62 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB346A1D0705AB171F80B11B /* Pods_Runner.framework */; };
|
||||
6A48DFB02EA70C7100C1F7CD /* plezy.icon in Resources */ = {isa = PBXBuildFile; fileRef = 6A48DFAF2EA70C7100C1F7CD /* plezy.icon */; };
|
||||
6A8A46212EDB320D0057B88C /* MPVKit-GPL in Frameworks */ = {isa = PBXBuildFile; productRef = 6A8A46202EDB320D0057B88C /* MPVKit-GPL */; };
|
||||
6A8A46212EDB320D0057B88C /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6A8A46202EDB320D0057B88C /* MPVKit */; };
|
||||
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */; };
|
||||
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
748A259343D7FF8DFD2C3910 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75D2EEFFFFE26C0F3CD48133 /* Pods_RunnerTests.framework */; };
|
||||
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F969587D0E464D999910F4 /* MpvPipController.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
F37BB572E0F0606E20596E43 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3469ED9D4F3E58F8C3AEB7DC /* Pods_RunnerTests.framework */; };
|
||||
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */; };
|
||||
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -47,29 +50,32 @@
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
1E1EC7646F3085BF9127AB2A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
2C12D873DE8572579684A82C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
28ABDA890783B15438AA8137 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3469ED9D4F3E58F8C3AEB7DC /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3A9EF9720208222F20B1495D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
5B81FA7048BFA976F49D555E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
6A48DFAF2EA70C7100C1F7CD /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
|
||||
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
|
||||
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
75D2EEFFFFE26C0F3CD48133 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
792502911151E64A2B752567 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
83B9EB35C25EEF40090F6A88 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
87ADF6219C65A13967DFF03C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
7F4B79C5218D46EDB4E433B0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
92F969587D0E464D999910F4 /* MpvPipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPipController.swift; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A8738BEC00467F05948734DD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
D239CED757CED630C2B0A161 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = SOURCE_ROOT; };
|
||||
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = SOURCE_ROOT; };
|
||||
BB346A1D0705AB171F80B11B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DE48FF2E074644167608B23D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
F3553CDE70191F0FC81A782E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -77,7 +83,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F37BB572E0F0606E20596E43 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
748A259343D7FF8DFD2C3910 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -85,8 +91,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50DEEA8E23D1B433DE5341D6 /* Pods_Runner.framework in Frameworks */,
|
||||
6A8A46212EDB320D0057B88C /* MPVKit-GPL in Frameworks */,
|
||||
6A8A46212EDB320D0057B88C /* MPVKit in Frameworks */,
|
||||
69E1B4BB161DC9D84D1DFC62 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -101,11 +107,11 @@
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67716449E04A72AA71A90156 /* Frameworks */ = {
|
||||
3DBA2EB0C0219E797C60DE48 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2C12D873DE8572579684A82C /* Pods_Runner.framework */,
|
||||
3469ED9D4F3E58F8C3AEB7DC /* Pods_RunnerTests.framework */,
|
||||
BB346A1D0705AB171F80B11B /* Pods_Runner.framework */,
|
||||
75D2EEFFFFE26C0F3CD48133 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -113,8 +119,11 @@
|
||||
6A8A46242EDB370C0057B88C /* MpvPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */,
|
||||
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */,
|
||||
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */,
|
||||
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */,
|
||||
92F969587D0E464D999910F4 /* MpvPipController.swift */,
|
||||
);
|
||||
path = MpvPlayer;
|
||||
sourceTree = "<group>";
|
||||
@@ -122,12 +131,12 @@
|
||||
875C8AEFBBA3759A1849D674 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8738BEC00467F05948734DD /* Pods-Runner.debug.xcconfig */,
|
||||
83B9EB35C25EEF40090F6A88 /* Pods-Runner.release.xcconfig */,
|
||||
1E1EC7646F3085BF9127AB2A /* Pods-Runner.profile.xcconfig */,
|
||||
5B81FA7048BFA976F49D555E /* Pods-RunnerTests.debug.xcconfig */,
|
||||
D239CED757CED630C2B0A161 /* Pods-RunnerTests.release.xcconfig */,
|
||||
87ADF6219C65A13967DFF03C /* Pods-RunnerTests.profile.xcconfig */,
|
||||
7F4B79C5218D46EDB4E433B0 /* Pods-Runner.debug.xcconfig */,
|
||||
F3553CDE70191F0FC81A782E /* Pods-Runner.release.xcconfig */,
|
||||
DE48FF2E074644167608B23D /* Pods-Runner.profile.xcconfig */,
|
||||
28ABDA890783B15438AA8137 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
3A9EF9720208222F20B1495D /* Pods-RunnerTests.release.xcconfig */,
|
||||
792502911151E64A2B752567 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -151,8 +160,8 @@
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
875C8AEFBBA3759A1849D674 /* Pods */,
|
||||
67716449E04A72AA71A90156 /* Frameworks */,
|
||||
6A48DFAF2EA70C7100C1F7CD /* plezy.icon */,
|
||||
3DBA2EB0C0219E797C60DE48 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -187,7 +196,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
684A4A968F313A7FDAEBF3B8 /* [CP] Check Pods Manifest.lock */,
|
||||
87A3350E66D519725E5CD790 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
582EBCC1C90BEBEC365D972D /* Frameworks */,
|
||||
@@ -206,15 +215,14 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
01D58DD9E289D97FD8DBE66E /* [CP] Check Pods Manifest.lock */,
|
||||
6838C6976E568CE05DBDE228 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
2E10BAE564A4AFFCFF5B4EF0 /* [CP] Embed Pods Frameworks */,
|
||||
BE3E47DA555D39F032C6A1DB /* [CP] Copy Pods Resources */,
|
||||
898098BAEC57934A6822754B /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -289,7 +297,23 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
01D58DD9E289D97FD8DBE66E /* [CP] Check Pods Manifest.lock */ = {
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
6838C6976E568CE05DBDE228 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -311,40 +335,7 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
2E10BAE564A4AFFCFF5B4EF0 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
684A4A968F313A7FDAEBF3B8 /* [CP] Check Pods Manifest.lock */ = {
|
||||
87A3350E66D519725E5CD790 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -366,6 +357,23 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
898098BAEC57934A6822754B /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -381,23 +389,6 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
BE3E47DA555D39F032C6A1DB /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -413,8 +404,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */,
|
||||
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */,
|
||||
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */,
|
||||
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */,
|
||||
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */,
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
@@ -530,7 +524,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5B81FA7048BFA976F49D555E /* Pods-RunnerTests.debug.xcconfig */;
|
||||
baseConfigurationReference = 28ABDA890783B15438AA8137 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -548,7 +542,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D239CED757CED630C2B0A161 /* Pods-RunnerTests.release.xcconfig */;
|
||||
baseConfigurationReference = 3A9EF9720208222F20B1495D /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -564,7 +558,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 87ADF6219C65A13967DFF03C /* Pods-RunnerTests.profile.xcconfig */;
|
||||
baseConfigurationReference = 792502911151E64A2B752567 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -783,16 +777,16 @@
|
||||
repositoryURL = "https://github.com/edde746/MPVKit";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = 2e887368b44ce1dc9e1649e7757ec62c2564e792;
|
||||
revision = b2179eec26c6ceda339e6c6b512e457341768761;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
6A8A46202EDB320D0057B88C /* MPVKit-GPL */ = {
|
||||
6A8A46202EDB320D0057B88C /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6A8A461F2EDB320D0057B88C /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = "MPVKit-GPL";
|
||||
productName = MPVKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/edde746/MPVKit",
|
||||
"state" : {
|
||||
"revision" : "2e887368b44ce1dc9e1649e7757ec62c2564e792"
|
||||
"revision" : "b2179eec26c6ceda339e6c6b512e457341768761"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/edde746/MPVKit",
|
||||
"state" : {
|
||||
"revision" : "2e887368b44ce1dc9e1649e7757ec62c2564e792"
|
||||
"revision" : "b2179eec26c6ceda339e6c6b512e457341768761"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -3,18 +3,11 @@ import UIKit
|
||||
import AVFoundation
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
|
||||
// Register MPV player plugin
|
||||
if let registrar = self.registrar(forPlugin: "MpvPlayerPlugin") {
|
||||
MpvPlayerPlugin.register(with: registrar)
|
||||
}
|
||||
|
||||
// Configure audio session for media playback
|
||||
do {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
@@ -28,4 +21,13 @@ import AVFoundation
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
|
||||
// Register MPV player plugin
|
||||
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "MpvPlayerPlugin") {
|
||||
MpvPlayerPlugin.register(with: registrar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<string>Main</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
@@ -66,5 +67,26 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import AVKit
|
||||
import UIKit
|
||||
|
||||
/// Delegate to notify the plugin of PiP lifecycle events
|
||||
protocol MpvPipDelegate: AnyObject {
|
||||
/// Called when PiP is about to start (system or app-initiated)
|
||||
func pipWillStart()
|
||||
func pipDidStart()
|
||||
/// Called when PiP stops. `restored` is true if the user pressed maximize (restore UI).
|
||||
func pipDidStop(restored: Bool)
|
||||
func pipDidFailToStart(error: Error?)
|
||||
/// Forward play/pause commands from PiP overlay to mpv
|
||||
func pipSetPlaying(_ playing: Bool)
|
||||
/// Forward skip forward/backward commands from PiP overlay to mpv
|
||||
func pipSkip(byInterval seconds: Double)
|
||||
/// Query whether mpv is currently playing
|
||||
var isPipPlaying: Bool { get }
|
||||
/// Get total duration in seconds
|
||||
var pipDuration: Double { get }
|
||||
}
|
||||
|
||||
/// Encapsulates all iOS Picture-in-Picture logic using AVSampleBufferDisplayLayer.
|
||||
/// Requires iOS 15+ for the ContentSource API; on older versions, `setup()` is a no-op.
|
||||
class MpvPipController: NSObject {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var pipController: AVPictureInPictureController?
|
||||
private let sampleBufferLayer = AVSampleBufferDisplayLayer()
|
||||
private var containerView: UIView?
|
||||
weak var delegate: MpvPipDelegate?
|
||||
|
||||
/// Pointer to the sample buffer layer for passing to mpv as `wid`
|
||||
var layerPointer: UnsafeMutableRawPointer {
|
||||
Unmanaged.passUnretained(sampleBufferLayer).toOpaque()
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
setup()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
guard #available(iOS 15.0, *) else { return }
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
} catch {
|
||||
print("[MpvPipController] Failed to configure audio session: \(error)")
|
||||
}
|
||||
|
||||
// The sample buffer layer must be in a visible view hierarchy for
|
||||
// isPictureInPicturePossible to become true.
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
||||
let view = UIView(frame: window.bounds)
|
||||
view.clipsToBounds = true
|
||||
view.isUserInteractionEnabled = false
|
||||
sampleBufferLayer.frame = view.bounds
|
||||
view.layer.addSublayer(sampleBufferLayer)
|
||||
window.addSubview(view)
|
||||
containerView = view
|
||||
}
|
||||
|
||||
createPipController()
|
||||
}
|
||||
|
||||
/// Helper that conforms to the iOS 15+ delegate protocols
|
||||
private var delegateHelper: AnyObject?
|
||||
|
||||
private func createPipController() {
|
||||
guard #available(iOS 15.0, *) else { return }
|
||||
let helper = PipDelegateHelper(controller: self)
|
||||
let contentSource = AVPictureInPictureController.ContentSource(
|
||||
sampleBufferDisplayLayer: sampleBufferLayer,
|
||||
playbackDelegate: helper
|
||||
)
|
||||
self.delegateHelper = helper
|
||||
pipController = AVPictureInPictureController(contentSource: contentSource)
|
||||
pipController?.delegate = helper
|
||||
if #available(iOS 14.2, *) {
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable/disable system auto-PiP (starts PiP automatically on background transition)
|
||||
func setAutoStart(_ enabled: Bool) {
|
||||
guard #available(iOS 14.2, *) else { return }
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled
|
||||
}
|
||||
|
||||
/// Push a black frame to the sample buffer layer so PiP has content
|
||||
/// to display immediately (before vo_pip decodes the first real frame).
|
||||
func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) {
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
let attrs: [String: Any] = [
|
||||
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
|
||||
]
|
||||
let status = CVPixelBufferCreate(
|
||||
kCFAllocatorDefault, Int(width), Int(height),
|
||||
kCVPixelFormatType_32BGRA, attrs as CFDictionary, &pixelBuffer
|
||||
)
|
||||
guard status == kCVReturnSuccess, let pb = pixelBuffer else { return }
|
||||
|
||||
// Fill with black
|
||||
CVPixelBufferLockBaseAddress(pb, [])
|
||||
if let base = CVPixelBufferGetBaseAddress(pb) {
|
||||
memset(base, 0, CVPixelBufferGetDataSize(pb))
|
||||
}
|
||||
CVPixelBufferUnlockBaseAddress(pb, [])
|
||||
|
||||
// Use current timebase time for PTS (if available) so the frame isn't stale
|
||||
let pts: CMTime
|
||||
if let tb = sampleBufferLayer.controlTimebase {
|
||||
pts = CMTimebaseGetTime(tb)
|
||||
} else {
|
||||
pts = CMTime(value: 0, timescale: 30)
|
||||
}
|
||||
|
||||
var timing = CMSampleTimingInfo(
|
||||
duration: CMTime(value: 1, timescale: 30),
|
||||
presentationTimeStamp: pts,
|
||||
decodeTimeStamp: .invalid
|
||||
)
|
||||
|
||||
// Create format description and sample buffer
|
||||
var formatDesc: CMVideoFormatDescription?
|
||||
CMVideoFormatDescriptionCreateForImageBuffer(
|
||||
allocator: kCFAllocatorDefault,
|
||||
imageBuffer: pb,
|
||||
formatDescriptionOut: &formatDesc
|
||||
)
|
||||
guard let fmt = formatDesc else { return }
|
||||
|
||||
var sampleBuffer: CMSampleBuffer?
|
||||
CMSampleBufferCreateReadyWithImageBuffer(
|
||||
allocator: kCFAllocatorDefault,
|
||||
imageBuffer: pb,
|
||||
formatDescription: fmt,
|
||||
sampleTiming: &timing,
|
||||
sampleBufferOut: &sampleBuffer
|
||||
)
|
||||
guard let sb = sampleBuffer else { return }
|
||||
|
||||
// Set DisplayImmediately so it shows regardless of timebase timing
|
||||
if let attachments = CMSampleBufferGetSampleAttachmentsArray(sb, createIfNecessary: true) as? [NSMutableDictionary],
|
||||
let dict = attachments.first {
|
||||
dict[kCMSampleAttachmentKey_DisplayImmediately] = true
|
||||
}
|
||||
|
||||
sampleBufferLayer.enqueue(sb)
|
||||
}
|
||||
|
||||
/// Sync the layer's controlTimebase with the actual playback position.
|
||||
/// This makes the PiP progress bar show the correct time.
|
||||
func syncTimebase(currentTime: Double, isPlaying: Bool) {
|
||||
guard let timebase = sampleBufferLayer.controlTimebase else { return }
|
||||
let cmTime = CMTime(seconds: currentTime, preferredTimescale: 1000)
|
||||
CMTimebaseSetTime(timebase, time: cmTime)
|
||||
CMTimebaseSetRate(timebase, rate: isPlaying ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
/// Ensure the layer has a timebase and blank frame so the system considers
|
||||
/// PiP possible (required for canStartPictureInPictureAutomaticallyFromInline).
|
||||
func warmLayer(currentTime: Double, isPlaying: Bool) {
|
||||
if sampleBufferLayer.controlTimebase == nil {
|
||||
var timebase: CMTimebase?
|
||||
CMTimebaseCreateWithSourceClock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
sourceClock: CMClockGetHostTimeClock(),
|
||||
timebaseOut: &timebase
|
||||
)
|
||||
if let tb = timebase {
|
||||
sampleBufferLayer.controlTimebase = tb
|
||||
}
|
||||
}
|
||||
syncTimebase(currentTime: currentTime, isPlaying: isPlaying)
|
||||
pushBlankFrame()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
static var isSupported: Bool {
|
||||
guard #available(iOS 15.0, *) else { return false }
|
||||
return AVPictureInPictureController.isPictureInPictureSupported()
|
||||
}
|
||||
|
||||
/// Start PiP. When `waitForFrame` is false (auto-PiP), skips the frame
|
||||
/// readiness check since the scene is about to deactivate.
|
||||
func startPip(waitForFrame: Bool = true, completion: @escaping (Bool) -> Void) {
|
||||
guard let pipController = pipController else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
var attempts = 0
|
||||
func tryStart() {
|
||||
let possible = pipController.isPictureInPicturePossible
|
||||
let hasTimebase = sampleBufferLayer.controlTimebase != nil
|
||||
|
||||
let hasFrame: Bool
|
||||
if !waitForFrame {
|
||||
hasFrame = true // Skip frame check for auto-PiP
|
||||
} else if #available(iOS 17.4, *) {
|
||||
hasFrame = sampleBufferLayer.isReadyForDisplay
|
||||
} else {
|
||||
hasFrame = true
|
||||
}
|
||||
|
||||
if possible && hasTimebase && hasFrame {
|
||||
print("[MpvPipController] vo_pip ready after \(attempts) retries, starting PiP")
|
||||
pipController.startPictureInPicture()
|
||||
completion(true)
|
||||
} else if attempts < 40 {
|
||||
attempts += 1
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { tryStart() }
|
||||
} else {
|
||||
print("[MpvPipController] PiP not ready after \(attempts) retries (possible=\(possible), timebase=\(hasTimebase))")
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
tryStart()
|
||||
}
|
||||
|
||||
func stopPip() {
|
||||
pipController?.stopPictureInPicture()
|
||||
}
|
||||
|
||||
/// Invalidate the playback state so PiP updates its UI (play/pause button)
|
||||
func invalidatePlaybackState() {
|
||||
pipController?.invalidatePlaybackState()
|
||||
}
|
||||
|
||||
/// Fully tear down PiP — removes the container view from the window and
|
||||
/// destroys the AVPictureInPictureController so the system can no longer
|
||||
/// trigger auto-PiP after the player is disposed.
|
||||
func teardown() {
|
||||
pipController?.stopPictureInPicture()
|
||||
if #available(iOS 14.2, *) {
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
}
|
||||
pipController = nil
|
||||
delegateHelper = nil
|
||||
sampleBufferLayer.flushAndRemoveImage()
|
||||
sampleBufferLayer.controlTimebase = nil
|
||||
sampleBufferLayer.removeFromSuperlayer()
|
||||
containerView?.removeFromSuperview()
|
||||
containerView = nil
|
||||
}
|
||||
|
||||
/// Flush enqueued sample buffers from the layer to free video frame memory
|
||||
func flushLayer() {
|
||||
sampleBufferLayer.flushAndRemoveImage()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PiP Delegate Helper (iOS 15+)
|
||||
|
||||
/// Separate class conforming to AVPictureInPictureControllerDelegate and
|
||||
/// AVPictureInPictureSampleBufferPlaybackDelegate since these require iOS 15+
|
||||
/// availability for the ContentSource-based delegate methods.
|
||||
@available(iOS 15.0, *)
|
||||
private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate,
|
||||
AVPictureInPictureSampleBufferPlaybackDelegate
|
||||
{
|
||||
weak var controller: MpvPipController?
|
||||
private var isRestoring = false
|
||||
|
||||
init(controller: MpvPipController) {
|
||||
self.controller = controller
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - AVPictureInPictureControllerDelegate
|
||||
|
||||
func pictureInPictureControllerWillStartPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
print("[MpvPipController] PiP will start")
|
||||
controller?.delegate?.pipWillStart()
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStartPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
print("[MpvPipController] PiP did start")
|
||||
controller?.delegate?.pipDidStart()
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
let restored = isRestoring
|
||||
isRestoring = false
|
||||
print("[MpvPipController] PiP did stop (restored: \(restored))")
|
||||
controller?.delegate?.pipDidStop(restored: restored)
|
||||
}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
failedToStartPictureInPictureWithError error: Error
|
||||
) {
|
||||
print("[MpvPipController] PiP failed to start: \(error)")
|
||||
controller?.delegate?.pipDidFailToStart(error: error)
|
||||
}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
||||
) {
|
||||
print("[MpvPipController] PiP restore user interface")
|
||||
isRestoring = true
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerWillStopPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
print("[MpvPipController] PiP will stop")
|
||||
}
|
||||
|
||||
// MARK: - AVPictureInPictureSampleBufferPlaybackDelegate
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
setPlaying playing: Bool
|
||||
) {
|
||||
print("[MpvPipController] PiP setPlaying: \(playing)")
|
||||
controller?.delegate?.pipSetPlaying(playing)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerTimeRangeForPlayback(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) -> CMTimeRange {
|
||||
let duration = controller?.delegate?.pipDuration ?? 0
|
||||
if duration > 0 {
|
||||
return CMTimeRange(
|
||||
start: .zero,
|
||||
duration: CMTime(seconds: duration, preferredTimescale: 1000)
|
||||
)
|
||||
}
|
||||
return CMTimeRange(start: .zero, duration: CMTime(seconds: 1, preferredTimescale: 1))
|
||||
}
|
||||
|
||||
func pictureInPictureControllerIsPlaybackPaused(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) -> Bool {
|
||||
return !(controller?.delegate?.isPipPlaying ?? false)
|
||||
}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
didTransitionToRenderSize newRenderSize: CMVideoDimensions
|
||||
) {}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
skipByInterval skipInterval: CMTime,
|
||||
completion completionHandler: @escaping () -> Void
|
||||
) {
|
||||
let seconds = CMTimeGetSeconds(skipInterval)
|
||||
print("[MpvPipController] PiP skip by \(seconds)s")
|
||||
controller?.delegate?.pipSkip(byInterval: seconds)
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,13 @@
|
||||
import Libmpv
|
||||
import UIKit
|
||||
|
||||
/// Protocol for receiving player events
|
||||
protocol MpvPlayerDelegate: AnyObject {
|
||||
func onPropertyChange(name: String, value: Any?)
|
||||
func onEvent(name: String, data: [String: Any]?)
|
||||
}
|
||||
/// Core MPV player using Metal rendering for iOS.
|
||||
class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
|
||||
// Workaround for MoltenVK problems that cause flicker
|
||||
// https://github.com/mpv-player/mpv/pull/13651
|
||||
private class MetalLayer: CAMetalLayer {
|
||||
override var drawableSize: CGSize {
|
||||
get { return super.drawableSize }
|
||||
set {
|
||||
if Int(newValue.width) > 1 && Int(newValue.height) > 1 {
|
||||
super.drawableSize = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix for target-colorspace-hint - needs main thread for EDR
|
||||
@available(iOS 16.0, *)
|
||||
override var wantsExtendedDynamicRangeContent: Bool {
|
||||
get { return super.wantsExtendedDynamicRangeContent }
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
super.wantsExtendedDynamicRangeContent = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
super.wantsExtendedDynamicRangeContent = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Core MPV player using Metal rendering for iOS
|
||||
class MpvPlayerCore: NSObject {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var metalLayer: MetalLayer?
|
||||
private var containerView: UIView? // Container for proper EDR activation
|
||||
private var mpv: OpaquePointer?
|
||||
private var containerView: UIView?
|
||||
private weak var window: UIWindow?
|
||||
private lazy var queue = DispatchQueue(label: "mpv", qos: .userInitiated)
|
||||
|
||||
weak var delegate: MpvPlayerDelegate?
|
||||
|
||||
private(set) var isInitialized = false
|
||||
private var isDisposing = false // Flag to prevent race conditions during disposal
|
||||
|
||||
// HDR settings
|
||||
private var hdrEnabled = true // User preference for HDR
|
||||
private var lastSigPeak: Double = 0.0 // Last known sig-peak for re-evaluation
|
||||
|
||||
// Async command tracking to prevent UI blocking
|
||||
private var pendingCommands: [UInt64: (Result<Void, Error>) -> Void] = [:]
|
||||
private var pendingCommandsLock = NSLock()
|
||||
private var nextRequestId: UInt64 = 1
|
||||
|
||||
// MARK: - Initialization
|
||||
var isPipStarting = false
|
||||
|
||||
func initialize(in window: UIWindow) -> Bool {
|
||||
guard !isInitialized else {
|
||||
@@ -70,14 +17,11 @@ class MpvPlayerCore: NSObject {
|
||||
|
||||
self.window = window
|
||||
|
||||
// Create container view for proper EDR activation
|
||||
// EDR requires the CAMetalLayer to be in a UIView hierarchy, not just window.layer
|
||||
let container = UIView(frame: window.bounds)
|
||||
container.backgroundColor = .clear
|
||||
container.isUserInteractionEnabled = false
|
||||
|
||||
// Create Metal layer for video rendering
|
||||
let layer = MetalLayer()
|
||||
let layer = MpvMetalLayer()
|
||||
layer.frame = container.bounds
|
||||
layer.contentsScale = UIScreen.main.nativeScale
|
||||
layer.framebufferOnly = true
|
||||
@@ -87,13 +31,8 @@ class MpvPlayerCore: NSObject {
|
||||
containerView = container
|
||||
metalLayer = layer
|
||||
|
||||
// Add container view to window (behind Flutter's root view controller)
|
||||
window.insertSubview(container, at: 0)
|
||||
|
||||
print(
|
||||
"[MpvPlayerCore] Metal layer added to window via container view, frame: \(layer.frame)")
|
||||
|
||||
// Initialize MPV with this Metal layer
|
||||
guard setupMpv() else {
|
||||
print("[MpvPlayerCore] Failed to setup MPV")
|
||||
layer.removeFromSuperlayer()
|
||||
@@ -103,7 +42,6 @@ class MpvPlayerCore: NSObject {
|
||||
return false
|
||||
}
|
||||
|
||||
// Setup background/foreground notifications
|
||||
setupNotifications()
|
||||
|
||||
isInitialized = true
|
||||
@@ -111,61 +49,111 @@ class MpvPlayerCore: NSObject {
|
||||
return true
|
||||
}
|
||||
|
||||
private func setupMpv() -> Bool {
|
||||
guard let metalLayer = metalLayer else { return false }
|
||||
func switchToPipVO(layerPtr: UnsafeMutableRawPointer) -> Bool {
|
||||
guard let mpv else { return false }
|
||||
|
||||
mpv = mpv_create()
|
||||
guard mpv != nil else {
|
||||
print("[MpvPlayerCore] Failed to create MPV context")
|
||||
return false
|
||||
}
|
||||
print("[MpvPlayerCore] Switching to pip VO for PiP")
|
||||
|
||||
// Logging
|
||||
#if DEBUG
|
||||
checkError(mpv_request_log_messages(mpv, "info"))
|
||||
#else
|
||||
checkError(mpv_request_log_messages(mpv, "warn"))
|
||||
#endif
|
||||
metalLayer?.removeFromSuperlayer()
|
||||
|
||||
// Set the Metal layer as the render target (must use local var for &)
|
||||
var layer = metalLayer
|
||||
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
|
||||
mpv_set_property_string(mpv, "vid", "no")
|
||||
|
||||
// Video output settings for Metal/Vulkan
|
||||
checkError(mpv_set_option_string(mpv, "vo", "gpu-next"))
|
||||
checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan"))
|
||||
checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk"))
|
||||
checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox"))
|
||||
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes"))
|
||||
var pointer = Int64(Int(bitPattern: layerPtr))
|
||||
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &pointer)
|
||||
|
||||
// Initialize MPV
|
||||
let initResult = mpv_initialize(mpv)
|
||||
if initResult < 0 {
|
||||
print(
|
||||
"[MpvPlayerCore] mpv_initialize failed: \(String(cString: mpv_error_string(initResult)))"
|
||||
)
|
||||
mpv_terminate_destroy(mpv)
|
||||
mpv = nil
|
||||
return false
|
||||
}
|
||||
mpv_set_property_string(mpv, "vo", "pip")
|
||||
mpv_set_property_string(mpv, "vid", "auto")
|
||||
|
||||
// Set up wakeup callback for event handling
|
||||
mpv_set_wakeup_callback(
|
||||
mpv,
|
||||
{ ctx in
|
||||
guard let ctx = ctx else { return } // Safe guard instead of force unwrap
|
||||
let core = Unmanaged<MpvPlayerCore>.fromOpaque(ctx).takeUnretainedValue()
|
||||
core.readEvents()
|
||||
}, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
|
||||
|
||||
// Observe video-params/sig-peak for HDR detection
|
||||
mpv_observe_property(mpv, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE)
|
||||
|
||||
print("[MpvPlayerCore] MPV initialized successfully")
|
||||
print("[MpvPlayerCore] Switched to pip VO successfully")
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Background/Foreground Handling
|
||||
func switchToGpuNextVO() -> Bool {
|
||||
guard let mpv, let metalLayer else { return false }
|
||||
|
||||
print("[MpvPlayerCore] Switching back to gpu-next VO")
|
||||
|
||||
mpv_set_property_string(mpv, "vid", "no")
|
||||
|
||||
var layer = metalLayer
|
||||
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &layer)
|
||||
|
||||
applyGpuNextOptions()
|
||||
mpv_set_property_string(mpv, "vid", "auto")
|
||||
|
||||
if metalLayer.superlayer == nil, let containerView {
|
||||
containerView.layer.addSublayer(metalLayer)
|
||||
}
|
||||
|
||||
print("[MpvPlayerCore] Switched back to gpu-next VO successfully")
|
||||
return true
|
||||
}
|
||||
|
||||
func setVisible(_ visible: Bool) {
|
||||
guard let containerView else { return }
|
||||
|
||||
if visible {
|
||||
containerView.removeFromSuperview()
|
||||
window?.insertSubview(containerView, at: 0)
|
||||
}
|
||||
|
||||
containerView.isHidden = !visible
|
||||
}
|
||||
|
||||
func updateFrame(_ frame: CGRect? = nil) {
|
||||
guard let metalLayer, let containerView else { return }
|
||||
|
||||
if let frame {
|
||||
containerView.frame = frame
|
||||
metalLayer.frame = containerView.bounds
|
||||
} else if let window {
|
||||
containerView.frame = window.bounds
|
||||
metalLayer.frame = containerView.bounds
|
||||
}
|
||||
|
||||
let scale = UIScreen.main.nativeScale
|
||||
metalLayer.drawableSize = CGSize(
|
||||
width: metalLayer.frame.width * scale,
|
||||
height: metalLayer.frame.height * scale
|
||||
)
|
||||
}
|
||||
|
||||
/// Nudge mpv to present the current paused frame after switching back from PiP.
|
||||
func forceDraw() {
|
||||
command(["seek", "0", "relative+exact"])
|
||||
}
|
||||
|
||||
override func updateEDRMode(sigPeak: Double) {
|
||||
guard let metalLayer else { return }
|
||||
|
||||
var edrHeadroom: CGFloat = 1.0
|
||||
if #available(iOS 16.0, *) {
|
||||
edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0
|
||||
metalLayer.wantsExtendedDynamicRangeContent =
|
||||
hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
|
||||
}
|
||||
|
||||
let shouldEnableEDR = hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
|
||||
print(
|
||||
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), headroom: \(edrHeadroom))"
|
||||
)
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
disposeSharedState(destroySynchronously: false)
|
||||
|
||||
metalLayer?.removeFromSuperlayer()
|
||||
metalLayer = nil
|
||||
containerView?.removeFromSuperview()
|
||||
containerView = nil
|
||||
isInitialized = false
|
||||
print("[MpvPlayerCore] Disposed")
|
||||
}
|
||||
|
||||
deinit {
|
||||
dispose()
|
||||
}
|
||||
|
||||
private func setupNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
@@ -183,7 +171,11 @@ class MpvPlayerCore: NSObject {
|
||||
}
|
||||
|
||||
@objc private func enterBackground() {
|
||||
// Disable video output to fix black screen when returning from background
|
||||
if isPipActive || isPipStarting {
|
||||
print("[MpvPlayerCore] Entering background - PiP active/starting, keeping video")
|
||||
return
|
||||
}
|
||||
|
||||
print("[MpvPlayerCore] Entering background - disabling video")
|
||||
if mpv != nil {
|
||||
mpv_set_option_string(mpv, "vid", "no")
|
||||
@@ -191,412 +183,14 @@ class MpvPlayerCore: NSObject {
|
||||
}
|
||||
|
||||
@objc private func enterForeground() {
|
||||
// Re-enable video output
|
||||
if isPipActive {
|
||||
print("[MpvPlayerCore] Entering foreground - PiP active, skipping vid restore")
|
||||
return
|
||||
}
|
||||
|
||||
print("[MpvPlayerCore] Entering foreground - enabling video")
|
||||
if mpv != nil {
|
||||
mpv_set_option_string(mpv, "vid", "auto")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MPV Properties and Commands
|
||||
|
||||
func setProperty(_ name: String, value: String) {
|
||||
guard mpv != nil else { return }
|
||||
|
||||
// Handle custom HDR toggle property
|
||||
if name == "hdr-enabled" {
|
||||
let enabled = value == "yes" || value == "true" || value == "1"
|
||||
setHDREnabled(enabled)
|
||||
return
|
||||
}
|
||||
|
||||
mpv_set_property_string(mpv, name, value)
|
||||
}
|
||||
|
||||
/// Enable or disable HDR mode
|
||||
func setHDREnabled(_ enabled: Bool) {
|
||||
hdrEnabled = enabled
|
||||
print("[MpvPlayerCore] HDR enabled: \(enabled)")
|
||||
|
||||
// Update MPV's target-colorspace-hint
|
||||
if mpv != nil {
|
||||
mpv_set_property_string(mpv, "target-colorspace-hint", enabled ? "yes" : "no")
|
||||
}
|
||||
|
||||
// Re-evaluate EDR mode with current sig-peak
|
||||
DispatchQueue.main.async {
|
||||
self.updateEDRMode(sigPeak: self.lastSigPeak)
|
||||
}
|
||||
}
|
||||
|
||||
func getProperty(_ name: String) -> String? {
|
||||
guard mpv != nil else { return nil }
|
||||
let cstr = mpv_get_property_string(mpv, name)
|
||||
defer { mpv_free(cstr) }
|
||||
return cstr.map { String(cString: $0) }
|
||||
}
|
||||
|
||||
func setLogLevel(_ level: String) {
|
||||
guard let mpv = mpv else { return }
|
||||
mpv_request_log_messages(mpv, level)
|
||||
}
|
||||
|
||||
func observeProperty(_ name: String, format: String) {
|
||||
guard mpv != nil else { return }
|
||||
|
||||
let mpvFormat: mpv_format
|
||||
switch format {
|
||||
case "double": mpvFormat = MPV_FORMAT_DOUBLE
|
||||
case "flag": mpvFormat = MPV_FORMAT_FLAG
|
||||
case "node": mpvFormat = MPV_FORMAT_NODE
|
||||
case "string": mpvFormat = MPV_FORMAT_STRING
|
||||
default: return
|
||||
}
|
||||
|
||||
mpv_observe_property(mpv, 0, name, mpvFormat)
|
||||
}
|
||||
|
||||
func command(_ args: [String]) {
|
||||
guard mpv != nil, !args.isEmpty else { return }
|
||||
command(args[0], args: Array(args.dropFirst()))
|
||||
}
|
||||
|
||||
/// Execute an MPV command asynchronously to prevent UI blocking.
|
||||
/// Uses mpv_command_async which returns immediately; the completion is called
|
||||
/// when MPV_EVENT_COMMAND_REPLY is received.
|
||||
func commandAsync(_ args: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let mpv = mpv, !args.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID
|
||||
pendingCommandsLock.lock()
|
||||
let requestId = nextRequestId
|
||||
nextRequestId += 1
|
||||
pendingCommands[requestId] = completion
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
// Build array of C strings for mpv_command_async
|
||||
var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
|
||||
cargs.append(nil) // null-terminate
|
||||
|
||||
// mpv_command_async returns immediately
|
||||
cargs.withUnsafeBufferPointer { buffer in
|
||||
var constPtrs = buffer.map { UnsafePointer($0) }
|
||||
let result = mpv_command_async(mpv, requestId, &constPtrs)
|
||||
if result < 0 {
|
||||
// Command submission failed, complete immediately with error
|
||||
pendingCommandsLock.lock()
|
||||
if let pending = pendingCommands.removeValue(forKey: requestId) {
|
||||
pendingCommandsLock.unlock()
|
||||
let error = NSError(domain: "mpv", code: Int(result),
|
||||
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(result))])
|
||||
DispatchQueue.main.async { pending(.failure(error)) }
|
||||
} else {
|
||||
pendingCommandsLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Free the C strings
|
||||
for ptr in cargs {
|
||||
free(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Visibility
|
||||
|
||||
func setVisible(_ visible: Bool) {
|
||||
guard let container = containerView else { return }
|
||||
|
||||
if visible {
|
||||
// Re-insert at the bottom of the window view stack
|
||||
container.removeFromSuperview()
|
||||
window?.insertSubview(container, at: 0)
|
||||
}
|
||||
|
||||
container.isHidden = !visible
|
||||
print("[MpvPlayerCore] setVisible(\(visible))")
|
||||
}
|
||||
|
||||
func updateFrame(_ frame: CGRect? = nil) {
|
||||
guard let metalLayer = metalLayer, let container = containerView else { return }
|
||||
|
||||
if let frame = frame {
|
||||
container.frame = frame
|
||||
metalLayer.frame = container.bounds
|
||||
} else if let window = window {
|
||||
container.frame = window.bounds
|
||||
metalLayer.frame = container.bounds
|
||||
}
|
||||
|
||||
// Update drawable size for proper scaling
|
||||
let scale = UIScreen.main.nativeScale
|
||||
metalLayer.drawableSize = CGSize(
|
||||
width: metalLayer.frame.width * scale,
|
||||
height: metalLayer.frame.height * scale
|
||||
)
|
||||
|
||||
print("[MpvPlayerCore] updateFrame: \(container.frame)")
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func command(_ cmd: String, args: [String] = []) {
|
||||
guard mpv != nil else { return }
|
||||
|
||||
// Build array of C strings for mpv_command
|
||||
var cargs: [UnsafeMutablePointer<CChar>?] = ([cmd] + args).map { strdup($0) }
|
||||
cargs.append(nil) // null-terminate
|
||||
defer {
|
||||
for ptr in cargs {
|
||||
free(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
// mpv_command expects UnsafePointer, use withUnsafeBufferPointer for the conversion
|
||||
cargs.withUnsafeBufferPointer { buffer in
|
||||
var constPtrs = buffer.map { UnsafePointer($0) }
|
||||
_ = mpv_command(mpv, &constPtrs)
|
||||
}
|
||||
}
|
||||
|
||||
private func readEvents() {
|
||||
queue.async { [weak self] in
|
||||
guard let self = self, !self.isDisposing, let mpv = self.mpv else { return }
|
||||
|
||||
while true {
|
||||
let event = mpv_wait_event(mpv, 0)
|
||||
guard let eventPtr = event else { break }
|
||||
|
||||
if eventPtr.pointee.event_id == MPV_EVENT_NONE {
|
||||
break
|
||||
}
|
||||
|
||||
self.handleEvent(eventPtr.pointee)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEvent(_ event: mpv_event) {
|
||||
switch event.event_id {
|
||||
case MPV_EVENT_PROPERTY_CHANGE:
|
||||
guard let data = event.data else { break }
|
||||
let property = data.assumingMemoryBound(to: mpv_event_property.self).pointee
|
||||
let name = String(cString: property.name)
|
||||
handlePropertyChange(name: name, property: property)
|
||||
|
||||
case MPV_EVENT_COMMAND_REPLY:
|
||||
// Handle async command completion
|
||||
let requestId = event.reply_userdata
|
||||
pendingCommandsLock.lock()
|
||||
let completion = pendingCommands.removeValue(forKey: requestId)
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
if let completion = completion {
|
||||
if event.error < 0 {
|
||||
let error = NSError(domain: "mpv", code: Int(event.error),
|
||||
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(event.error))])
|
||||
DispatchQueue.main.async { completion(.failure(error)) }
|
||||
} else {
|
||||
DispatchQueue.main.async { completion(.success(())) }
|
||||
}
|
||||
}
|
||||
|
||||
case MPV_EVENT_FILE_LOADED:
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onEvent(name: "file-loaded", data: nil)
|
||||
}
|
||||
|
||||
case MPV_EVENT_END_FILE:
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onEvent(name: "end-file", data: nil)
|
||||
}
|
||||
|
||||
case MPV_EVENT_SHUTDOWN:
|
||||
print("[MpvPlayerCore] MPV shutdown event")
|
||||
|
||||
case MPV_EVENT_PLAYBACK_RESTART:
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onEvent(name: "playback-restart", data: nil)
|
||||
}
|
||||
|
||||
case MPV_EVENT_LOG_MESSAGE:
|
||||
if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
|
||||
let msg = msgPtr.pointee
|
||||
let prefix = msg.prefix.map { String(cString: $0) } ?? ""
|
||||
let level = msg.level.map { String(cString: $0) } ?? ""
|
||||
let text = msg.text.map { String(cString: $0) } ?? ""
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onEvent(name: "log-message", data: [
|
||||
"prefix": prefix,
|
||||
"level": level,
|
||||
"text": text
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePropertyChange(name: String, property: mpv_event_property) {
|
||||
var value: Any?
|
||||
|
||||
switch property.format {
|
||||
case MPV_FORMAT_DOUBLE:
|
||||
if let ptr = property.data {
|
||||
value = ptr.assumingMemoryBound(to: Double.self).pointee
|
||||
}
|
||||
|
||||
case MPV_FORMAT_FLAG:
|
||||
if let ptr = property.data {
|
||||
value = ptr.assumingMemoryBound(to: Int32.self).pointee != 0
|
||||
}
|
||||
|
||||
case MPV_FORMAT_NODE:
|
||||
if let ptr = property.data {
|
||||
let node = ptr.assumingMemoryBound(to: mpv_node.self).pointee
|
||||
value = convertNode(node)
|
||||
}
|
||||
|
||||
case MPV_FORMAT_STRING:
|
||||
if let ptr = property.data {
|
||||
let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
|
||||
value = cstr.map { String(cString: $0) }
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Handle sig-peak for HDR/EDR activation
|
||||
if name == "video-params/sig-peak", let sigPeak = value as? Double {
|
||||
lastSigPeak = sigPeak
|
||||
DispatchQueue.main.async {
|
||||
self.updateEDRMode(sigPeak: sigPeak)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onPropertyChange(name: name, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HDR/EDR Support
|
||||
|
||||
private func updateEDRMode(sigPeak: Double) {
|
||||
guard let layer = metalLayer else { return }
|
||||
|
||||
// Check if screen supports EDR (iOS 16+)
|
||||
var edrHeadroom: CGFloat = 1.0
|
||||
if #available(iOS 16.0, *) {
|
||||
edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0
|
||||
}
|
||||
|
||||
let isHDRContent = sigPeak > 1.0
|
||||
let screenSupportsEDR = edrHeadroom > 1.0
|
||||
let shouldEnableEDR = hdrEnabled && isHDRContent && screenSupportsEDR
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
layer.wantsExtendedDynamicRangeContent = shouldEnableEDR
|
||||
}
|
||||
|
||||
print(
|
||||
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), headroom: \(edrHeadroom))"
|
||||
)
|
||||
}
|
||||
|
||||
private func convertNode(_ node: mpv_node) -> Any? {
|
||||
switch node.format {
|
||||
case MPV_FORMAT_STRING:
|
||||
return node.u.string.map { String(cString: $0) }
|
||||
|
||||
case MPV_FORMAT_FLAG:
|
||||
return node.u.flag != 0
|
||||
|
||||
case MPV_FORMAT_INT64:
|
||||
return node.u.int64
|
||||
|
||||
case MPV_FORMAT_DOUBLE:
|
||||
return node.u.double_
|
||||
|
||||
case MPV_FORMAT_NODE_ARRAY:
|
||||
guard let list = node.u.list?.pointee else { return nil }
|
||||
var array = [Any]()
|
||||
for i in 0..<Int(list.num) {
|
||||
if let item = convertNode(list.values[i]) {
|
||||
array.append(item)
|
||||
}
|
||||
}
|
||||
return array
|
||||
|
||||
case MPV_FORMAT_NODE_MAP:
|
||||
guard let list = node.u.list?.pointee else { return nil }
|
||||
var dict = [String: Any]()
|
||||
for i in 0..<Int(list.num) {
|
||||
if let key = list.keys?[i].map({ String(cString: $0) }),
|
||||
let val = convertNode(list.values[i])
|
||||
{
|
||||
dict[key] = val
|
||||
}
|
||||
}
|
||||
return dict
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func checkError(_ status: CInt) {
|
||||
if status < 0 {
|
||||
print("[MpvPlayerCore] MPV error: \(String(cString: mpv_error_string(status)))")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
func dispose() {
|
||||
// Set disposing flag first to prevent race conditions with event callbacks
|
||||
isDisposing = true
|
||||
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
// Cancel any pending async commands
|
||||
pendingCommandsLock.lock()
|
||||
let pending = pendingCommands
|
||||
pendingCommands.removeAll()
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
// Complete pending commands with cancellation error
|
||||
let cancelError = NSError(domain: "mpv", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Player disposed"])
|
||||
for (_, completion) in pending {
|
||||
DispatchQueue.main.async { completion(.failure(cancelError)) }
|
||||
}
|
||||
|
||||
let mpvHandle = mpv
|
||||
mpv = nil
|
||||
|
||||
// Use async to avoid blocking the main thread (prevents deadlock)
|
||||
queue.async {
|
||||
if let handle = mpvHandle {
|
||||
mpv_set_wakeup_callback(handle, nil, nil)
|
||||
mpv_terminate_destroy(handle)
|
||||
}
|
||||
}
|
||||
metalLayer?.removeFromSuperlayer()
|
||||
metalLayer = nil
|
||||
containerView?.removeFromSuperview()
|
||||
containerView = nil
|
||||
isInitialized = false
|
||||
print("[MpvPlayerCore] Disposed")
|
||||
}
|
||||
|
||||
deinit {
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,65 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
import AVKit
|
||||
|
||||
/// Flutter plugin that bridges MPV player to Dart via method and event channels
|
||||
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerDelegate {
|
||||
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginShared {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var playerCore: MpvPlayerCore?
|
||||
private var eventSink: FlutterEventSink?
|
||||
var eventSink: FlutterEventSink?
|
||||
private weak var registrar: FlutterPluginRegistrar?
|
||||
private var nameToId: [String: Int] = [:]
|
||||
var nameToId: [String: Int] = [:]
|
||||
|
||||
// MpvPluginShared conformance
|
||||
var coreBase: MpvPlayerCoreBase? { playerCore }
|
||||
func setPlayerVisible(_ visible: Bool) { playerCore?.setVisible(visible) }
|
||||
func updatePlayerFrame() { playerCore?.updateFrame() }
|
||||
|
||||
// PiP
|
||||
private var pipController: MpvPipController?
|
||||
private var pipChannel: FlutterMethodChannel?
|
||||
private var autoPipEnabled = false
|
||||
private var isManualPipRequest = false
|
||||
private var pipTimebaseSyncTimer: Timer?
|
||||
private var pendingInlineRestoreAfterPip = false
|
||||
private var sceneActivationObserverRegistered = false
|
||||
|
||||
// MARK: - FlutterPlugin Registration
|
||||
|
||||
static func register(with registrar: FlutterPluginRegistrar) {
|
||||
// Method channel for commands
|
||||
let methodChannel = FlutterMethodChannel(
|
||||
name: "com.plezy/mpv_player",
|
||||
binaryMessenger: registrar.messenger()
|
||||
)
|
||||
|
||||
// Event channel for state updates
|
||||
let eventChannel = FlutterEventChannel(
|
||||
name: "com.plezy/mpv_player/events",
|
||||
binaryMessenger: registrar.messenger()
|
||||
)
|
||||
let pipChannel = FlutterMethodChannel(
|
||||
name: "com.plezy/pip",
|
||||
binaryMessenger: registrar.messenger()
|
||||
)
|
||||
|
||||
let instance = MpvPlayerPlugin()
|
||||
instance.registrar = registrar
|
||||
instance.pipChannel = pipChannel
|
||||
|
||||
registrar.addMethodCallDelegate(instance, channel: methodChannel)
|
||||
eventChannel.setStreamHandler(instance)
|
||||
|
||||
print("[MpvPlayerPlugin] Registered with Flutter")
|
||||
pipChannel.setMethodCallHandler(instance.handlePipCall)
|
||||
}
|
||||
|
||||
// MARK: - FlutterStreamHandler
|
||||
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
self.eventSink = events
|
||||
print("[MpvPlayerPlugin] Event stream connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
self.eventSink = nil
|
||||
print("[MpvPlayerPlugin] Event stream disconnected")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -55,40 +69,198 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
||||
switch call.method {
|
||||
case "initialize":
|
||||
handleInitialize(result: result)
|
||||
|
||||
case "dispose":
|
||||
handleDispose(result: result)
|
||||
|
||||
case "setProperty":
|
||||
handleSetProperty(call: call, result: result)
|
||||
|
||||
case "getProperty":
|
||||
handleGetProperty(call: call, result: result)
|
||||
|
||||
case "observeProperty":
|
||||
handleObserveProperty(call: call, result: result)
|
||||
|
||||
case "command":
|
||||
handleCommand(call: call, result: result)
|
||||
|
||||
case "setVisible":
|
||||
handleSetVisible(call: call, result: result)
|
||||
|
||||
case "isInitialized":
|
||||
result(playerCore?.isInitialized ?? false)
|
||||
|
||||
case "updateFrame":
|
||||
handleUpdateFrame(result: result)
|
||||
|
||||
case "setLogLevel":
|
||||
handleSetLogLevel(call: call, result: result)
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Method Handlers
|
||||
// MARK: - PiP
|
||||
|
||||
private func ensurePipController() -> MpvPipController {
|
||||
if let existing = pipController { return existing }
|
||||
let controller = MpvPipController()
|
||||
controller.delegate = self
|
||||
pipController = controller
|
||||
return controller
|
||||
}
|
||||
|
||||
private func registerSceneActivationObserver() {
|
||||
guard !sceneActivationObserverRegistered else { return }
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(sceneDidActivate),
|
||||
name: UIScene.didActivateNotification,
|
||||
object: nil
|
||||
)
|
||||
sceneActivationObserverRegistered = true
|
||||
}
|
||||
|
||||
private func unregisterSceneActivationObserver() {
|
||||
guard sceneActivationObserverRegistered else { return }
|
||||
NotificationCenter.default.removeObserver(self, name: UIScene.didActivateNotification, object: nil)
|
||||
sceneActivationObserverRegistered = false
|
||||
}
|
||||
|
||||
private var isSceneActive: Bool {
|
||||
UIApplication.shared.connectedScenes.contains { $0.activationState == .foregroundActive }
|
||||
}
|
||||
|
||||
private func restoreInlinePlayerAfterPip() {
|
||||
guard pendingInlineRestoreAfterPip,
|
||||
let playerCore = playerCore,
|
||||
!playerCore.isPipActive,
|
||||
!playerCore.isPipStarting else { return }
|
||||
|
||||
print("[MpvPlayerPlugin] Restoring inline player after PiP")
|
||||
playerCore.setVisible(true)
|
||||
playerCore.updateFrame()
|
||||
if playerCore.isPaused {
|
||||
playerCore.forceDraw()
|
||||
}
|
||||
pendingInlineRestoreAfterPip = false
|
||||
}
|
||||
|
||||
private func handlePipCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { result(nil); return }
|
||||
switch call.method {
|
||||
case "isSupported":
|
||||
result(MpvPipController.isSupported)
|
||||
case "enter":
|
||||
self.enterPip(manual: true, result: result)
|
||||
case "setAutoPipReady":
|
||||
if let args = call.arguments as? [String: Any], let ready = args["ready"] as? Bool {
|
||||
self.autoPipEnabled = ready
|
||||
if ready {
|
||||
let pip = self.ensurePipController()
|
||||
pip.setAutoStart(true)
|
||||
// Warm the layer so the system considers PiP possible
|
||||
if let pc = self.playerCore {
|
||||
pip.warmLayer(currentTime: pc.timePos, isPlaying: !pc.isPaused)
|
||||
}
|
||||
} else {
|
||||
self.pipController?.setAutoStart(false)
|
||||
}
|
||||
}
|
||||
result(nil)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to PiP VO and prepare the sample buffer layer for PiP display.
|
||||
/// Returns the MpvPipController on success, nil on failure.
|
||||
@discardableResult
|
||||
private func switchToPipAndPrepare() -> MpvPipController? {
|
||||
guard let playerCore = playerCore else { return nil }
|
||||
let pip = ensurePipController()
|
||||
guard playerCore.switchToPipVO(layerPtr: pip.layerPointer) else { return nil }
|
||||
pendingInlineRestoreAfterPip = false
|
||||
playerCore.isPipStarting = true
|
||||
pip.pushBlankFrame()
|
||||
pip.syncTimebase(currentTime: playerCore.timePos, isPlaying: !playerCore.isPaused)
|
||||
pip.invalidatePlaybackState()
|
||||
return pip
|
||||
}
|
||||
|
||||
/// Manual PiP entry (button press). Auto-PiP is handled by the system via
|
||||
/// canStartPictureInPictureAutomaticallyFromInline + pipWillStart delegate.
|
||||
private func enterPip(manual: Bool, result: FlutterResult? = nil) {
|
||||
guard MpvPipController.isSupported else {
|
||||
result?(["success": false, "errorCode": "ios_version", "errorMessage": "Requires iOS 15.0+"])
|
||||
return
|
||||
}
|
||||
guard playerCore != nil else {
|
||||
result?(["success": false, "errorCode": "failed", "errorMessage": "Player not initialized"])
|
||||
return
|
||||
}
|
||||
guard let pip = switchToPipAndPrepare() else {
|
||||
result?(["success": false, "errorCode": "vo_switch_failed", "errorMessage": "Failed to switch VO"])
|
||||
return
|
||||
}
|
||||
|
||||
isManualPipRequest = manual
|
||||
pip.startPip(waitForFrame: manual) { [weak self] started in
|
||||
if started {
|
||||
result?(["success": true])
|
||||
} else {
|
||||
self?.cleanupPip(notify: false)
|
||||
result?(["success": false, "errorCode": "failed", "errorMessage": "PiP failed to start"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified cleanup for all PiP exit paths
|
||||
private func cleanupPip(notify: Bool, pause: Bool = false) {
|
||||
playerCore?.isPipStarting = false
|
||||
playerCore?.isPipActive = false
|
||||
isManualPipRequest = false
|
||||
stopPipTimebaseSync()
|
||||
pipController?.flushLayer()
|
||||
let restoredInlineVO = playerCore?.switchToGpuNextVO() ?? false
|
||||
if pause { playerCore?.setProperty("pause", value: "yes") }
|
||||
pendingInlineRestoreAfterPip = restoredInlineVO
|
||||
if pendingInlineRestoreAfterPip {
|
||||
if isSceneActive {
|
||||
restoreInlinePlayerAfterPip()
|
||||
} else {
|
||||
print("[MpvPlayerPlugin] Deferring inline restore until scene activation")
|
||||
}
|
||||
}
|
||||
if notify { pipChannel?.invokeMethod("onPipChanged", arguments: false) }
|
||||
}
|
||||
|
||||
/// Scene became active — restore inline playback if needed and re-warm the
|
||||
/// sample-buffer layer so future auto-PiP remains possible.
|
||||
@objc private func sceneDidActivate() {
|
||||
restoreInlinePlayerAfterPip()
|
||||
if autoPipEnabled, let pip = pipController, let pc = playerCore, !pc.isPipActive {
|
||||
pip.warmLayer(currentTime: pc.timePos, isPlaying: !pc.isPaused)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timebase Sync
|
||||
|
||||
private func syncPipTimebase() {
|
||||
guard let playerCore = playerCore, let pipController = pipController else { return }
|
||||
pipController.syncTimebase(
|
||||
currentTime: playerCore.timePos,
|
||||
isPlaying: !playerCore.isPaused
|
||||
)
|
||||
}
|
||||
|
||||
private func startPipTimebaseSync() {
|
||||
stopPipTimebaseSync()
|
||||
pipTimebaseSyncTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
|
||||
self?.syncPipTimebase()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopPipTimebaseSync() {
|
||||
pipTimebaseSyncTimer?.invalidate()
|
||||
pipTimebaseSyncTimer = nil
|
||||
}
|
||||
|
||||
// MARK: - Platform-Specific Method Handlers
|
||||
|
||||
private func handleInitialize(result: @escaping FlutterResult) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
@@ -97,45 +269,43 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already initialized
|
||||
if self.playerCore?.isInitialized == true {
|
||||
print("[MpvPlayerPlugin] Already initialized")
|
||||
self.registerSceneActivationObserver()
|
||||
result(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the key window
|
||||
guard let window = self.findKeyWindow() else {
|
||||
print("[MpvPlayerPlugin] Failed to find key window")
|
||||
result(FlutterError(code: "NO_WINDOW", message: "Could not find key window", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
// Create and initialize player core
|
||||
let core = MpvPlayerCore()
|
||||
core.delegate = self
|
||||
|
||||
guard core.initialize(in: window) else {
|
||||
print("[MpvPlayerPlugin] Failed to initialize MPV")
|
||||
result(FlutterError(code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
self.playerCore = core
|
||||
|
||||
// Start hidden
|
||||
self.registerSceneActivationObserver()
|
||||
core.setVisible(false)
|
||||
|
||||
print("[MpvPlayerPlugin] Initialized successfully")
|
||||
result(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleDispose(result: @escaping FlutterResult) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.playerCore?.dispose()
|
||||
self?.playerCore = nil
|
||||
print("[MpvPlayerPlugin] Disposed")
|
||||
guard let self = self else { result(nil); return }
|
||||
self.pipController?.teardown()
|
||||
self.pipController = nil
|
||||
self.autoPipEnabled = false
|
||||
self.pendingInlineRestoreAfterPip = false
|
||||
self.unregisterSceneActivationObserver()
|
||||
self.stopPipTimebaseSync()
|
||||
self.playerCore?.dispose()
|
||||
self.playerCore = nil
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
@@ -149,109 +319,15 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
||||
}
|
||||
|
||||
playerCore?.setProperty(name, value: value)
|
||||
|
||||
if name == "pause" {
|
||||
pipController?.invalidatePlaybackState()
|
||||
if playerCore?.isPipActive == true { syncPipTimebase() }
|
||||
}
|
||||
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let name = args["name"] as? String else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
let value = playerCore?.getProperty(name)
|
||||
result(value)
|
||||
}
|
||||
|
||||
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let name = args["name"] as? String,
|
||||
let format = args["format"] as? String,
|
||||
let id = args["id"] as? Int else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
nameToId[name] = id
|
||||
playerCore?.observeProperty(name, format: format)
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let commandArgs = args["args"] as? [String] else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
// Use async command to prevent UI blocking during network operations
|
||||
playerCore?.commandAsync(commandArgs) { commandResult in
|
||||
switch commandResult {
|
||||
case .success:
|
||||
result(nil)
|
||||
case .failure(let error):
|
||||
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
|
||||
}
|
||||
} ?? result(nil)
|
||||
}
|
||||
|
||||
private func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let visible = args["visible"] as? Bool else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.playerCore?.setVisible(visible)
|
||||
|
||||
// Update frame when becoming visible
|
||||
if visible {
|
||||
self?.playerCore?.updateFrame()
|
||||
}
|
||||
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleUpdateFrame(result: @escaping FlutterResult) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.playerCore?.updateFrame()
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSetLogLevel(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let level = args["level"] as? String else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'level'", details: nil))
|
||||
return
|
||||
}
|
||||
playerCore?.setLogLevel(level)
|
||||
result(nil)
|
||||
}
|
||||
|
||||
// MARK: - MpvPlayerDelegate
|
||||
|
||||
func onPropertyChange(name: String, value: Any?) {
|
||||
guard let eventSink = eventSink else { return }
|
||||
|
||||
if let propId = nameToId[name] {
|
||||
eventSink([propId, value as Any])
|
||||
}
|
||||
}
|
||||
|
||||
func onEvent(name: String, data: [String: Any]?) {
|
||||
guard let eventSink = eventSink else { return }
|
||||
|
||||
var event: [String: Any] = ["type": "event", "name": name]
|
||||
if let data = data {
|
||||
event["data"] = data
|
||||
}
|
||||
|
||||
eventSink(event)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func findKeyWindow() -> UIWindow? {
|
||||
@@ -262,3 +338,59 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
||||
return window
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MpvPipDelegate
|
||||
|
||||
extension MpvPlayerPlugin: MpvPipDelegate {
|
||||
|
||||
func pipWillStart() {
|
||||
// If PiP was system-initiated (not via our enterPip), switch VO now
|
||||
guard let playerCore = playerCore, !playerCore.isPipStarting else { return }
|
||||
print("[MpvPlayerPlugin] System-initiated PiP detected, switching VO")
|
||||
if switchToPipAndPrepare() == nil {
|
||||
print("[MpvPlayerPlugin] VO switch failed for system-initiated PiP")
|
||||
pipController?.stopPip()
|
||||
}
|
||||
}
|
||||
|
||||
func pipDidStart() {
|
||||
playerCore?.isPipStarting = false
|
||||
playerCore?.isPipActive = true
|
||||
pendingInlineRestoreAfterPip = false
|
||||
pipChannel?.invokeMethod("onPipChanged", arguments: true)
|
||||
syncPipTimebase()
|
||||
startPipTimebaseSync()
|
||||
|
||||
if isManualPipRequest {
|
||||
isManualPipRequest = false
|
||||
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func pipDidStop(restored: Bool) {
|
||||
cleanupPip(notify: true, pause: !restored)
|
||||
}
|
||||
|
||||
func pipDidFailToStart(error: Error?) {
|
||||
cleanupPip(notify: true)
|
||||
}
|
||||
|
||||
func pipSetPlaying(_ playing: Bool) {
|
||||
playerCore?.setProperty("pause", value: playing ? "no" : "yes")
|
||||
pipController?.invalidatePlaybackState()
|
||||
syncPipTimebase()
|
||||
}
|
||||
|
||||
func pipSkip(byInterval seconds: Double) {
|
||||
guard let playerCore = playerCore else { return }
|
||||
let newTime = max(0, playerCore.timePos + seconds)
|
||||
playerCore.command(["seek", String(newTime), "absolute"])
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.syncPipTimebase()
|
||||
self?.pipController?.invalidatePlaybackState()
|
||||
}
|
||||
}
|
||||
|
||||
var isPipPlaying: Bool { !(playerCore?.isPaused ?? true) }
|
||||
var pipDuration: Double { playerCore?.duration ?? 0 }
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ default_platform(:ios)
|
||||
platform :ios do
|
||||
desc "Build and deploy to App Store"
|
||||
lane :deploy_appstore do
|
||||
git_commit = `git rev-parse --short HEAD`.strip
|
||||
|
||||
# Build the Flutter app
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true")
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=app-store")
|
||||
|
||||
# Upload to App Store
|
||||
upload_to_app_store(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,335 @@
|
||||
// Generated from USB HID Usage Tables — do not edit by hand.
|
||||
|
||||
/// Human-readable labels for physical keyboard keys, keyed by USB HID usage code.
|
||||
const hidKeyLabels = <int, String>{
|
||||
// Special
|
||||
0x00000010: 'Hyper',
|
||||
0x00000011: 'Super',
|
||||
0x00000012: 'Fn',
|
||||
0x00000013: 'Fn Lock',
|
||||
0x00000014: 'Suspend',
|
||||
0x00000015: 'Resume',
|
||||
0x00000016: 'Turbo',
|
||||
0x00000017: 'Privacy Screen Toggle',
|
||||
0x00000018: 'Microphone Mute',
|
||||
// Generic Desktop
|
||||
0x00010082: 'Sleep',
|
||||
0x00010083: 'Wake Up',
|
||||
0x000100b5: 'Display Toggle',
|
||||
// Game Buttons
|
||||
0x0005ff01: 'Game Button 1',
|
||||
0x0005ff02: 'Game Button 2',
|
||||
0x0005ff03: 'Game Button 3',
|
||||
0x0005ff04: 'Game Button 4',
|
||||
0x0005ff05: 'Game Button 5',
|
||||
0x0005ff06: 'Game Button 6',
|
||||
0x0005ff07: 'Game Button 7',
|
||||
0x0005ff08: 'Game Button 8',
|
||||
0x0005ff09: 'Game Button 9',
|
||||
0x0005ff0a: 'Game Button 10',
|
||||
0x0005ff0b: 'Game Button 11',
|
||||
0x0005ff0c: 'Game Button 12',
|
||||
0x0005ff0d: 'Game Button 13',
|
||||
0x0005ff0e: 'Game Button 14',
|
||||
0x0005ff0f: 'Game Button 15',
|
||||
0x0005ff10: 'Game Button 16',
|
||||
0x0005ff11: 'Game Button A',
|
||||
0x0005ff12: 'Game Button B',
|
||||
0x0005ff13: 'Game Button C',
|
||||
0x0005ff14: 'Game Button Left 1',
|
||||
0x0005ff15: 'Game Button Left 2',
|
||||
0x0005ff16: 'Game Button Mode',
|
||||
0x0005ff17: 'Game Button Right 1',
|
||||
0x0005ff18: 'Game Button Right 2',
|
||||
0x0005ff19: 'Game Button Select',
|
||||
0x0005ff1a: 'Game Button Start',
|
||||
0x0005ff1b: 'Game Button Thumb Left',
|
||||
0x0005ff1c: 'Game Button Thumb Right',
|
||||
0x0005ff1d: 'Game Button X',
|
||||
0x0005ff1e: 'Game Button Y',
|
||||
0x0005ff1f: 'Game Button Z',
|
||||
// Keyboard/Keypad
|
||||
0x00070004: 'A',
|
||||
0x00070005: 'B',
|
||||
0x00070006: 'C',
|
||||
0x00070007: 'D',
|
||||
0x00070008: 'E',
|
||||
0x00070009: 'F',
|
||||
0x0007000a: 'G',
|
||||
0x0007000b: 'H',
|
||||
0x0007000c: 'I',
|
||||
0x0007000d: 'J',
|
||||
0x0007000e: 'K',
|
||||
0x0007000f: 'L',
|
||||
0x00070010: 'M',
|
||||
0x00070011: 'N',
|
||||
0x00070012: 'O',
|
||||
0x00070013: 'P',
|
||||
0x00070014: 'Q',
|
||||
0x00070015: 'R',
|
||||
0x00070016: 'S',
|
||||
0x00070017: 'T',
|
||||
0x00070018: 'U',
|
||||
0x00070019: 'V',
|
||||
0x0007001a: 'W',
|
||||
0x0007001b: 'X',
|
||||
0x0007001c: 'Y',
|
||||
0x0007001d: 'Z',
|
||||
0x0007001e: '1',
|
||||
0x0007001f: '2',
|
||||
0x00070020: '3',
|
||||
0x00070021: '4',
|
||||
0x00070022: '5',
|
||||
0x00070023: '6',
|
||||
0x00070024: '7',
|
||||
0x00070025: '8',
|
||||
0x00070026: '9',
|
||||
0x00070027: '0',
|
||||
0x00070028: 'Enter',
|
||||
0x00070029: 'Escape',
|
||||
0x0007002a: 'Backspace',
|
||||
0x0007002b: 'Tab',
|
||||
0x0007002c: 'Space',
|
||||
0x0007002d: '-',
|
||||
0x0007002e: '=',
|
||||
0x0007002f: '[',
|
||||
0x00070030: ']',
|
||||
0x00070031: r'\',
|
||||
0x00070033: ';',
|
||||
0x00070034: "'",
|
||||
0x00070035: '`',
|
||||
0x00070036: ',',
|
||||
0x00070037: '.',
|
||||
0x00070038: '/',
|
||||
0x00070039: 'Caps Lock',
|
||||
0x0007003a: 'F1',
|
||||
0x0007003b: 'F2',
|
||||
0x0007003c: 'F3',
|
||||
0x0007003d: 'F4',
|
||||
0x0007003e: 'F5',
|
||||
0x0007003f: 'F6',
|
||||
0x00070040: 'F7',
|
||||
0x00070041: 'F8',
|
||||
0x00070042: 'F9',
|
||||
0x00070043: 'F10',
|
||||
0x00070044: 'F11',
|
||||
0x00070045: 'F12',
|
||||
0x00070046: 'Print Screen',
|
||||
0x00070047: 'Scroll Lock',
|
||||
0x00070048: 'Pause',
|
||||
0x00070049: 'Insert',
|
||||
0x0007004a: 'Home',
|
||||
0x0007004b: 'Page Up',
|
||||
0x0007004c: 'Delete',
|
||||
0x0007004d: 'End',
|
||||
0x0007004e: 'Page Down',
|
||||
0x0007004f: 'Arrow Right',
|
||||
0x00070050: 'Arrow Left',
|
||||
0x00070051: 'Arrow Down',
|
||||
0x00070052: 'Arrow Up',
|
||||
0x00070053: 'Num Lock',
|
||||
0x00070054: 'Numpad /',
|
||||
0x00070055: 'Numpad *',
|
||||
0x00070056: 'Numpad -',
|
||||
0x00070057: 'Numpad +',
|
||||
0x00070058: 'Numpad Enter',
|
||||
0x00070059: 'Numpad 1',
|
||||
0x0007005a: 'Numpad 2',
|
||||
0x0007005b: 'Numpad 3',
|
||||
0x0007005c: 'Numpad 4',
|
||||
0x0007005d: 'Numpad 5',
|
||||
0x0007005e: 'Numpad 6',
|
||||
0x0007005f: 'Numpad 7',
|
||||
0x00070060: 'Numpad 8',
|
||||
0x00070061: 'Numpad 9',
|
||||
0x00070062: 'Numpad 0',
|
||||
0x00070063: 'Numpad .',
|
||||
0x00070064: 'Intl Backslash',
|
||||
0x00070065: 'Context Menu',
|
||||
0x00070066: 'Power',
|
||||
0x00070067: 'Numpad =',
|
||||
0x00070068: 'F13',
|
||||
0x00070069: 'F14',
|
||||
0x0007006a: 'F15',
|
||||
0x0007006b: 'F16',
|
||||
0x0007006c: 'F17',
|
||||
0x0007006d: 'F18',
|
||||
0x0007006e: 'F19',
|
||||
0x0007006f: 'F20',
|
||||
0x00070070: 'F21',
|
||||
0x00070071: 'F22',
|
||||
0x00070072: 'F23',
|
||||
0x00070073: 'F24',
|
||||
0x00070074: 'Open',
|
||||
0x00070075: 'Help',
|
||||
0x00070077: 'Select',
|
||||
0x00070079: 'Again',
|
||||
0x0007007a: 'Undo',
|
||||
0x0007007b: 'Cut',
|
||||
0x0007007c: 'Copy',
|
||||
0x0007007d: 'Paste',
|
||||
0x0007007e: 'Find',
|
||||
0x0007007f: 'Volume Mute',
|
||||
0x00070080: 'Volume Up',
|
||||
0x00070081: 'Volume Down',
|
||||
0x00070085: 'Numpad ,',
|
||||
0x00070087: 'Intl Ro',
|
||||
0x00070088: 'Kana Mode',
|
||||
0x00070089: 'Intl Yen',
|
||||
0x0007008a: 'Convert',
|
||||
0x0007008b: 'Non Convert',
|
||||
0x00070090: 'Lang 1',
|
||||
0x00070091: 'Lang 2',
|
||||
0x00070092: 'Lang 3',
|
||||
0x00070093: 'Lang 4',
|
||||
0x00070094: 'Lang 5',
|
||||
0x0007009b: 'Abort',
|
||||
0x000700a3: 'Props',
|
||||
0x000700b6: 'Numpad (',
|
||||
0x000700b7: 'Numpad )',
|
||||
0x000700bb: 'Numpad Backspace',
|
||||
0x000700d0: 'Numpad Memory Store',
|
||||
0x000700d1: 'Numpad Memory Recall',
|
||||
0x000700d2: 'Numpad Memory Clear',
|
||||
0x000700d3: 'Numpad Memory Add',
|
||||
0x000700d4: 'Numpad Memory Subtract',
|
||||
0x000700d7: 'Numpad Sign Change',
|
||||
0x000700d8: 'Numpad Clear',
|
||||
0x000700d9: 'Numpad Clear Entry',
|
||||
0x000700e0: 'Ctrl',
|
||||
0x000700e1: 'Shift',
|
||||
0x000700e2: 'Alt',
|
||||
0x000700e3: 'Meta',
|
||||
0x000700e4: 'Ctrl',
|
||||
0x000700e5: 'Shift',
|
||||
0x000700e6: 'Alt',
|
||||
0x000700e7: 'Meta',
|
||||
// Consumer
|
||||
0x000c0030: 'Power',
|
||||
0x000c0032: 'Sleep',
|
||||
0x000c0040: 'Menu',
|
||||
0x000c0041: 'Menu Pick',
|
||||
0x000c0042: 'Menu Up',
|
||||
0x000c0043: 'Menu Down',
|
||||
0x000c0044: 'Menu Left',
|
||||
0x000c0045: 'Menu Right',
|
||||
0x000c0046: 'Menu Escape',
|
||||
0x000c0047: 'Menu Value Increase',
|
||||
0x000c0048: 'Menu Value Decrease',
|
||||
0x000c0060: 'Info',
|
||||
0x000c0061: 'Closed Caption',
|
||||
0x000c0062: 'Closed Caption Select',
|
||||
0x000c0063: 'VCR/TV',
|
||||
0x000c0064: 'Broadcast Mode',
|
||||
0x000c0065: 'Snapshot',
|
||||
0x000c0066: 'Still',
|
||||
0x000c0067: 'PiP Toggle',
|
||||
0x000c0068: 'PiP Swap',
|
||||
0x000c0069: 'Red Button',
|
||||
0x000c006a: 'Green Button',
|
||||
0x000c006b: 'Blue Button',
|
||||
0x000c006c: 'Yellow Button',
|
||||
0x000c006d: 'Aspect',
|
||||
0x000c006e: '3D Mode Select',
|
||||
0x000c006f: 'Brightness Up',
|
||||
0x000c0070: 'Brightness Down',
|
||||
0x000c0072: 'Brightness Toggle',
|
||||
0x000c0073: 'Brightness Min',
|
||||
0x000c0074: 'Brightness Max',
|
||||
0x000c0075: 'Brightness Auto',
|
||||
0x000c0079: 'Kbd Illum Up',
|
||||
0x000c007a: 'Kbd Illum Down',
|
||||
0x000c0082: 'Mode Step',
|
||||
0x000c0083: 'Recall Last',
|
||||
0x000c0084: 'Enter Channel',
|
||||
0x000c0085: 'Order Movie',
|
||||
0x000c0086: 'Channel',
|
||||
0x000c0088: 'Media Computer',
|
||||
0x000c0089: 'Media TV',
|
||||
0x000c008a: 'Media WWW',
|
||||
0x000c008b: 'Media DVD',
|
||||
0x000c008c: 'Media Telephone',
|
||||
0x000c008d: 'Program Guide',
|
||||
0x000c008e: 'Media Video Phone',
|
||||
0x000c008f: 'Media Games',
|
||||
0x000c0090: 'Media Messages',
|
||||
0x000c0091: 'Media CD',
|
||||
0x000c0092: 'Media VCR',
|
||||
0x000c0093: 'Media Tuner',
|
||||
0x000c0094: 'Quit',
|
||||
0x000c0095: 'Help',
|
||||
0x000c0096: 'Media Tape',
|
||||
0x000c0097: 'Media Cable',
|
||||
0x000c0098: 'Media Satellite',
|
||||
0x000c0099: 'Media Security',
|
||||
0x000c009a: 'Media Home',
|
||||
0x000c009b: 'Media Call',
|
||||
0x000c009c: 'Channel Up',
|
||||
0x000c009d: 'Channel Down',
|
||||
0x000c009e: 'Media SAP',
|
||||
0x000c00b0: 'Play',
|
||||
0x000c00b1: 'Pause',
|
||||
0x000c00b2: 'Record',
|
||||
0x000c00b3: 'Fast Forward',
|
||||
0x000c00b4: 'Rewind',
|
||||
0x000c00b5: 'Next Track',
|
||||
0x000c00b6: 'Previous Track',
|
||||
0x000c00b7: 'Stop',
|
||||
0x000c00b8: 'Eject',
|
||||
0x000c00b9: 'Random Play',
|
||||
0x000c00ba: 'Select Disc',
|
||||
0x000c00bb: 'Enter Disc',
|
||||
0x000c00bc: 'Repeat',
|
||||
0x000c00c0: 'Frame Forward',
|
||||
0x000c00c1: 'Frame Back',
|
||||
0x000c00cc: 'Stop/Eject',
|
||||
0x000c00cd: 'Play/Pause',
|
||||
0x000c00ce: 'Play/Skip',
|
||||
0x000c00cf: 'Voice Command',
|
||||
0x000c00e0: 'Volume',
|
||||
0x000c00e2: 'Mute',
|
||||
0x000c00e5: 'Bass Boost',
|
||||
0x000c00e6: 'Surround Mode',
|
||||
0x000c00e7: 'Loudness',
|
||||
0x000c00e9: 'Volume Up',
|
||||
0x000c00ea: 'Volume Down',
|
||||
0x000c0183: 'Media Select',
|
||||
0x000c0184: 'Launch Word Processor',
|
||||
0x000c0186: 'Launch Spreadsheet',
|
||||
0x000c018a: 'Launch Mail',
|
||||
0x000c018d: 'Launch Contacts',
|
||||
0x000c018e: 'Launch Calendar',
|
||||
0x000c0192: 'Launch Calculator',
|
||||
0x000c0194: 'Launch File Browser',
|
||||
0x000c0196: 'Launch Browser',
|
||||
0x000c019c: 'Log Off',
|
||||
0x000c019e: 'Lock Screen',
|
||||
0x000c019f: 'Launch Control Panel',
|
||||
0x000c01a2: 'Select Task',
|
||||
0x000c01a7: 'Launch Documents',
|
||||
0x000c01ab: 'Spell Check',
|
||||
0x000c01ae: 'Launch Keyboard Layout',
|
||||
0x000c01b1: 'Launch Screen Saver',
|
||||
0x000c01b7: 'Launch Audio Browser',
|
||||
0x000c01cb: 'Launch Assistant',
|
||||
0x000c0201: 'New',
|
||||
0x000c0203: 'Close',
|
||||
0x000c0207: 'Save',
|
||||
0x000c0208: 'Print',
|
||||
0x000c0221: 'Browser Search',
|
||||
0x000c0223: 'Browser Home',
|
||||
0x000c0224: 'Browser Back',
|
||||
0x000c0225: 'Browser Forward',
|
||||
0x000c0226: 'Browser Stop',
|
||||
0x000c0227: 'Browser Refresh',
|
||||
0x000c022a: 'Browser Favorites',
|
||||
0x000c022d: 'Zoom In',
|
||||
0x000c022e: 'Zoom Out',
|
||||
0x000c0232: 'Zoom Toggle',
|
||||
0x000c0279: 'Redo',
|
||||
0x000c0289: 'Mail Reply',
|
||||
0x000c028b: 'Mail Forward',
|
||||
0x000c028c: 'Mail Send',
|
||||
0x000c029d: 'Keyboard Layout Select',
|
||||
0x000c029f: 'Show All Windows',
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,406 @@
|
||||
// Generated from iso_639_codes.json — do not edit by hand.
|
||||
|
||||
class LanguageEntry {
|
||||
final String code1;
|
||||
final String code2;
|
||||
final String? code2B;
|
||||
final String name;
|
||||
const LanguageEntry(this.code1, this.code2, this.code2B, this.name);
|
||||
}
|
||||
|
||||
const languageEntries = <String, LanguageEntry>{
|
||||
'aa': LanguageEntry('aa', 'aar', null, 'Afar'),
|
||||
'ab': LanguageEntry('ab', 'abk', null, 'Abkhaz'),
|
||||
'ae': LanguageEntry('ae', 'ave', null, 'Avestan'),
|
||||
'af': LanguageEntry('af', 'afr', null, 'Afrikaans'),
|
||||
'ak': LanguageEntry('ak', 'aka', null, 'Akan'),
|
||||
'am': LanguageEntry('am', 'amh', null, 'Amharic'),
|
||||
'an': LanguageEntry('an', 'arg', null, 'Aragonese'),
|
||||
'ar': LanguageEntry('ar', 'ara', null, 'Arabic'),
|
||||
'as': LanguageEntry('as', 'asm', null, 'Assamese'),
|
||||
'av': LanguageEntry('av', 'ava', null, 'Avaric'),
|
||||
'ay': LanguageEntry('ay', 'aym', null, 'Aymara'),
|
||||
'az': LanguageEntry('az', 'aze', null, 'Azerbaijani'),
|
||||
'ba': LanguageEntry('ba', 'bak', null, 'Bashkir'),
|
||||
'be': LanguageEntry('be', 'bel', null, 'Belarusian'),
|
||||
'bg': LanguageEntry('bg', 'bul', null, 'Bulgarian'),
|
||||
'bh': LanguageEntry('bh', 'bih', null, 'Bihari'),
|
||||
'bi': LanguageEntry('bi', 'bis', null, 'Bislama'),
|
||||
'bm': LanguageEntry('bm', 'bam', null, 'Bambara'),
|
||||
'bn': LanguageEntry('bn', 'ben', null, 'Bengali, Bangla'),
|
||||
'bo': LanguageEntry('bo', 'bod', 'tib', 'Tibetan Standard, Tibetan, Central'),
|
||||
'br': LanguageEntry('br', 'bre', null, 'Breton'),
|
||||
'bs': LanguageEntry('bs', 'bos', null, 'Bosnian'),
|
||||
'ca': LanguageEntry('ca', 'cat', null, 'Catalan'),
|
||||
'ce': LanguageEntry('ce', 'che', null, 'Chechen'),
|
||||
'ch': LanguageEntry('ch', 'cha', null, 'Chamorro'),
|
||||
'co': LanguageEntry('co', 'cos', null, 'Corsican'),
|
||||
'cr': LanguageEntry('cr', 'cre', null, 'Cree'),
|
||||
'cs': LanguageEntry('cs', 'ces', 'cze', 'Czech'),
|
||||
'cu': LanguageEntry('cu', 'chu', null, 'Old Church Slavonic, Church Slavonic, Old Bulgarian'),
|
||||
'cv': LanguageEntry('cv', 'chv', null, 'Chuvash'),
|
||||
'cy': LanguageEntry('cy', 'cym', 'wel', 'Welsh'),
|
||||
'da': LanguageEntry('da', 'dan', null, 'Danish'),
|
||||
'de': LanguageEntry('de', 'deu', 'ger', 'German'),
|
||||
'dv': LanguageEntry('dv', 'div', null, 'Divehi, Dhivehi, Maldivian'),
|
||||
'dz': LanguageEntry('dz', 'dzo', null, 'Dzongkha'),
|
||||
'ee': LanguageEntry('ee', 'ewe', null, 'Ewe'),
|
||||
'el': LanguageEntry('el', 'ell', 'gre', 'Greek (modern)'),
|
||||
'en': LanguageEntry('en', 'eng', null, 'English'),
|
||||
'eo': LanguageEntry('eo', 'epo', null, 'Esperanto'),
|
||||
'es': LanguageEntry('es', 'spa', null, 'Spanish'),
|
||||
'et': LanguageEntry('et', 'est', null, 'Estonian'),
|
||||
'eu': LanguageEntry('eu', 'eus', 'baq', 'Basque'),
|
||||
'fa': LanguageEntry('fa', 'fas', 'per', 'Persian (Farsi)'),
|
||||
'ff': LanguageEntry('ff', 'ful', null, 'Fula, Fulah, Pulaar, Pular'),
|
||||
'fi': LanguageEntry('fi', 'fin', null, 'Finnish'),
|
||||
'fj': LanguageEntry('fj', 'fij', null, 'Fijian'),
|
||||
'fo': LanguageEntry('fo', 'fao', null, 'Faroese'),
|
||||
'fr': LanguageEntry('fr', 'fra', 'fre', 'French'),
|
||||
'fy': LanguageEntry('fy', 'fry', null, 'Western Frisian'),
|
||||
'ga': LanguageEntry('ga', 'gle', null, 'Irish'),
|
||||
'gd': LanguageEntry('gd', 'gla', null, 'Scottish Gaelic, Gaelic'),
|
||||
'gl': LanguageEntry('gl', 'glg', null, 'Galician'),
|
||||
'gn': LanguageEntry('gn', 'grn', null, 'Guaraní'),
|
||||
'gu': LanguageEntry('gu', 'guj', null, 'Gujarati'),
|
||||
'gv': LanguageEntry('gv', 'glv', null, 'Manx'),
|
||||
'ha': LanguageEntry('ha', 'hau', null, 'Hausa'),
|
||||
'he': LanguageEntry('he', 'heb', null, 'Hebrew (modern)'),
|
||||
'hi': LanguageEntry('hi', 'hin', null, 'Hindi'),
|
||||
'ho': LanguageEntry('ho', 'hmo', null, 'Hiri Motu'),
|
||||
'hr': LanguageEntry('hr', 'hrv', null, 'Croatian'),
|
||||
'ht': LanguageEntry('ht', 'hat', null, 'Haitian, Haitian Creole'),
|
||||
'hu': LanguageEntry('hu', 'hun', null, 'Hungarian'),
|
||||
'hy': LanguageEntry('hy', 'hye', 'arm', 'Armenian'),
|
||||
'hz': LanguageEntry('hz', 'her', null, 'Herero'),
|
||||
'ia': LanguageEntry('ia', 'ina', null, 'Interlingua'),
|
||||
'id': LanguageEntry('id', 'ind', null, 'Indonesian'),
|
||||
'ie': LanguageEntry('ie', 'ile', null, 'Interlingue'),
|
||||
'ig': LanguageEntry('ig', 'ibo', null, 'Igbo'),
|
||||
'ii': LanguageEntry('ii', 'iii', null, 'Nuosu'),
|
||||
'ik': LanguageEntry('ik', 'ipk', null, 'Inupiaq'),
|
||||
'io': LanguageEntry('io', 'ido', null, 'Ido'),
|
||||
'is': LanguageEntry('is', 'isl', 'ice', 'Icelandic'),
|
||||
'it': LanguageEntry('it', 'ita', null, 'Italian'),
|
||||
'iu': LanguageEntry('iu', 'iku', null, 'Inuktitut'),
|
||||
'ja': LanguageEntry('ja', 'jpn', null, 'Japanese'),
|
||||
'jv': LanguageEntry('jv', 'jav', null, 'Javanese'),
|
||||
'ka': LanguageEntry('ka', 'kat', 'geo', 'Georgian'),
|
||||
'kg': LanguageEntry('kg', 'kon', null, 'Kongo'),
|
||||
'ki': LanguageEntry('ki', 'kik', null, 'Kikuyu, Gikuyu'),
|
||||
'kj': LanguageEntry('kj', 'kua', null, 'Kwanyama, Kuanyama'),
|
||||
'kk': LanguageEntry('kk', 'kaz', null, 'Kazakh'),
|
||||
'kl': LanguageEntry('kl', 'kal', null, 'Kalaallisut, Greenlandic'),
|
||||
'km': LanguageEntry('km', 'khm', null, 'Khmer'),
|
||||
'kn': LanguageEntry('kn', 'kan', null, 'Kannada'),
|
||||
'ko': LanguageEntry('ko', 'kor', null, 'Korean'),
|
||||
'kr': LanguageEntry('kr', 'kau', null, 'Kanuri'),
|
||||
'ks': LanguageEntry('ks', 'kas', null, 'Kashmiri'),
|
||||
'ku': LanguageEntry('ku', 'kur', null, 'Kurdish'),
|
||||
'kv': LanguageEntry('kv', 'kom', null, 'Komi'),
|
||||
'kw': LanguageEntry('kw', 'cor', null, 'Cornish'),
|
||||
'ky': LanguageEntry('ky', 'kir', null, 'Kyrgyz'),
|
||||
'la': LanguageEntry('la', 'lat', null, 'Latin'),
|
||||
'lb': LanguageEntry('lb', 'ltz', null, 'Luxembourgish, Letzeburgesch'),
|
||||
'lg': LanguageEntry('lg', 'lug', null, 'Ganda'),
|
||||
'li': LanguageEntry('li', 'lim', null, 'Limburgish, Limburgan, Limburger'),
|
||||
'ln': LanguageEntry('ln', 'lin', null, 'Lingala'),
|
||||
'lo': LanguageEntry('lo', 'lao', null, 'Lao'),
|
||||
'lt': LanguageEntry('lt', 'lit', null, 'Lithuanian'),
|
||||
'lu': LanguageEntry('lu', 'lub', null, 'Luba-Katanga'),
|
||||
'lv': LanguageEntry('lv', 'lav', null, 'Latvian'),
|
||||
'mg': LanguageEntry('mg', 'mlg', null, 'Malagasy'),
|
||||
'mh': LanguageEntry('mh', 'mah', null, 'Marshallese'),
|
||||
'mi': LanguageEntry('mi', 'mri', 'mao', 'Māori'),
|
||||
'mk': LanguageEntry('mk', 'mkd', 'mac', 'Macedonian'),
|
||||
'ml': LanguageEntry('ml', 'mal', null, 'Malayalam'),
|
||||
'mn': LanguageEntry('mn', 'mon', null, 'Mongolian'),
|
||||
'mr': LanguageEntry('mr', 'mar', null, 'Marathi (Marāṭhī)'),
|
||||
'ms': LanguageEntry('ms', 'msa', 'may', 'Malay'),
|
||||
'mt': LanguageEntry('mt', 'mlt', null, 'Maltese'),
|
||||
'my': LanguageEntry('my', 'mya', 'bur', 'Burmese'),
|
||||
'na': LanguageEntry('na', 'nau', null, 'Nauruan'),
|
||||
'nb': LanguageEntry('nb', 'nob', null, 'Norwegian Bokmål'),
|
||||
'nd': LanguageEntry('nd', 'nde', null, 'Northern Ndebele'),
|
||||
'ne': LanguageEntry('ne', 'nep', null, 'Nepali'),
|
||||
'ng': LanguageEntry('ng', 'ndo', null, 'Ndonga'),
|
||||
'nl': LanguageEntry('nl', 'nld', 'dut', 'Dutch'),
|
||||
'nn': LanguageEntry('nn', 'nno', null, 'Norwegian Nynorsk'),
|
||||
'no': LanguageEntry('no', 'nor', null, 'Norwegian'),
|
||||
'nr': LanguageEntry('nr', 'nbl', null, 'Southern Ndebele'),
|
||||
'nv': LanguageEntry('nv', 'nav', null, 'Navajo, Navaho'),
|
||||
'ny': LanguageEntry('ny', 'nya', null, 'Chichewa, Chewa, Nyanja'),
|
||||
'oc': LanguageEntry('oc', 'oci', null, 'Occitan'),
|
||||
'oj': LanguageEntry('oj', 'oji', null, 'Ojibwe, Ojibwa'),
|
||||
'om': LanguageEntry('om', 'orm', null, 'Oromo'),
|
||||
'or': LanguageEntry('or', 'ori', null, 'Oriya'),
|
||||
'os': LanguageEntry('os', 'oss', null, 'Ossetian, Ossetic'),
|
||||
'pa': LanguageEntry('pa', 'pan', null, '(Eastern) Punjabi'),
|
||||
'pi': LanguageEntry('pi', 'pli', null, 'Pāli'),
|
||||
'pl': LanguageEntry('pl', 'pol', null, 'Polish'),
|
||||
'ps': LanguageEntry('ps', 'pus', null, 'Pashto, Pushto'),
|
||||
'pt': LanguageEntry('pt', 'por', null, 'Portuguese'),
|
||||
'qu': LanguageEntry('qu', 'que', null, 'Quechua'),
|
||||
'rm': LanguageEntry('rm', 'roh', null, 'Romansh'),
|
||||
'rn': LanguageEntry('rn', 'run', null, 'Kirundi'),
|
||||
'ro': LanguageEntry('ro', 'ron', 'rum', 'Romanian'),
|
||||
'ru': LanguageEntry('ru', 'rus', null, 'Russian'),
|
||||
'rw': LanguageEntry('rw', 'kin', null, 'Kinyarwanda'),
|
||||
'sa': LanguageEntry('sa', 'san', null, 'Sanskrit (Saṁskṛta)'),
|
||||
'sc': LanguageEntry('sc', 'srd', null, 'Sardinian'),
|
||||
'sd': LanguageEntry('sd', 'snd', null, 'Sindhi'),
|
||||
'se': LanguageEntry('se', 'sme', null, 'Northern Sami'),
|
||||
'sg': LanguageEntry('sg', 'sag', null, 'Sango'),
|
||||
'si': LanguageEntry('si', 'sin', null, 'Sinhalese, Sinhala'),
|
||||
'sk': LanguageEntry('sk', 'slk', 'slo', 'Slovak'),
|
||||
'sl': LanguageEntry('sl', 'slv', null, 'Slovene'),
|
||||
'sm': LanguageEntry('sm', 'smo', null, 'Samoan'),
|
||||
'sn': LanguageEntry('sn', 'sna', null, 'Shona'),
|
||||
'so': LanguageEntry('so', 'som', null, 'Somali'),
|
||||
'sq': LanguageEntry('sq', 'sqi', 'alb', 'Albanian'),
|
||||
'sr': LanguageEntry('sr', 'srp', null, 'Serbian'),
|
||||
'ss': LanguageEntry('ss', 'ssw', null, 'Swati'),
|
||||
'st': LanguageEntry('st', 'sot', null, 'Southern Sotho'),
|
||||
'su': LanguageEntry('su', 'sun', null, 'Sundanese'),
|
||||
'sv': LanguageEntry('sv', 'swe', null, 'Swedish'),
|
||||
'sw': LanguageEntry('sw', 'swa', null, 'Swahili'),
|
||||
'ta': LanguageEntry('ta', 'tam', null, 'Tamil'),
|
||||
'te': LanguageEntry('te', 'tel', null, 'Telugu'),
|
||||
'tg': LanguageEntry('tg', 'tgk', null, 'Tajik'),
|
||||
'th': LanguageEntry('th', 'tha', null, 'Thai'),
|
||||
'ti': LanguageEntry('ti', 'tir', null, 'Tigrinya'),
|
||||
'tk': LanguageEntry('tk', 'tuk', null, 'Turkmen'),
|
||||
'tl': LanguageEntry('tl', 'tgl', null, 'Tagalog'),
|
||||
'tn': LanguageEntry('tn', 'tsn', null, 'Tswana'),
|
||||
'to': LanguageEntry('to', 'ton', null, 'Tonga (Tonga Islands)'),
|
||||
'tr': LanguageEntry('tr', 'tur', null, 'Turkish'),
|
||||
'ts': LanguageEntry('ts', 'tso', null, 'Tsonga'),
|
||||
'tt': LanguageEntry('tt', 'tat', null, 'Tatar'),
|
||||
'tw': LanguageEntry('tw', 'twi', null, 'Twi'),
|
||||
'ty': LanguageEntry('ty', 'tah', null, 'Tahitian'),
|
||||
'ug': LanguageEntry('ug', 'uig', null, 'Uyghur'),
|
||||
'uk': LanguageEntry('uk', 'ukr', null, 'Ukrainian'),
|
||||
'ur': LanguageEntry('ur', 'urd', null, 'Urdu'),
|
||||
'uz': LanguageEntry('uz', 'uzb', null, 'Uzbek'),
|
||||
've': LanguageEntry('ve', 'ven', null, 'Venda'),
|
||||
'vi': LanguageEntry('vi', 'vie', null, 'Vietnamese'),
|
||||
'vo': LanguageEntry('vo', 'vol', null, 'Volapük'),
|
||||
'wa': LanguageEntry('wa', 'wln', null, 'Walloon'),
|
||||
'wo': LanguageEntry('wo', 'wol', null, 'Wolof'),
|
||||
'xh': LanguageEntry('xh', 'xho', null, 'Xhosa'),
|
||||
'yi': LanguageEntry('yi', 'yid', null, 'Yiddish'),
|
||||
'yo': LanguageEntry('yo', 'yor', null, 'Yoruba'),
|
||||
'za': LanguageEntry('za', 'zha', null, 'Zhuang, Chuang'),
|
||||
'zh': LanguageEntry('zh', 'zho', 'chi', 'Chinese'),
|
||||
'zu': LanguageEntry('zu', 'zul', null, 'Zulu'),
|
||||
};
|
||||
|
||||
const code2ToCode1 = <String, String>{
|
||||
'aar': 'aa',
|
||||
'abk': 'ab',
|
||||
'ave': 'ae',
|
||||
'afr': 'af',
|
||||
'aka': 'ak',
|
||||
'amh': 'am',
|
||||
'arg': 'an',
|
||||
'ara': 'ar',
|
||||
'asm': 'as',
|
||||
'ava': 'av',
|
||||
'aym': 'ay',
|
||||
'aze': 'az',
|
||||
'bak': 'ba',
|
||||
'bel': 'be',
|
||||
'bul': 'bg',
|
||||
'bih': 'bh',
|
||||
'bis': 'bi',
|
||||
'bam': 'bm',
|
||||
'ben': 'bn',
|
||||
'bod': 'bo',
|
||||
'bre': 'br',
|
||||
'bos': 'bs',
|
||||
'cat': 'ca',
|
||||
'che': 'ce',
|
||||
'cha': 'ch',
|
||||
'cos': 'co',
|
||||
'cre': 'cr',
|
||||
'ces': 'cs',
|
||||
'chu': 'cu',
|
||||
'chv': 'cv',
|
||||
'cym': 'cy',
|
||||
'dan': 'da',
|
||||
'deu': 'de',
|
||||
'div': 'dv',
|
||||
'dzo': 'dz',
|
||||
'ewe': 'ee',
|
||||
'ell': 'el',
|
||||
'eng': 'en',
|
||||
'epo': 'eo',
|
||||
'spa': 'es',
|
||||
'est': 'et',
|
||||
'eus': 'eu',
|
||||
'fas': 'fa',
|
||||
'ful': 'ff',
|
||||
'fin': 'fi',
|
||||
'fij': 'fj',
|
||||
'fao': 'fo',
|
||||
'fra': 'fr',
|
||||
'fry': 'fy',
|
||||
'gle': 'ga',
|
||||
'gla': 'gd',
|
||||
'glg': 'gl',
|
||||
'grn': 'gn',
|
||||
'guj': 'gu',
|
||||
'glv': 'gv',
|
||||
'hau': 'ha',
|
||||
'heb': 'he',
|
||||
'hin': 'hi',
|
||||
'hmo': 'ho',
|
||||
'hrv': 'hr',
|
||||
'hat': 'ht',
|
||||
'hun': 'hu',
|
||||
'hye': 'hy',
|
||||
'her': 'hz',
|
||||
'ina': 'ia',
|
||||
'ind': 'id',
|
||||
'ile': 'ie',
|
||||
'ibo': 'ig',
|
||||
'iii': 'ii',
|
||||
'ipk': 'ik',
|
||||
'ido': 'io',
|
||||
'isl': 'is',
|
||||
'ita': 'it',
|
||||
'iku': 'iu',
|
||||
'jpn': 'ja',
|
||||
'jav': 'jv',
|
||||
'kat': 'ka',
|
||||
'kon': 'kg',
|
||||
'kik': 'ki',
|
||||
'kua': 'kj',
|
||||
'kaz': 'kk',
|
||||
'kal': 'kl',
|
||||
'khm': 'km',
|
||||
'kan': 'kn',
|
||||
'kor': 'ko',
|
||||
'kau': 'kr',
|
||||
'kas': 'ks',
|
||||
'kur': 'ku',
|
||||
'kom': 'kv',
|
||||
'cor': 'kw',
|
||||
'kir': 'ky',
|
||||
'lat': 'la',
|
||||
'ltz': 'lb',
|
||||
'lug': 'lg',
|
||||
'lim': 'li',
|
||||
'lin': 'ln',
|
||||
'lao': 'lo',
|
||||
'lit': 'lt',
|
||||
'lub': 'lu',
|
||||
'lav': 'lv',
|
||||
'mlg': 'mg',
|
||||
'mah': 'mh',
|
||||
'mri': 'mi',
|
||||
'mkd': 'mk',
|
||||
'mal': 'ml',
|
||||
'mon': 'mn',
|
||||
'mar': 'mr',
|
||||
'msa': 'ms',
|
||||
'mlt': 'mt',
|
||||
'mya': 'my',
|
||||
'nau': 'na',
|
||||
'nob': 'nb',
|
||||
'nde': 'nd',
|
||||
'nep': 'ne',
|
||||
'ndo': 'ng',
|
||||
'nld': 'nl',
|
||||
'nno': 'nn',
|
||||
'nor': 'no',
|
||||
'nbl': 'nr',
|
||||
'nav': 'nv',
|
||||
'nya': 'ny',
|
||||
'oci': 'oc',
|
||||
'oji': 'oj',
|
||||
'orm': 'om',
|
||||
'ori': 'or',
|
||||
'oss': 'os',
|
||||
'pan': 'pa',
|
||||
'pli': 'pi',
|
||||
'pol': 'pl',
|
||||
'pus': 'ps',
|
||||
'por': 'pt',
|
||||
'que': 'qu',
|
||||
'roh': 'rm',
|
||||
'run': 'rn',
|
||||
'ron': 'ro',
|
||||
'rus': 'ru',
|
||||
'kin': 'rw',
|
||||
'san': 'sa',
|
||||
'srd': 'sc',
|
||||
'snd': 'sd',
|
||||
'sme': 'se',
|
||||
'sag': 'sg',
|
||||
'sin': 'si',
|
||||
'slk': 'sk',
|
||||
'slv': 'sl',
|
||||
'smo': 'sm',
|
||||
'sna': 'sn',
|
||||
'som': 'so',
|
||||
'sqi': 'sq',
|
||||
'srp': 'sr',
|
||||
'ssw': 'ss',
|
||||
'sot': 'st',
|
||||
'sun': 'su',
|
||||
'swe': 'sv',
|
||||
'swa': 'sw',
|
||||
'tam': 'ta',
|
||||
'tel': 'te',
|
||||
'tgk': 'tg',
|
||||
'tha': 'th',
|
||||
'tir': 'ti',
|
||||
'tuk': 'tk',
|
||||
'tgl': 'tl',
|
||||
'tsn': 'tn',
|
||||
'ton': 'to',
|
||||
'tur': 'tr',
|
||||
'tso': 'ts',
|
||||
'tat': 'tt',
|
||||
'twi': 'tw',
|
||||
'tah': 'ty',
|
||||
'uig': 'ug',
|
||||
'ukr': 'uk',
|
||||
'urd': 'ur',
|
||||
'uzb': 'uz',
|
||||
'ven': 've',
|
||||
'vie': 'vi',
|
||||
'vol': 'vo',
|
||||
'wln': 'wa',
|
||||
'wol': 'wo',
|
||||
'xho': 'xh',
|
||||
'yid': 'yi',
|
||||
'yor': 'yo',
|
||||
'zha': 'za',
|
||||
'zho': 'zh',
|
||||
'zul': 'zu',
|
||||
};
|
||||
|
||||
const code2BToCode1 = <String, String>{
|
||||
'tib': 'bo',
|
||||
'cze': 'cs',
|
||||
'wel': 'cy',
|
||||
'ger': 'de',
|
||||
'gre': 'el',
|
||||
'baq': 'eu',
|
||||
'per': 'fa',
|
||||
'fre': 'fr',
|
||||
'arm': 'hy',
|
||||
'ice': 'is',
|
||||
'geo': 'ka',
|
||||
'mao': 'mi',
|
||||
'mac': 'mk',
|
||||
'may': 'ms',
|
||||
'bur': 'my',
|
||||
'dut': 'nl',
|
||||
'rum': 'ro',
|
||||
'slo': 'sk',
|
||||
'alb': 'sq',
|
||||
'chi': 'zh',
|
||||
};
|
||||
@@ -12,12 +12,12 @@ import '../utils/global_key_utils.dart';
|
||||
part 'app_database.g.dart';
|
||||
|
||||
// Simplified database with API cache for offline support
|
||||
@DriftDatabase(tables: [DownloadedMedia, DownloadQueue, ApiCache, OfflineWatchProgress])
|
||||
@DriftDatabase(tables: [DownloadedMedia, DownloadQueue, ApiCache, OfflineWatchProgress, SyncRules])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 8; // Added bgTaskId column to DownloadedMedia
|
||||
int get schemaVersion => 12;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@@ -38,6 +38,34 @@ class AppDatabase extends _$AppDatabase {
|
||||
appLogger.w('bgTaskId column may already exist: $e');
|
||||
}
|
||||
}
|
||||
if (from < 9) {
|
||||
appLogger.i('Adding mediaIndex column to DownloadedMedia (v9 migration)');
|
||||
try {
|
||||
await m.addColumn(downloadedMedia, downloadedMedia.mediaIndex);
|
||||
} catch (e) {
|
||||
appLogger.w('mediaIndex column may already exist: $e');
|
||||
}
|
||||
}
|
||||
if (from < 10) {
|
||||
appLogger.i('Adding SyncRules table (v10 migration)');
|
||||
await m.createTable(syncRules);
|
||||
}
|
||||
if (from < 11) {
|
||||
appLogger.i('Adding enabled column to SyncRules (v11 migration)');
|
||||
try {
|
||||
await m.addColumn(syncRules, syncRules.enabled);
|
||||
} catch (e) {
|
||||
appLogger.w('enabled column may already exist: $e');
|
||||
}
|
||||
}
|
||||
if (from < 12) {
|
||||
appLogger.i('Adding downloadFilter column to SyncRules (v12 migration)');
|
||||
try {
|
||||
await m.addColumn(syncRules, syncRules.downloadFilter);
|
||||
} catch (e) {
|
||||
appLogger.w('downloadFilter column may already exist: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -193,6 +221,69 @@ class AppDatabase extends _$AppDatabase {
|
||||
return delete(offlineWatchProgress).go();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Sync Rules Operations
|
||||
// ============================================================
|
||||
|
||||
Future<List<SyncRuleItem>> getSyncRules() {
|
||||
return select(syncRules).get();
|
||||
}
|
||||
|
||||
Future<SyncRuleItem?> getSyncRule(String globalKey) {
|
||||
return (select(syncRules)..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> insertSyncRule({
|
||||
required String serverId,
|
||||
required String ratingKey,
|
||||
required String globalKey,
|
||||
required String targetType,
|
||||
required int episodeCount,
|
||||
int mediaIndex = 0,
|
||||
String downloadFilter = 'unwatched',
|
||||
}) async {
|
||||
await into(syncRules).insertOnConflictUpdate(
|
||||
SyncRulesCompanion.insert(
|
||||
serverId: serverId,
|
||||
ratingKey: ratingKey,
|
||||
globalKey: globalKey,
|
||||
targetType: targetType,
|
||||
episodeCount: episodeCount,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
mediaIndex: Value(mediaIndex),
|
||||
downloadFilter: Value(downloadFilter),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateSyncRuleCount(String globalKey, int episodeCount) async {
|
||||
await (update(
|
||||
syncRules,
|
||||
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(episodeCount: Value(episodeCount)));
|
||||
}
|
||||
|
||||
Future<void> updateSyncRuleFilter(String globalKey, String downloadFilter) async {
|
||||
await (update(
|
||||
syncRules,
|
||||
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(downloadFilter: Value(downloadFilter)));
|
||||
}
|
||||
|
||||
Future<void> updateSyncRuleEnabled(String globalKey, bool enabled) async {
|
||||
await (update(
|
||||
syncRules,
|
||||
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(enabled: Value(enabled)));
|
||||
}
|
||||
|
||||
Future<void> updateSyncRuleLastExecuted(String globalKey) async {
|
||||
await (update(syncRules)..where((t) => t.globalKey.equals(globalKey))).write(
|
||||
SyncRulesCompanion(lastExecutedAt: Value(DateTime.now().millisecondsSinceEpoch)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteSyncRule(String globalKey) async {
|
||||
await (delete(syncRules)..where((t) => t.globalKey.equals(globalKey))).go();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Downloaded Media Queries for Watch State Sync
|
||||
// ============================================================
|
||||
@@ -225,6 +316,12 @@ LazyDatabase _openConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
return NativeDatabase(file);
|
||||
return NativeDatabase.createInBackground(
|
||||
file,
|
||||
setup: (db) {
|
||||
db.execute('PRAGMA journal_mode=WAL');
|
||||
db.execute('PRAGMA synchronous=NORMAL');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
+2206
-412
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'app_database.dart';
|
||||
import '../models/download_models.dart';
|
||||
|
||||
/// Extension methods on AppDatabase for download operations
|
||||
extension DownloadDatabaseOperations on AppDatabase {
|
||||
/// Insert a new download into the database
|
||||
Future<void> insertDownload({
|
||||
required String serverId,
|
||||
required String ratingKey,
|
||||
required String globalKey,
|
||||
required String type,
|
||||
String? parentRatingKey,
|
||||
String? grandparentRatingKey,
|
||||
required int status,
|
||||
int mediaIndex = 0,
|
||||
}) async {
|
||||
await into(downloadedMedia).insert(
|
||||
DownloadedMediaCompanion.insert(
|
||||
serverId: serverId,
|
||||
ratingKey: ratingKey,
|
||||
globalKey: globalKey,
|
||||
type: type,
|
||||
parentRatingKey: Value(parentRatingKey),
|
||||
grandparentRatingKey: Value(grandparentRatingKey),
|
||||
status: status,
|
||||
mediaIndex: Value(mediaIndex),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
/// Add item to download queue
|
||||
Future<void> addToQueue({
|
||||
required String mediaGlobalKey,
|
||||
int priority = 0,
|
||||
bool downloadSubtitles = true,
|
||||
bool downloadArtwork = true,
|
||||
}) async {
|
||||
await into(downloadQueue).insert(
|
||||
DownloadQueueCompanion.insert(
|
||||
mediaGlobalKey: mediaGlobalKey,
|
||||
priority: Value(priority),
|
||||
addedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
downloadSubtitles: Value(downloadSubtitles),
|
||||
downloadArtwork: Value(downloadArtwork),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get next item from queue (highest priority, oldest first)
|
||||
/// Only returns items that are not paused
|
||||
Future<DownloadQueueItem?> getNextQueueItem() async {
|
||||
// Join with downloadedMedia to check status and filter out paused items
|
||||
final query = select(
|
||||
downloadQueue,
|
||||
).join([innerJoin(downloadedMedia, downloadedMedia.globalKey.equalsExp(downloadQueue.mediaGlobalKey))]);
|
||||
|
||||
query
|
||||
..where(downloadedMedia.status.equals(DownloadStatus.queued.index))
|
||||
..orderBy([
|
||||
OrderingTerm(expression: downloadQueue.priority, mode: OrderingMode.desc),
|
||||
OrderingTerm(expression: downloadQueue.addedAt),
|
||||
])
|
||||
..limit(1);
|
||||
|
||||
final result = await query.getSingleOrNull();
|
||||
return result?.readTable(downloadQueue);
|
||||
}
|
||||
|
||||
/// Update download status
|
||||
Future<void> updateDownloadStatus(String globalKey, int status) async {
|
||||
await (update(
|
||||
downloadedMedia,
|
||||
)..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(status: Value(status)));
|
||||
}
|
||||
|
||||
/// Update download progress
|
||||
Future<void> updateDownloadProgress(String globalKey, int progress, int downloadedBytes, int totalBytes) async {
|
||||
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
|
||||
DownloadedMediaCompanion(
|
||||
progress: Value(progress),
|
||||
downloadedBytes: Value(downloadedBytes),
|
||||
totalBytes: Value(totalBytes),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update video file path
|
||||
Future<void> updateVideoFilePath(String globalKey, String filePath) async {
|
||||
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
|
||||
DownloadedMediaCompanion(
|
||||
videoFilePath: Value(filePath),
|
||||
downloadedAt: Value(DateTime.now().millisecondsSinceEpoch),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update artwork paths
|
||||
Future<void> updateArtworkPaths({required String globalKey, String? thumbPath}) async {
|
||||
await (update(
|
||||
downloadedMedia,
|
||||
)..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(thumbPath: Value(thumbPath)));
|
||||
}
|
||||
|
||||
/// Update download error and increment retry count
|
||||
Future<void> updateDownloadError(String globalKey, String errorMessage) async {
|
||||
// Get current retry count to increment it
|
||||
final existing = await getDownloadedMedia(globalKey);
|
||||
final currentCount = existing?.retryCount ?? 0;
|
||||
|
||||
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
|
||||
DownloadedMediaCompanion(errorMessage: Value(errorMessage), retryCount: Value(currentCount + 1)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear download error and reset retry count (for retry)
|
||||
Future<void> clearDownloadError(String globalKey) async {
|
||||
await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write(
|
||||
const DownloadedMediaCompanion(errorMessage: Value(null), retryCount: Value(0)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove item from queue
|
||||
Future<void> removeFromQueue(String mediaGlobalKey) async {
|
||||
await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(mediaGlobalKey))).go();
|
||||
}
|
||||
|
||||
/// Get downloaded media item
|
||||
Future<DownloadedMediaItem?> getDownloadedMedia(String globalKey) {
|
||||
return (select(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull();
|
||||
}
|
||||
|
||||
/// Delete a download
|
||||
Future<void> deleteDownload(String globalKey) async {
|
||||
await (delete(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).go();
|
||||
await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(globalKey))).go();
|
||||
}
|
||||
|
||||
/// Get all downloaded episodes for a season
|
||||
Future<List<DownloadedMediaItem>> getEpisodesBySeason(String seasonKey) {
|
||||
return (select(downloadedMedia)..where((t) => t.parentRatingKey.equals(seasonKey))).get();
|
||||
}
|
||||
|
||||
/// Get all downloaded episodes for a show
|
||||
Future<List<DownloadedMediaItem>> getEpisodesByShow(String showKey) {
|
||||
return (select(downloadedMedia)..where((t) => t.grandparentRatingKey.equals(showKey))).get();
|
||||
}
|
||||
|
||||
/// Get all downloaded items for a specific server
|
||||
Future<List<DownloadedMediaItem>> getDownloadsByServerId(String serverId) {
|
||||
return (select(downloadedMedia)..where((t) => t.serverId.equals(serverId))).get();
|
||||
}
|
||||
|
||||
/// Update the background_downloader task ID for a download
|
||||
Future<void> updateBgTaskId(String globalKey, String? taskId) async {
|
||||
await (update(
|
||||
downloadedMedia,
|
||||
)..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(bgTaskId: Value(taskId)));
|
||||
}
|
||||
|
||||
/// Get the background_downloader task ID for a download
|
||||
Future<String?> getBgTaskId(String globalKey) async {
|
||||
final item = await getDownloadedMedia(globalKey);
|
||||
return item?.bgTaskId;
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,26 @@ class DownloadedMedia extends Table {
|
||||
TextColumn get errorMessage => text().nullable()();
|
||||
IntColumn get retryCount => integer().withDefault(const Constant(0))();
|
||||
TextColumn get bgTaskId => text().nullable()();
|
||||
IntColumn get mediaIndex => integer().withDefault(const Constant(0))();
|
||||
}
|
||||
|
||||
/// Persistent sync rules for auto-downloading unwatched episodes.
|
||||
///
|
||||
/// Each rule keeps a rolling window of N unwatched episodes for a show/season,
|
||||
/// or mirrors the current contents of a collection/playlist.
|
||||
@DataClassName('SyncRuleItem')
|
||||
class SyncRules extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get serverId => text()();
|
||||
TextColumn get ratingKey => text()();
|
||||
TextColumn get globalKey => text().unique()();
|
||||
TextColumn get targetType => text()(); // 'show', 'season', 'collection', 'playlist'
|
||||
IntColumn get episodeCount => integer()();
|
||||
BoolColumn get enabled => boolean().withDefault(const Constant(true))();
|
||||
IntColumn get createdAt => integer()();
|
||||
IntColumn get lastExecutedAt => integer().nullable()();
|
||||
IntColumn get mediaIndex => integer().withDefault(const Constant(0))();
|
||||
TextColumn get downloadFilter => text().withDefault(const Constant('unwatched'))();
|
||||
}
|
||||
|
||||
/// Queue for offline watch progress and manual watch actions.
|
||||
|
||||
@@ -63,36 +63,47 @@ extension DpadKeyExtension on LogicalKeyboardKey {
|
||||
bool get isDownKey => this == LogicalKeyboardKey.arrowDown;
|
||||
}
|
||||
|
||||
/// Global helper to suppress the next SELECT key-up event.
|
||||
class SelectKeyUpSuppressor {
|
||||
static bool _suppressSelectUntilKeyUp = false;
|
||||
/// Base class for suppressing key-up events after a key category triggers an
|
||||
/// action (e.g. opening a sheet). While suppressed, all events for the matched
|
||||
/// key category are consumed; suppression auto-clears on [KeyUpEvent].
|
||||
class _KeyUpSuppressor {
|
||||
final bool Function(LogicalKeyboardKey) _keyMatcher;
|
||||
|
||||
static void suppressSelectUntilKeyUp() {
|
||||
_suppressSelectUntilKeyUp = true;
|
||||
}
|
||||
_KeyUpSuppressor(this._keyMatcher);
|
||||
|
||||
static void clearSuppression() {
|
||||
_suppressSelectUntilKeyUp = false;
|
||||
}
|
||||
bool _suppressed = false;
|
||||
|
||||
static bool consumeIfSuppressed(KeyEvent event) {
|
||||
if (!_suppressSelectUntilKeyUp) return false;
|
||||
if (event.logicalKey.isSelectKey) {
|
||||
if (event is KeyUpEvent) {
|
||||
_suppressSelectUntilKeyUp = false;
|
||||
}
|
||||
void suppress() => _suppressed = true;
|
||||
|
||||
void clearSuppression() => _suppressed = false;
|
||||
|
||||
/// Returns `true` (consumed) when the event belongs to the matched key
|
||||
/// category and suppression is active. Clears suppression on [KeyUpEvent].
|
||||
bool consumeIfSuppressed(KeyEvent event) {
|
||||
if (!_suppressed) return false;
|
||||
if (_keyMatcher(event.logicalKey)) {
|
||||
if (event is KeyUpEvent) _suppressed = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Global helper to suppress the next SELECT key-up event.
|
||||
class SelectKeyUpSuppressor {
|
||||
static final _instance = _KeyUpSuppressor((k) => k.isSelectKey);
|
||||
|
||||
static void suppressSelectUntilKeyUp() => _instance.suppress();
|
||||
static void clearSuppression() => _instance.clearSuppression();
|
||||
static bool consumeIfSuppressed(KeyEvent event) => _instance.consumeIfSuppressed(event);
|
||||
}
|
||||
|
||||
/// Global helper to suppress the next BACK key-up event.
|
||||
///
|
||||
/// Use this when a modal (bottom sheet, dialog) closes to prevent
|
||||
/// the BACK key-up from propagating to the underlying screen.
|
||||
class BackKeyUpSuppressor {
|
||||
static bool _suppressBackUntilKeyUp = false;
|
||||
static final _instance = _KeyUpSuppressor((k) => k.isBackKey);
|
||||
static bool _closedViaBackKey = false;
|
||||
|
||||
/// Mark that a modal is being closed via back key press.
|
||||
@@ -109,26 +120,17 @@ class BackKeyUpSuppressor {
|
||||
_closedViaBackKey = false;
|
||||
return;
|
||||
}
|
||||
_suppressBackUntilKeyUp = true;
|
||||
_instance.suppress();
|
||||
}
|
||||
|
||||
/// Clear any pending suppression. Call when opening a new modal
|
||||
/// to ensure stale suppression from previous closes doesn't affect it.
|
||||
static void clearSuppression() {
|
||||
_suppressBackUntilKeyUp = false;
|
||||
_instance.clearSuppression();
|
||||
_closedViaBackKey = false;
|
||||
}
|
||||
|
||||
static bool consumeIfSuppressed(KeyEvent event) {
|
||||
if (!_suppressBackUntilKeyUp) return false;
|
||||
if (event.logicalKey.isBackKey) {
|
||||
if (event is KeyUpEvent) {
|
||||
_suppressBackUntilKeyUp = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool consumeIfSuppressed(KeyEvent event) => _instance.consumeIfSuppressed(event);
|
||||
}
|
||||
|
||||
/// Tracks whether a back key is currently physically pressed.
|
||||
|
||||
@@ -13,6 +13,9 @@ class FocusableAction {
|
||||
/// Icon color. Ignored when [child] is provided.
|
||||
final Color? iconColor;
|
||||
|
||||
/// Icon fill weight (0.0–1.0). Defaults to 1.0. Ignored when [child] is provided.
|
||||
final double iconFill;
|
||||
|
||||
final String? tooltip;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@@ -23,6 +26,7 @@ class FocusableAction {
|
||||
const FocusableAction({
|
||||
this.icon = Icons.circle,
|
||||
this.iconColor,
|
||||
this.iconFill = 1.0,
|
||||
this.tooltip,
|
||||
this.onPressed,
|
||||
this.child,
|
||||
@@ -84,8 +88,11 @@ class FocusableActionBarState extends State<FocusableActionBar> {
|
||||
late List<FocusNode> _focusNodes;
|
||||
late List<bool> _focusStates;
|
||||
|
||||
/// Access a focus node by index (e.g. for external `requestFocus()` calls).
|
||||
FocusNode getFocusNode(int index) => _focusNodes[index];
|
||||
FocusNode? getFocusNode(int index) => index >= 0 && index < _focusNodes.length ? _focusNodes[index] : null;
|
||||
|
||||
void requestFocusOnFirst() {
|
||||
if (_focusNodes.isNotEmpty) _focusNodes.first.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -135,9 +142,7 @@ class FocusableActionBarState extends State<FocusableActionBar> {
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < widget.actions.length; i++) _buildButton(i, isKeyboard, duration),
|
||||
],
|
||||
children: [for (var i = 0; i < widget.actions.length; i++) _buildButton(i, isKeyboard, duration)],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,9 +161,7 @@ class FocusableActionBarState extends State<FocusableActionBar> {
|
||||
}
|
||||
return dpadKeyHandler(
|
||||
onSelect: action.onPressed,
|
||||
onLeft: index > 0
|
||||
? () => _focusNodes[index - 1].requestFocus()
|
||||
: widget.onNavigateLeft,
|
||||
onLeft: index > 0 ? () => _focusNodes[index - 1].requestFocus() : widget.onNavigateLeft,
|
||||
onRight: index < _focusNodes.length - 1
|
||||
? () => _focusNodes[index + 1].requestFocus()
|
||||
: widget.onNavigateRight,
|
||||
@@ -171,9 +174,10 @@ class FocusableActionBarState extends State<FocusableActionBar> {
|
||||
duration: duration,
|
||||
child: Container(
|
||||
decoration: FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: 20),
|
||||
child: action.child ??
|
||||
child:
|
||||
action.child ??
|
||||
IconButton(
|
||||
icon: AppIcon(action.icon, fill: 1, color: action.iconColor),
|
||||
icon: AppIcon(action.icon, fill: action.iconFill, color: action.iconColor),
|
||||
tooltip: action.tooltip,
|
||||
onPressed: action.onPressed,
|
||||
),
|
||||
|
||||
@@ -85,11 +85,7 @@ class _FocusableButtonState extends State<FocusableButton> {
|
||||
onNavigateLeft: widget.onNavigateLeft,
|
||||
onNavigateRight: widget.onNavigateRight,
|
||||
onBack: widget.onBack,
|
||||
child: AnimatedOpacity(
|
||||
opacity: showFocus ? 1.0 : opacity,
|
||||
duration: duration,
|
||||
child: widget.child,
|
||||
),
|
||||
child: AnimatedOpacity(opacity: showFocus ? 1.0 : opacity, duration: duration, child: widget.child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'dpad_navigator.dart';
|
||||
import 'key_event_utils.dart';
|
||||
|
||||
/// Callbacks for chip key event handling.
|
||||
class ChipKeyCallbacks {
|
||||
/// Called when SELECT key is pressed.
|
||||
/// Called when SELECT key is pressed (short press when [onLongPress] is set).
|
||||
final VoidCallback? onSelect;
|
||||
|
||||
/// Called when SELECT key is held for 500ms.
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Called when DOWN arrow is pressed.
|
||||
final VoidCallback? onNavigateDown;
|
||||
|
||||
@@ -25,6 +31,7 @@ class ChipKeyCallbacks {
|
||||
|
||||
const ChipKeyCallbacks({
|
||||
this.onSelect,
|
||||
this.onLongPress,
|
||||
this.onNavigateDown,
|
||||
this.onNavigateUp,
|
||||
this.onNavigateLeft,
|
||||
@@ -53,6 +60,8 @@ class ChipKeyCallbacks {
|
||||
mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
|
||||
FocusNode? _internalFocusNode;
|
||||
bool _isFocused = false;
|
||||
Timer? _longPressTimer;
|
||||
bool _isSelectKeyDown = false;
|
||||
|
||||
/// Override to return the widget's optional external focus node.
|
||||
FocusNode? get widgetFocusNode;
|
||||
@@ -85,6 +94,7 @@ mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
|
||||
void disposeFocusNode() {
|
||||
focusNode.removeListener(_onFocusChange);
|
||||
_internalFocusNode?.dispose();
|
||||
_longPressTimer?.cancel();
|
||||
}
|
||||
|
||||
void _onFocusChange() {
|
||||
@@ -96,7 +106,7 @@ mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
|
||||
/// Shared key event handler for chip widgets.
|
||||
///
|
||||
/// Handles common key patterns:
|
||||
/// - SELECT key -> onSelect
|
||||
/// - SELECT key -> onSelect (short press) / onLongPress (hold 500ms)
|
||||
/// - Arrow keys -> navigation callbacks
|
||||
/// - BACK key -> onBack
|
||||
///
|
||||
@@ -112,16 +122,52 @@ mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
|
||||
}
|
||||
}
|
||||
|
||||
if (!event.isActionable) {
|
||||
return KeyEventResult.ignored;
|
||||
if (SelectKeyUpSuppressor.consumeIfSuppressed(event)) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// SELECT key activates the chip
|
||||
if (key.isSelectKey && callbacks.onSelect != null) {
|
||||
callbacks.onSelect!();
|
||||
// SELECT key with long press support
|
||||
if (key.isSelectKey) {
|
||||
if (callbacks.onLongPress != null) {
|
||||
if (event is KeyDownEvent) {
|
||||
if (!_isSelectKeyDown) {
|
||||
_isSelectKeyDown = true;
|
||||
_longPressTimer?.cancel();
|
||||
_longPressTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
|
||||
callbacks.onLongPress?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event is KeyRepeatEvent) {
|
||||
return KeyEventResult.handled;
|
||||
} else if (event is KeyUpEvent) {
|
||||
final timerWasActive = _longPressTimer?.isActive ?? false;
|
||||
_longPressTimer?.cancel();
|
||||
if (timerWasActive && _isSelectKeyDown) {
|
||||
callbacks.onSelect?.call();
|
||||
}
|
||||
_isSelectKeyDown = false;
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
} else if (callbacks.onSelect != null) {
|
||||
return handleOneShotSelect(event, callbacks.onSelect!);
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu key triggers long press directly
|
||||
if (event.isActionable && key.isContextMenuKey && callbacks.onLongPress != null) {
|
||||
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
|
||||
callbacks.onLongPress!();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (!event.isActionable) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
// LEFT arrow - call callback if provided, otherwise propagate to parent
|
||||
if (key.isLeftKey) {
|
||||
if (callbacks.onNavigateLeft != null) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'dpad_navigator.dart';
|
||||
import 'focusable_wrapper.dart';
|
||||
import 'input_mode_tracker.dart';
|
||||
|
||||
/// A D-pad friendly slider that uses LEFT/RIGHT to adjust value
|
||||
/// and lets UP/DOWN pass through for focus traversal.
|
||||
///
|
||||
/// Hides the thumb in keyboard mode when not focused, matching
|
||||
/// the timeline slider pattern.
|
||||
class FocusableSlider extends StatefulWidget {
|
||||
final double value;
|
||||
final double min;
|
||||
final double max;
|
||||
final int? divisions;
|
||||
final ValueChanged<double>? onChanged;
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
|
||||
const FocusableSlider({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.min = 0.0,
|
||||
this.max = 1.0,
|
||||
this.divisions,
|
||||
this.onChanged,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FocusableSlider> createState() => _FocusableSliderState();
|
||||
}
|
||||
|
||||
class _FocusableSliderState extends State<FocusableSlider> {
|
||||
bool _isFocused = false;
|
||||
|
||||
double get _step {
|
||||
if (widget.divisions != null && widget.divisions! > 0) {
|
||||
return (widget.max - widget.min) / widget.divisions!;
|
||||
}
|
||||
return (widget.max - widget.min) * 0.05;
|
||||
}
|
||||
|
||||
KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
|
||||
final key = event.logicalKey;
|
||||
if (key.isLeftKey || key.isRightKey) {
|
||||
if (event.isActionable && widget.onChanged != null) {
|
||||
final delta = key.isRightKey ? _step : -_step;
|
||||
final newValue = (widget.value + delta).clamp(widget.min, widget.max);
|
||||
widget.onChanged!(newValue);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusableWrapper(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
descendantsAreFocusable: false,
|
||||
disableScale: true,
|
||||
focusColor: Colors.transparent,
|
||||
onFocusChange: (focused) => setState(() => _isFocused = focused),
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
|
||||
thumbSize: WidgetStatePropertyAll(
|
||||
(!InputModeTracker.isKeyboardMode(context) || _isFocused) ? const Size(4, 20) : Size.zero,
|
||||
),
|
||||
),
|
||||
child: Slider(
|
||||
value: widget.value,
|
||||
min: widget.min,
|
||||
max: widget.max,
|
||||
divisions: widget.divisions,
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,17 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
final renderObject = context.findRenderObject();
|
||||
if (renderObject == null) return;
|
||||
|
||||
final scrollable = Scrollable.maybeOf(context);
|
||||
// Find the nearest vertical scrollable that actually has scroll range.
|
||||
// Skip inner scrollables with no extent (e.g. shrinkWrap ListView
|
||||
// with NeverScrollableScrollPhysics inside an outer scroll view)
|
||||
// and horizontal scrollables (e.g. TabBarView) since we only do
|
||||
// vertical scroll calculations.
|
||||
var scrollable = Scrollable.maybeOf(context);
|
||||
while (scrollable != null) {
|
||||
final pos = scrollable.position;
|
||||
if (pos.axis == Axis.vertical && pos.maxScrollExtent > pos.minScrollExtent) break;
|
||||
scrollable = Scrollable.maybeOf(scrollable.context);
|
||||
}
|
||||
if (scrollable == null) return;
|
||||
|
||||
final viewport = scrollable.context.findRenderObject() as RenderBox?;
|
||||
@@ -296,6 +306,7 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
scrollDelta -= (_focusDecorationPadding - projectedItemTop);
|
||||
}
|
||||
|
||||
if (!position.maxScrollExtent.isFinite) return;
|
||||
final targetOffset = (currentOffset + scrollDelta).clamp(position.minScrollExtent, position.maxScrollExtent);
|
||||
|
||||
position.animateTo(targetOffset, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
@@ -355,12 +366,8 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
_isSelectKeyDown = false;
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
} else {
|
||||
// Simple select handling without long-press
|
||||
if (event is KeyDownEvent) {
|
||||
widget.onSelect?.call();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
} else if (widget.onSelect != null) {
|
||||
return handleOneShotSelect(event, widget.onSelect!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +426,12 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
// Choose decoration based on useBackgroundFocus
|
||||
final decoration = widget.useBackgroundFocus
|
||||
? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius)
|
||||
: FocusTheme.focusDecoration(context, isFocused: showFocus, borderRadius: widget.borderRadius, color: widget.focusColor);
|
||||
: FocusTheme.focusDecoration(
|
||||
context,
|
||||
isFocused: showFocus,
|
||||
borderRadius: widget.borderRadius,
|
||||
color: widget.focusColor,
|
||||
);
|
||||
|
||||
Widget result = Focus(
|
||||
focusNode: _focusNode,
|
||||
|
||||
@@ -78,11 +78,32 @@ KeyEventResult handleBackKeyAction(KeyEvent event, VoidCallback onBack) {
|
||||
}
|
||||
|
||||
KeyEventResult handleBackKeyNavigation<T>(BuildContext context, KeyEvent event, {T? result}) {
|
||||
// Let system handle back if there's nothing to pop (e.g. root screen)
|
||||
if (!Navigator.canPop(context)) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
// Don't handle back when a dialog/overlay is on top of our route —
|
||||
// the overlay handles its own dismissal via DismissAction.
|
||||
if (ModalRoute.of(context)?.isCurrent != true) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
// Handle on KeyUpEvent to prevent double-pop when returning from child screens
|
||||
// (KeyDownEvent can be received by both the popping screen and the returned-to screen)
|
||||
return handleBackKeyAction(event, () => Navigator.pop(context, result));
|
||||
}
|
||||
|
||||
/// Handles a select key as a one-shot button activation.
|
||||
///
|
||||
/// Fires [onActivate] on the initial [KeyDownEvent] only.
|
||||
/// Consumes all select key events (down, repeat, up) to prevent
|
||||
/// unhandled events from reaching platform-level handling.
|
||||
/// Returns [KeyEventResult.ignored] for non-select keys.
|
||||
KeyEventResult handleOneShotSelect(KeyEvent event, VoidCallback onActivate) {
|
||||
if (!event.logicalKey.isSelectKey) return KeyEventResult.ignored;
|
||||
if (event is KeyDownEvent) onActivate();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
/// Creates a [FocusOnKeyEventCallback] that dispatches d-pad / arrow keys to
|
||||
/// the provided directional callbacks.
|
||||
///
|
||||
@@ -90,7 +111,8 @@ KeyEventResult handleBackKeyNavigation<T>(BuildContext context, KeyEvent event,
|
||||
/// (passed through to the framework). Directions mapped to a callback
|
||||
/// automatically return [KeyEventResult.handled].
|
||||
///
|
||||
/// Only [KeyDownEvent] and [KeyRepeatEvent] are handled (via [isActionable]).
|
||||
/// Directional keys repeat on [KeyRepeatEvent] (via [isActionable]).
|
||||
/// Select is one-shot: fires on [KeyDownEvent] only, consumes repeat and up.
|
||||
///
|
||||
/// ```dart
|
||||
/// Focus(
|
||||
@@ -111,6 +133,13 @@ FocusOnKeyEventCallback dpadKeyHandler({
|
||||
VoidCallback? onSelect,
|
||||
}) {
|
||||
return (FocusNode _, KeyEvent event) {
|
||||
// Select: one-shot activation (no repeat), must run before isActionable
|
||||
// filter so KeyUpEvent is also consumed.
|
||||
if (onSelect != null) {
|
||||
final result = handleOneShotSelect(event, onSelect);
|
||||
if (result != KeyEventResult.ignored) return result;
|
||||
}
|
||||
|
||||
if (!event.isActionable) return KeyEventResult.ignored;
|
||||
final key = event.logicalKey;
|
||||
|
||||
@@ -130,10 +159,6 @@ FocusOnKeyEventCallback dpadKeyHandler({
|
||||
onRight();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isSelectKey && onSelect != null) {
|
||||
onSelect();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,978 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Plezy"
|
||||
},
|
||||
"auth": {
|
||||
"signInWithPlex": "Log ind med Plex",
|
||||
"showQRCode": "Vis QR-kode",
|
||||
"authenticate": "Godkend",
|
||||
"authenticationTimeout": "Godkendelse fik timeout. Prøv igen.",
|
||||
"scanQRToSignIn": "Scan denne QR-kode for at logge ind",
|
||||
"waitingForAuth": "Venter på godkendelse...\nFærdiggør login i din browser.",
|
||||
"useBrowser": "Brug browser"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Annuller",
|
||||
"save": "Gem",
|
||||
"close": "Luk",
|
||||
"clear": "Ryd",
|
||||
"reset": "Nulstil",
|
||||
"later": "Senere",
|
||||
"submit": "Indsend",
|
||||
"confirm": "Bekræft",
|
||||
"retry": "Prøv igen",
|
||||
"logout": "Log ud",
|
||||
"unknown": "Ukendt",
|
||||
"refresh": "Opdater",
|
||||
"yes": "Ja",
|
||||
"no": "Nej",
|
||||
"delete": "Slet",
|
||||
"shuffle": "Bland",
|
||||
"addTo": "Tilføj til...",
|
||||
"createNew": "Opret ny",
|
||||
"connect": "Forbind",
|
||||
"disconnect": "Afbryd",
|
||||
"play": "Afspil",
|
||||
"pause": "Pause",
|
||||
"resume": "Genoptag",
|
||||
"error": "Fejl",
|
||||
"search": "Søg",
|
||||
"home": "Hjem",
|
||||
"back": "Tilbage",
|
||||
"settings": "Indstillinger",
|
||||
"mute": "Lydløs",
|
||||
"ok": "OK",
|
||||
"reconnect": "Genopret forbindelse",
|
||||
"exitConfirmTitle": "Luk app?",
|
||||
"exitConfirmMessage": "Er du sikker på, at du vil afslutte?",
|
||||
"dontAskAgain": "Spørg ikke igen",
|
||||
"exit": "Afslut",
|
||||
"viewAll": "Vis alle",
|
||||
"checkingNetwork": "Tjekker netværk...",
|
||||
"refreshingServers": "Opdaterer servere...",
|
||||
"loadingServers": "Indlæser servere...",
|
||||
"connectingToServers": "Forbinder til servere...",
|
||||
"startingOfflineMode": "Starter offlinetilstand...",
|
||||
"loading": "Indlæser..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licenser",
|
||||
"switchProfile": "Skift profil",
|
||||
"subtitleStyling": "Undertekststil",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"update": {
|
||||
"available": "Opdatering tilgængelig",
|
||||
"versionAvailable": "Version ${version} er tilgængelig",
|
||||
"currentVersion": "Nuværende: ${version}",
|
||||
"skipVersion": "Spring denne version over",
|
||||
"viewRelease": "Vis udgivelse",
|
||||
"latestVersion": "Du har den nyeste version",
|
||||
"checkFailed": "Kunne ikke søge efter opdateringer"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Indstillinger",
|
||||
"supportDeveloper": "Støt Plezy",
|
||||
"supportDeveloperDescription": "Doner via Liberapay for at finansiere udviklingen",
|
||||
"language": "Sprog",
|
||||
"theme": "Tema",
|
||||
"appearance": "Udseende",
|
||||
"videoPlayback": "Videoafspilning",
|
||||
"videoPlaybackDescription": "Konfigurer afspilningsadfærd",
|
||||
"advanced": "Avanceret",
|
||||
"episodePosterMode": "Episodeplakatstil",
|
||||
"seriesPoster": "Serieplakat",
|
||||
"seasonPoster": "Sæsonplakat",
|
||||
"episodeThumbnail": "Miniature",
|
||||
"showHeroSectionDescription": "Vis karrusel med udvalgt indhold på startskærmen",
|
||||
"secondsLabel": "Sekunder",
|
||||
"minutesLabel": "Minutter",
|
||||
"secondsShort": "s",
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Indtast varighed (${min}-${max})",
|
||||
"systemTheme": "System",
|
||||
"lightTheme": "Lys",
|
||||
"darkTheme": "Mørk",
|
||||
"oledTheme": "OLED",
|
||||
"libraryDensity": "Bibliotekstæthed",
|
||||
"compact": "Kompakt",
|
||||
"comfortable": "Komfortabel",
|
||||
"viewMode": "Visningstilstand",
|
||||
"gridView": "Gitter",
|
||||
"listView": "Liste",
|
||||
"showHeroSection": "Vis hero-sektion",
|
||||
"useGlobalHubs": "Brug Plex Home-layout",
|
||||
"useGlobalHubsDescription": "Vis startsidehubbe som den officielle Plex-klient. Når slået fra, vises anbefalinger per bibliotek.",
|
||||
"showServerNameOnHubs": "Vis servernavn på hubbe",
|
||||
"showServerNameOnHubsDescription": "Vis altid servernavnet i hubtitler. Når slået fra, vises kun ved duplikerede navne.",
|
||||
"alwaysKeepSidebarOpen": "Hold altid sidepanelet åbent",
|
||||
"alwaysKeepSidebarOpenDescription": "Sidepanelet forbliver udvidet, og indholdsområdet tilpasser sig",
|
||||
"showUnwatchedCount": "Vis antal usete",
|
||||
"showUnwatchedCountDescription": "Vis antal usete episoder på serier og sæsoner",
|
||||
"hideSpoilers": "Skjul spoilere for usete episoder",
|
||||
"hideSpoilersDescription": "Slør miniaturebilleder og skjul beskrivelser for episoder, du ikke har set endnu",
|
||||
"playerBackend": "Afspillerbackend",
|
||||
"exoPlayer": "ExoPlayer (Anbefalet)",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Hardwaredekodning",
|
||||
"hardwareDecodingDescription": "Brug hardwareacceleration når tilgængelig",
|
||||
"bufferSize": "Bufferstørrelse",
|
||||
"bufferSizeMB": "${size}MB",
|
||||
"bufferSizeAuto": "Auto (Anbefalet)",
|
||||
"bufferSizeWarning": "Din enhed har ${heap}MB hukommelse. En buffer på ${size}MB kan forårsage afspilningsproblemer.",
|
||||
"subtitleStyling": "Undertekststil",
|
||||
"subtitleStylingDescription": "Tilpas underteksters udseende",
|
||||
"smallSkipDuration": "Kort spring-varighed",
|
||||
"largeSkipDuration": "Lang spring-varighed",
|
||||
"rewindOnResume": "Spol tilbage ved genoptagelse",
|
||||
"secondsUnit": "${seconds} sekunder",
|
||||
"defaultSleepTimer": "Standard sove-timer",
|
||||
"minutesUnit": "${minutes} minutter",
|
||||
"rememberTrackSelections": "Husk sporvalg per serie/film",
|
||||
"rememberTrackSelectionsDescription": "Gem automatisk lyd- og undertekstsprogpræferencer når du skifter spor under afspilning",
|
||||
"clickVideoTogglesPlayback": "Klik på video skifter afspil/pause",
|
||||
"clickVideoTogglesPlaybackDescription": "Når aktiveret, afspiller/pauser klik på videoen. Ellers vises/skjules betjeningselementer.",
|
||||
"videoPlayerControls": "Videoafspillerkontroller",
|
||||
"keyboardShortcuts": "Tastaturgenveje",
|
||||
"keyboardShortcutsDescription": "Tilpas tastaturgenveje",
|
||||
"videoPlayerNavigation": "Videoafspillernavigation",
|
||||
"videoPlayerNavigationDescription": "Brug piletaster til at navigere videoafspillerkontroller",
|
||||
"watchTogetherRelay": "Watch Together-relay",
|
||||
"watchTogetherRelayDescription": "Angiv en brugerdefineret relay-server til Watch Together. Alle deltagere skal bruge den samme server.",
|
||||
"watchTogetherRelayHint": "https://min-relay.eksempel.dk",
|
||||
"crashReporting": "Fejlrapportering",
|
||||
"crashReportingDescription": "Send fejlrapporter for at hjælpe med at forbedre appen",
|
||||
"debugLogging": "Fejlfindingslogning",
|
||||
"debugLoggingDescription": "Aktiver detaljeret logning til fejlfinding",
|
||||
"viewLogs": "Vis logs",
|
||||
"viewLogsDescription": "Vis applikationslogs",
|
||||
"clearCache": "Ryd cache",
|
||||
"clearCacheDescription": "Dette rydder alle cachelagrede billeder og data. Appen kan tage længere tid om at indlæse indhold efter rydning.",
|
||||
"clearCacheSuccess": "Cache ryddet",
|
||||
"resetSettings": "Nulstil indstillinger",
|
||||
"resetSettingsDescription": "Alle indstillinger nulstilles til standardværdier. Denne handling kan ikke fortrydes.",
|
||||
"resetSettingsSuccess": "Indstillinger nulstillet",
|
||||
"shortcutsReset": "Genveje nulstillet til standard",
|
||||
"about": "Om",
|
||||
"aboutDescription": "App-information og licenser",
|
||||
"updates": "Opdateringer",
|
||||
"updateAvailable": "Opdatering tilgængelig",
|
||||
"checkForUpdates": "Søg efter opdateringer",
|
||||
"validationErrorEnterNumber": "Indtast et gyldigt tal",
|
||||
"validationErrorDuration": "Varighed skal være mellem ${min} og ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Genvej allerede tildelt til ${action}",
|
||||
"shortcutUpdated": "Genvej opdateret for ${action}",
|
||||
"autoSkip": "Auto-spring",
|
||||
"autoSkipIntro": "Auto-spring intro",
|
||||
"autoSkipIntroDescription": "Spring automatisk intromarkører over efter få sekunder",
|
||||
"autoSkipCredits": "Auto-spring rulletekster",
|
||||
"autoSkipCreditsDescription": "Spring automatisk rulletekster over og afspil næste episode",
|
||||
"autoSkipDelay": "Auto-spring forsinkelse",
|
||||
"autoSkipDelayDescription": "Vent ${seconds} sekunder før automatisk spring",
|
||||
"introPattern": "Intromarkørmønster",
|
||||
"introPatternDescription": "Regulært udtryk til at genkende intromarkører i kapiteltitler",
|
||||
"creditsPattern": "Rulletekstmarkørmønster",
|
||||
"creditsPatternDescription": "Regulært udtryk til at genkende rulletekstmarkører i kapiteltitler",
|
||||
"invalidRegex": "Ugyldigt regulært udtryk",
|
||||
"downloads": "Downloads",
|
||||
"downloadLocationDescription": "Vælg hvor downloadet indhold skal gemmes",
|
||||
"downloadLocationDefault": "Standard (App-lagring)",
|
||||
"downloadLocationCustom": "Brugerdefineret placering",
|
||||
"selectFolder": "Vælg mappe",
|
||||
"resetToDefault": "Nulstil til standard",
|
||||
"currentPath": "Nuværende: ${path}",
|
||||
"downloadLocationChanged": "Downloadplacering ændret",
|
||||
"downloadLocationReset": "Downloadplacering nulstillet",
|
||||
"downloadLocationInvalid": "Valgt mappe er ikke skrivbar",
|
||||
"downloadLocationSelectError": "Kunne ikke vælge mappe",
|
||||
"downloadOnWifiOnly": "Download kun på WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Forhindre downloads på mobildata",
|
||||
"autoRemoveWatchedDownloads": "Fjern sete downloads automatisk",
|
||||
"autoRemoveWatchedDownloadsDescription": "Slet automatisk downloadede episoder og film, når de markeres som set",
|
||||
"cellularDownloadBlocked": "Downloads er deaktiveret på mobildata. Opret forbindelse til WiFi eller ændr indstillingen.",
|
||||
"maxVolume": "Maksimal lydstyrke",
|
||||
"maxVolumeDescription": "Tillad lydstyrkeforstærkning over 100% for stille medier",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Vis hvad du ser på Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Synkroniser visningshistorik med Trakt",
|
||||
"companionRemoteServer": "Companion Remote Server",
|
||||
"companionRemoteServerDescription": "Tillad mobilenheder på dit netværk at styre denne app",
|
||||
"autoPip": "Auto billede-i-billede",
|
||||
"autoPipDescription": "Gå automatisk til billede-i-billede når du forlader appen under afspilning",
|
||||
"matchContentFrameRate": "Match indholdets billedhastighed",
|
||||
"matchContentFrameRateDescription": "Juster skærmens opdateringshastighed til videoindhold for at reducere hakken og spare batteri",
|
||||
"matchRefreshRate": "Match opdateringshastighed",
|
||||
"matchRefreshRateDescription": "Skift skærmens opdateringshastighed til at matche videoindhold i fuldskærm",
|
||||
"matchDynamicRange": "Match dynamisk område",
|
||||
"matchDynamicRangeDescription": "Aktiver automatisk HDR for HDR-indhold og gendan SDR når afspilleren lukkes",
|
||||
"displaySwitchDelay": "Forsinkelse ved skærmskift",
|
||||
"tunneledPlayback": "Tunneleret afspilning",
|
||||
"tunneledPlaybackDescription": "Brug hardwareaccelereret videotunneling. Deaktiver hvis du ser sort skærm med lyd på HDR-indhold",
|
||||
"requireProfileSelectionOnOpen": "Spørg om profil ved åbning",
|
||||
"requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åbnes",
|
||||
"confirmExitOnBack": "Bekræft før lukning",
|
||||
"confirmExitOnBackDescription": "Vis bekræftelsesdialog ved tryk på tilbage for at lukke appen",
|
||||
"forceTvMode": "Gennemtving TV-tilstand",
|
||||
"forceTvModeDescription": "Brug TV-layoutet uanset automatisk registrering. Nyttigt på Android TV-enheder, der ikke rapporterer leanback-funktionen. Genstarter appen ved ændring.",
|
||||
"autoHidePerformanceOverlay": "Skjul ydelses-overlay automatisk",
|
||||
"autoHidePerformanceOverlayDescription": "Fade ydelses-overlayet med afspilningskontrollerne",
|
||||
"showNavBarLabels": "Vis navigationsbarlabels",
|
||||
"showNavBarLabelsDescription": "Vis tekstlabels under navigationsbarikoner",
|
||||
"liveTvDefaultFavorites": "Standard til favoritkanaler",
|
||||
"liveTvDefaultFavoritesDescription": "Vis kun favoritkanaler ved åbning af Live TV",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Søg film, serier, musik...",
|
||||
"tryDifferentTerm": "Prøv en anden søgning",
|
||||
"searchYourMedia": "Søg i dine medier",
|
||||
"enterTitleActorOrKeyword": "Indtast titel, skuespiller eller nøgleord"
|
||||
},
|
||||
"hotkeys": {
|
||||
"setShortcutFor": "Indstil genvej for ${actionName}",
|
||||
"clearShortcut": "Ryd genvej",
|
||||
"actions": {
|
||||
"playPause": "Afspil/Pause",
|
||||
"volumeUp": "Lydstyrke op",
|
||||
"volumeDown": "Lydstyrke ned",
|
||||
"seekForward": "Spol frem (${seconds}s)",
|
||||
"seekBackward": "Spol tilbage (${seconds}s)",
|
||||
"fullscreenToggle": "Skift fuldskærm",
|
||||
"muteToggle": "Skift lydløs",
|
||||
"subtitleToggle": "Skift undertekster",
|
||||
"audioTrackNext": "Næste lydspor",
|
||||
"subtitleTrackNext": "Næste undertekstspor",
|
||||
"chapterNext": "Næste kapitel",
|
||||
"chapterPrevious": "Forrige kapitel",
|
||||
"speedIncrease": "Øg hastighed",
|
||||
"speedDecrease": "Sænk hastighed",
|
||||
"speedReset": "Nulstil hastighed",
|
||||
"subSeekNext": "Søg til næste undertekst",
|
||||
"subSeekPrev": "Søg til forrige undertekst",
|
||||
"shaderToggle": "Skift shadere",
|
||||
"skipMarker": "Spring intro/rulletekster over"
|
||||
}
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Filinfo",
|
||||
"video": "Video",
|
||||
"audio": "Lyd",
|
||||
"file": "Fil",
|
||||
"advanced": "Avanceret",
|
||||
"codec": "Codec",
|
||||
"resolution": "Opløsning",
|
||||
"bitrate": "Bitrate",
|
||||
"frameRate": "Billedhastighed",
|
||||
"aspectRatio": "Billedformat",
|
||||
"profile": "Profil",
|
||||
"bitDepth": "Bitdybde",
|
||||
"colorSpace": "Farverum",
|
||||
"colorRange": "Farveområde",
|
||||
"colorPrimaries": "Farveprimærer",
|
||||
"chromaSubsampling": "Chroma-subsampling",
|
||||
"channels": "Kanaler",
|
||||
"subtitles": "Undertekster",
|
||||
"overallBitrate": "Samlet bitrate",
|
||||
"path": "Sti",
|
||||
"size": "Størrelse",
|
||||
"container": "Container",
|
||||
"duration": "Varighed",
|
||||
"optimizedForStreaming": "Optimeret til streaming",
|
||||
"has64bitOffsets": "64-bit offsets"
|
||||
},
|
||||
"mediaMenu": {
|
||||
"markAsWatched": "Markér som set",
|
||||
"markAsUnwatched": "Markér som uset",
|
||||
"removeFromContinueWatching": "Fjern fra Fortsæt med at se",
|
||||
"goToSeries": "Gå til serie",
|
||||
"goToSeason": "Gå til sæson",
|
||||
"shufflePlay": "Afspil tilfældigt",
|
||||
"fileInfo": "Filinfo",
|
||||
"deleteFromServer": "Slet fra server",
|
||||
"confirmDelete": "Dette sletter permanent disse medier og filer fra din server. Dette kan ikke fortrydes.",
|
||||
"deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.",
|
||||
"mediaDeletedSuccessfully": "Medieelement slettet",
|
||||
"mediaFailedToDelete": "Kunne ikke slette medieelement",
|
||||
"rate": "Bedøm",
|
||||
"playFromBeginning": "Afspil fra begyndelsen",
|
||||
"playVersion": "Afspil version..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, film",
|
||||
"mediaCardShow": "${title}, TV-serie",
|
||||
"mediaCardEpisode": "${title}, ${episodeInfo}",
|
||||
"mediaCardSeason": "${title}, ${seasonInfo}",
|
||||
"mediaCardWatched": "set",
|
||||
"mediaCardPartiallyWatched": "${percent} procent set",
|
||||
"mediaCardUnwatched": "uset",
|
||||
"tapToPlay": "Tryk for at afspille"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Afspil tilfældigt",
|
||||
"playTrailer": "Afspil trailer",
|
||||
"markAsWatched": "Markér som set",
|
||||
"markAsUnwatched": "Markér som uset"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Lyd",
|
||||
"subtitlesLabel": "Undertekster",
|
||||
"resetToZero": "Nulstil til 0ms",
|
||||
"addTime": "+${amount}${unit}",
|
||||
"minusTime": "-${amount}${unit}",
|
||||
"playsLater": "${label} afspilles senere",
|
||||
"playsEarlier": "${label} afspilles tidligere",
|
||||
"noOffset": "Ingen forskydning",
|
||||
"letterbox": "Letterbox",
|
||||
"fillScreen": "Fyld skærm",
|
||||
"stretch": "Stræk",
|
||||
"lockRotation": "Lås rotation",
|
||||
"unlockRotation": "Lås rotation op",
|
||||
"timerActive": "Timer aktiv",
|
||||
"playbackWillPauseIn": "Afspilning pauses om ${duration}",
|
||||
"stillWatching": "Ser du stadig?",
|
||||
"pausingIn": "Pauser om ${seconds}s",
|
||||
"continueWatching": "Fortsæt",
|
||||
"autoPlayNext": "Auto-afspil næste",
|
||||
"playNext": "Afspil næste",
|
||||
"playButton": "Afspil",
|
||||
"pauseButton": "Pause",
|
||||
"seekBackwardButton": "Spol ${seconds} sekunder tilbage",
|
||||
"seekForwardButton": "Spol ${seconds} sekunder frem",
|
||||
"previousButton": "Forrige episode",
|
||||
"nextButton": "Næste episode",
|
||||
"previousChapterButton": "Forrige kapitel",
|
||||
"nextChapterButton": "Næste kapitel",
|
||||
"muteButton": "Lydløs",
|
||||
"unmuteButton": "Slå lyd til",
|
||||
"settingsButton": "Videoindstillinger",
|
||||
"tracksButton": "Lyd og undertekster",
|
||||
"chaptersButton": "Kapitler",
|
||||
"versionsButton": "Videoversioner",
|
||||
"pipButton": "Billede-i-billede-tilstand",
|
||||
"aspectRatioButton": "Billedformat",
|
||||
"ambientLighting": "Omgivelsesbelysning",
|
||||
"fullscreenButton": "Fuldskærm",
|
||||
"exitFullscreenButton": "Forlad fuldskærm",
|
||||
"alwaysOnTopButton": "Altid øverst",
|
||||
"rotationLockButton": "Rotationslås",
|
||||
"lockScreen": "Lås skærm",
|
||||
"screenLockButton": "Skærmlås",
|
||||
"longPressToUnlock": "Langt tryk for at låse op",
|
||||
"timelineSlider": "Videotidslinje",
|
||||
"volumeSlider": "Lydstyrkeniveau",
|
||||
"endsAt": "Slutter kl. ${time}",
|
||||
"pipActive": "Afspiller i billede-i-billede",
|
||||
"pipFailed": "Billede-i-billede kunne ikke starte",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Kræver Android 8.0 eller nyere",
|
||||
"iosVersion": "Kræver iOS 15.0 eller nyere",
|
||||
"permissionDisabled": "Billede-i-billede-tilladelse er deaktiveret. Aktivér i Indstillinger > Apps > Plezy > Billede-i-billede",
|
||||
"notSupported": "Enheden understøtter ikke billede-i-billede",
|
||||
"voSwitchFailed": "Kunne ikke skifte videooutput til billede-i-billede",
|
||||
"failed": "Billede-i-billede kunne ikke starte",
|
||||
"unknown": "Der opstod en fejl: ${error}"
|
||||
},
|
||||
"chapters": "Kapitler",
|
||||
"noChaptersAvailable": "Ingen kapitler tilgængelige",
|
||||
"queue": "Kø",
|
||||
"noQueueItems": "Ingen elementer i køen",
|
||||
"searchSubtitles": "Søg undertekster",
|
||||
"language": "Sprog",
|
||||
"noSubtitlesFound": "Ingen undertekster fundet",
|
||||
"subtitleDownloaded": "Undertekst downloadet",
|
||||
"subtitleDownloadFailed": "Kunne ikke downloade undertekst",
|
||||
"searchLanguages": "Søg sprog..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrator",
|
||||
"restricted": "Begrænset",
|
||||
"protected": "Beskyttet",
|
||||
"current": "NUVÆRENDE"
|
||||
},
|
||||
"messages": {
|
||||
"markedAsWatched": "Markeret som set",
|
||||
"markedAsUnwatched": "Markeret som uset",
|
||||
"markedAsWatchedOffline": "Markeret som set (synkroniseres online)",
|
||||
"markedAsUnwatchedOffline": "Markeret som uset (synkroniseres online)",
|
||||
"autoRemovedWatchedDownload": "Automatisk fjernet: ${title}",
|
||||
"removedFromContinueWatching": "Fjernet fra Fortsæt med at se",
|
||||
"errorLoading": "Fejl: ${error}",
|
||||
"fileInfoNotAvailable": "Filinfo ikke tilgængelig",
|
||||
"errorLoadingFileInfo": "Fejl ved indlæsning af filinfo: ${error}",
|
||||
"errorLoadingSeries": "Fejl ved indlæsning af serie",
|
||||
"errorLoadingSeason": "Fejl ved indlæsning af sæson",
|
||||
"musicNotSupported": "Musikafspilning understøttes endnu ikke",
|
||||
"logsCleared": "Logs ryddet",
|
||||
"logsCopied": "Logs kopieret til udklipsholder",
|
||||
"noLogsAvailable": "Ingen logs tilgængelige",
|
||||
"libraryScanning": "Scanner \"${title}\"...",
|
||||
"libraryScanStarted": "Biblioteksscanning startet for \"${title}\"",
|
||||
"libraryScanFailed": "Kunne ikke scanne bibliotek: ${error}",
|
||||
"metadataRefreshing": "Opdaterer metadata for \"${title}\"...",
|
||||
"metadataRefreshStarted": "Metadataopdatering startet for \"${title}\"",
|
||||
"metadataRefreshFailed": "Kunne ikke opdatere metadata: ${error}",
|
||||
"logoutConfirm": "Er du sikker på, at du vil logge ud?",
|
||||
"noSeasonsFound": "Ingen sæsoner fundet",
|
||||
"noEpisodesFound": "Ingen episoder fundet i første sæson",
|
||||
"noEpisodesFoundGeneral": "Ingen episoder fundet",
|
||||
"noResultsFound": "Ingen resultater fundet",
|
||||
"sleepTimerSet": "Sove-timer indstillet til ${label}",
|
||||
"noItemsAvailable": "Ingen elementer tilgængelige",
|
||||
"failedToCreatePlayQueueNoItems": "Kunne ikke oprette afspilningskø — ingen elementer",
|
||||
"failedPlayback": "Kunne ikke ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Skifter til kompatibel afspiller...",
|
||||
"serverLimitTitle": "Afspilning mislykkedes",
|
||||
"serverLimitBody": "Serveren returnerede en fejl (HTTP 500). Det betyder som regel, at serverejeren har sat en båndbredde- eller transkodningsgrænse, der afviser din session. Der er intet at gøre fra klienten — serverejeren skal justere sine indstillinger.",
|
||||
"logsUploaded": "Logs uploadet",
|
||||
"logsUploadFailed": "Kunne ikke uploade logs",
|
||||
"logId": "Log-ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"text": "Tekst",
|
||||
"border": "Kant",
|
||||
"background": "Baggrund",
|
||||
"fontSize": "Skriftstørrelse",
|
||||
"textColor": "Tekstfarve",
|
||||
"borderSize": "Kantstørrelse",
|
||||
"borderColor": "Kantfarve",
|
||||
"backgroundOpacity": "Baggrundsgennemsigtighed",
|
||||
"backgroundColor": "Baggrundsfarve",
|
||||
"position": "Position",
|
||||
"assOverride": "ASS-tilsidesættelse",
|
||||
"bold": "Fed",
|
||||
"italic": "Kursiv"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "mpv.conf",
|
||||
"description": "Avancerede videoafspillerindstillinger",
|
||||
"presets": "Forudindstillinger",
|
||||
"noPresets": "Ingen gemte forudindstillinger",
|
||||
"saveAsPreset": "Gem som forudindstilling...",
|
||||
"presetName": "Forudindstillingsnavn",
|
||||
"presetNameHint": "Indtast et navn for denne forudindstilling",
|
||||
"loadPreset": "Indlæs",
|
||||
"deletePreset": "Slet",
|
||||
"presetSaved": "Forudindstilling gemt",
|
||||
"presetLoaded": "Forudindstilling indlæst",
|
||||
"presetDeleted": "Forudindstilling slettet",
|
||||
"confirmDeletePreset": "Er du sikker på, at du vil slette denne forudindstilling?",
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Bekræft handling"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Opdag",
|
||||
"switchProfile": "Skift profil",
|
||||
"noContentAvailable": "Intet indhold tilgængeligt",
|
||||
"addMediaToLibraries": "Tilføj medier til dine biblioteker",
|
||||
"continueWatching": "Fortsæt med at se",
|
||||
"playEpisode": "S${season}E${episode}",
|
||||
"overview": "Oversigt",
|
||||
"cast": "Rollebesætning",
|
||||
"extras": "Trailere og ekstra",
|
||||
"studio": "Studie",
|
||||
"rating": "Bedømmelse",
|
||||
"movie": "Film",
|
||||
"tvShow": "TV-serie",
|
||||
"minutesLeft": "${minutes} min tilbage"
|
||||
},
|
||||
"errors": {
|
||||
"searchFailed": "Søgning mislykkedes: ${error}",
|
||||
"connectionTimeout": "Forbindelsestimeout ved indlæsning af ${context}",
|
||||
"connectionFailed": "Kunne ikke forbinde til Plex-server",
|
||||
"failedToLoad": "Kunne ikke indlæse ${context}: ${error}",
|
||||
"noClientAvailable": "Ingen klient tilgængelig",
|
||||
"authenticationFailed": "Godkendelse mislykkedes: ${error}",
|
||||
"couldNotLaunchUrl": "Kunne ikke åbne godkendelses-URL",
|
||||
"pleaseEnterToken": "Indtast et token",
|
||||
"invalidToken": "Ugyldigt token",
|
||||
"failedToVerifyToken": "Kunne ikke verificere token: ${error}",
|
||||
"failedToSwitchProfile": "Kunne ikke skifte til ${displayName}"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Biblioteker",
|
||||
"scanLibraryFiles": "Scan biblioteksfiler",
|
||||
"scanLibrary": "Scan bibliotek",
|
||||
"analyze": "Analysér",
|
||||
"analyzeLibrary": "Analysér bibliotek",
|
||||
"refreshMetadata": "Opdater metadata",
|
||||
"emptyTrash": "Tøm papirkurv",
|
||||
"emptyingTrash": "Tømmer papirkurv for \"${title}\"...",
|
||||
"trashEmptied": "Papirkurv tømt for \"${title}\"",
|
||||
"failedToEmptyTrash": "Kunne ikke tømme papirkurv: ${error}",
|
||||
"analyzing": "Analyserer \"${title}\"...",
|
||||
"analysisStarted": "Analyse startet for \"${title}\"",
|
||||
"failedToAnalyze": "Kunne ikke analysere bibliotek: ${error}",
|
||||
"noLibrariesFound": "Ingen biblioteker fundet",
|
||||
"thisLibraryIsEmpty": "Dette bibliotek er tomt",
|
||||
"all": "Alle",
|
||||
"clearAll": "Ryd alle",
|
||||
"scanLibraryConfirm": "Er du sikker på, at du vil scanne \"${title}\"?",
|
||||
"analyzeLibraryConfirm": "Er du sikker på, at du vil analysere \"${title}\"?",
|
||||
"refreshMetadataConfirm": "Er du sikker på, at du vil opdatere metadata for \"${title}\"?",
|
||||
"emptyTrashConfirm": "Er du sikker på, at du vil tømme papirkurven for \"${title}\"?",
|
||||
"manageLibraries": "Administrer biblioteker",
|
||||
"sort": "Sortér",
|
||||
"sortBy": "Sortér efter",
|
||||
"filters": "Filtre",
|
||||
"confirmActionMessage": "Er du sikker på, at du vil udføre denne handling?",
|
||||
"showLibrary": "Vis bibliotek",
|
||||
"hideLibrary": "Skjul bibliotek",
|
||||
"libraryOptions": "Biblioteksindstillinger",
|
||||
"content": "biblioteksindhold",
|
||||
"selectLibrary": "Vælg bibliotek",
|
||||
"filtersWithCount": "Filtre (${count})",
|
||||
"noRecommendations": "Ingen anbefalinger tilgængelige",
|
||||
"noCollections": "Ingen samlinger i dette bibliotek",
|
||||
"noFoldersFound": "Ingen mapper fundet",
|
||||
"folders": "mapper",
|
||||
"tabs": {
|
||||
"recommended": "Anbefalet",
|
||||
"browse": "Gennemse",
|
||||
"collections": "Samlinger",
|
||||
"playlists": "Playlister"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Gruppering",
|
||||
"all": "Alle",
|
||||
"movies": "Film",
|
||||
"shows": "TV-serier",
|
||||
"seasons": "Sæsoner",
|
||||
"episodes": "Episoder",
|
||||
"folders": "Mapper"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "Om",
|
||||
"openSourceLicenses": "Open source-licenser",
|
||||
"versionLabel": "Version ${version}",
|
||||
"appDescription": "En smuk Plex-klient til Flutter",
|
||||
"viewLicensesDescription": "Se licenser for tredjepartsbiblioteker"
|
||||
},
|
||||
"serverSelection": {
|
||||
"allServerConnectionsFailed": "Kunne ikke forbinde til nogen servere. Tjek dit netværk og prøv igen.",
|
||||
"noServersFoundForAccount": "Ingen servere fundet for ${username} (${email})",
|
||||
"failedToLoadServers": "Kunne ikke indlæse servere: ${error}"
|
||||
},
|
||||
"hubDetail": {
|
||||
"title": "Titel",
|
||||
"releaseYear": "Udgivelsesår",
|
||||
"dateAdded": "Tilføjelsesdato",
|
||||
"rating": "Bedømmelse",
|
||||
"noItemsFound": "Ingen elementer fundet"
|
||||
},
|
||||
"logs": {
|
||||
"clearLogs": "Ryd logs",
|
||||
"copyLogs": "Kopiér logs",
|
||||
"uploadLogs": "Upload logs"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Relaterede pakker",
|
||||
"license": "Licens",
|
||||
"licenseNumber": "Licens ${number}",
|
||||
"licensesCount": "${count} licenser"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Biblioteker",
|
||||
"downloads": "Downloads",
|
||||
"liveTv": "Live TV"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "Live TV",
|
||||
"guide": "Guide",
|
||||
"noChannels": "Ingen kanaler tilgængelige",
|
||||
"noDvr": "Ingen DVR konfigureret på nogen server",
|
||||
"noPrograms": "Ingen programdata tilgængelig",
|
||||
"live": "LIVE",
|
||||
"reloadGuide": "Genindlæs guide",
|
||||
"now": "Nu",
|
||||
"today": "I dag",
|
||||
"midnight": "Midnat",
|
||||
"overnight": "Nat",
|
||||
"morning": "Morgen",
|
||||
"daytime": "Dagtid",
|
||||
"evening": "Aften",
|
||||
"lateNight": "Sen aften",
|
||||
"whatsOn": "Hvad der kører",
|
||||
"watchChannel": "Se kanal",
|
||||
"favorites": "Favoritter",
|
||||
"reorderFavorites": "Omarranger favoritter",
|
||||
"joinSession": "Deltag i igangværende session",
|
||||
"watchFromStart": "Se fra start (${minutes} min siden)",
|
||||
"watchLive": "Se live",
|
||||
"goToLive": "Gå til live"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Samlinger",
|
||||
"collection": "Samling",
|
||||
"empty": "Samlingen er tom",
|
||||
"unknownLibrarySection": "Kan ikke slette: Ukendt bibliotekssektion",
|
||||
"deleteCollection": "Slet samling",
|
||||
"deleteConfirm": "Er du sikker på, at du vil slette \"${title}\"? Denne handling kan ikke fortrydes.",
|
||||
"deleted": "Samling slettet",
|
||||
"deleteFailed": "Kunne ikke slette samling",
|
||||
"deleteFailedWithError": "Kunne ikke slette samling: ${error}",
|
||||
"failedToLoadItems": "Kunne ikke indlæse samlingselementer: ${error}",
|
||||
"selectCollection": "Vælg samling",
|
||||
"collectionName": "Samlingsnavn",
|
||||
"enterCollectionName": "Indtast samlingsnavn",
|
||||
"addedToCollection": "Tilføjet til samling",
|
||||
"errorAddingToCollection": "Kunne ikke tilføje til samling",
|
||||
"created": "Samling oprettet",
|
||||
"removeFromCollection": "Fjern fra samling",
|
||||
"removeFromCollectionConfirm": "Fjern \"${title}\" fra denne samling?",
|
||||
"removedFromCollection": "Fjernet fra samling",
|
||||
"removeFromCollectionFailed": "Kunne ikke fjerne fra samling",
|
||||
"removeFromCollectionError": "Fejl ved fjernelse fra samling: ${error}",
|
||||
"searchCollections": "Søg i samlinger..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Playlister",
|
||||
"playlist": "Playliste",
|
||||
"noPlaylists": "Ingen playlister fundet",
|
||||
"create": "Opret playliste",
|
||||
"playlistName": "Playlistenavn",
|
||||
"enterPlaylistName": "Indtast playlistenavn",
|
||||
"delete": "Slet playliste",
|
||||
"removeItem": "Fjern fra playliste",
|
||||
"smartPlaylist": "Smart playliste",
|
||||
"itemCount": "${count} elementer",
|
||||
"oneItem": "1 element",
|
||||
"emptyPlaylist": "Denne playliste er tom",
|
||||
"deleteConfirm": "Slet playliste?",
|
||||
"deleteMessage": "Er du sikker på, at du vil slette \"${name}\"?",
|
||||
"created": "Playliste oprettet",
|
||||
"deleted": "Playliste slettet",
|
||||
"itemAdded": "Tilføjet til playliste",
|
||||
"itemRemoved": "Fjernet fra playliste",
|
||||
"selectPlaylist": "Vælg playliste",
|
||||
"errorCreating": "Kunne ikke oprette playliste",
|
||||
"errorDeleting": "Kunne ikke slette playliste",
|
||||
"errorLoading": "Kunne ikke indlæse playlister",
|
||||
"errorAdding": "Kunne ikke tilføje til playliste",
|
||||
"errorReordering": "Kunne ikke ændre rækkefølge på playlisteelement",
|
||||
"errorRemoving": "Kunne ikke fjerne fra playliste"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Se sammen",
|
||||
"description": "Se indhold synkroniseret med venner og familie",
|
||||
"createSession": "Opret session",
|
||||
"creating": "Opretter...",
|
||||
"joinSession": "Deltag i session",
|
||||
"joining": "Deltager...",
|
||||
"controlMode": "Kontroltilstand",
|
||||
"controlModeQuestion": "Hvem kan styre afspilning?",
|
||||
"hostOnly": "Kun vært",
|
||||
"anyone": "Alle",
|
||||
"hostingSession": "Vært for session",
|
||||
"inSession": "I session",
|
||||
"sessionCode": "Sessionskode",
|
||||
"hostControlsPlayback": "Vært styrer afspilning",
|
||||
"anyoneCanControl": "Alle kan styre afspilning",
|
||||
"hostControls": "Værtskontrol",
|
||||
"anyoneControls": "Alle styrer",
|
||||
"participants": "Deltagere",
|
||||
"host": "Vært",
|
||||
"hostBadge": "HOST",
|
||||
"youAreHost": "Du er vært",
|
||||
"watchingWithOthers": "Ser med andre",
|
||||
"endSession": "Afslut session",
|
||||
"leaveSession": "Forlad session",
|
||||
"endSessionQuestion": "Afslut session?",
|
||||
"leaveSessionQuestion": "Forlad session?",
|
||||
"endSessionConfirm": "Dette afslutter sessionen for alle deltagere.",
|
||||
"leaveSessionConfirm": "Du vil blive fjernet fra sessionen.",
|
||||
"endSessionConfirmOverlay": "Dette afslutter se-sessionen for alle deltagere.",
|
||||
"leaveSessionConfirmOverlay": "Du vil blive afbrudt fra se-sessionen.",
|
||||
"end": "Afslut",
|
||||
"leave": "Forlad",
|
||||
"syncing": "Synkroniserer...",
|
||||
"joinWatchSession": "Deltag i se-session",
|
||||
"enterCodeHint": "Indtast 5-tegns kode",
|
||||
"pasteFromClipboard": "Indsæt fra udklipsholder",
|
||||
"pleaseEnterCode": "Indtast en sessionskode",
|
||||
"codeMustBe5Chars": "Sessionskode skal være 5 tegn",
|
||||
"joinInstructions": "Indtast sessionskoden delt af værten for at deltage i se-sessionen.",
|
||||
"failedToCreate": "Kunne ikke oprette session",
|
||||
"failedToJoin": "Kunne ikke deltage i session",
|
||||
"sessionCodeCopied": "Sessionskode kopieret til udklipsholder",
|
||||
"relayUnreachable": "Relay-serveren kan ikke nås. Dette kan skyldes, at din udbyder blokerer forbindelsen. Du kan stadig prøve, men Se sammen virker muligvis ikke.",
|
||||
"reconnectingToHost": "Genopretter forbindelse til vært...",
|
||||
"currentPlayback": "Nuværende afspilning",
|
||||
"joinCurrentPlayback": "Deltag i nuværende afspilning",
|
||||
"joinCurrentPlaybackDescription": "Hop tilbage til det værten ser nu",
|
||||
"failedToOpenCurrentPlayback": "Kunne ikke åbne nuværende afspilning",
|
||||
"participantJoined": "${name} deltog",
|
||||
"participantLeft": "${name} forlod",
|
||||
"participantPaused": "${name} satte på pause",
|
||||
"participantResumed": "${name} genoptog",
|
||||
"participantSeeked": "${name} spoled",
|
||||
"participantBuffering": "${name} bufferer",
|
||||
"waitingForParticipants": "Venter på at andre indlæser...",
|
||||
"recentRooms": "Seneste rum",
|
||||
"renameRoom": "Omdøb rum",
|
||||
"removeRoom": "Fjern"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"manage": "Administrer",
|
||||
"tvShows": "TV-serier",
|
||||
"movies": "Film",
|
||||
"noDownloads": "Ingen downloads endnu",
|
||||
"noDownloadsDescription": "Downloadet indhold vises her til offlinevisning",
|
||||
"downloadNow": "Download",
|
||||
"deleteDownload": "Slet download",
|
||||
"retryDownload": "Prøv download igen",
|
||||
"downloadQueued": "Download i kø",
|
||||
"serverErrorBitrate": "Serverfejl — filen overskrider muligvis grænsen for fjernstreaming-bitrate",
|
||||
"episodesQueued": "${count} episoder i downloadkø",
|
||||
"downloadDeleted": "Download slettet",
|
||||
"deleteConfirm": "Er du sikker på, at du vil slette \"${title}\"? Den downloadede fil fjernes fra din enhed.",
|
||||
"deletingWithProgress": "Sletter ${title}... (${current} af ${total})",
|
||||
"noDownloadsTree": "Ingen downloads",
|
||||
"pauseAll": "Pause alle",
|
||||
"resumeAll": "Genoptag alle",
|
||||
"deleteAll": "Slet alle",
|
||||
"selectVersion": "Vælg version",
|
||||
"allEpisodes": "Alle episoder",
|
||||
"unwatchedOnly": "Kun usete",
|
||||
"nextNUnwatched": "Næste ${count} usete",
|
||||
"customAmount": "Angiv antal...",
|
||||
"howManyEpisodes": "Hvor mange episoder?",
|
||||
"itemsQueued": "${count} elementer sat i kø til download",
|
||||
"keepSynced": "Hold synkroniseret",
|
||||
"downloadOnce": "Download én gang",
|
||||
"keepNUnwatched": "Behold ${count} usete",
|
||||
"editSyncRule": "Rediger synkroniseringsregel",
|
||||
"removeSyncRule": "Fjern synkroniseringsregel",
|
||||
"removeSyncRuleConfirm": "Stop synkronisering af \"${title}\"? Downloadede episoder beholdes.",
|
||||
"syncRuleCreated": "Synkroniseringsregel oprettet — beholder ${count} usete episoder",
|
||||
"syncRuleUpdated": "Synkroniseringsregel opdateret",
|
||||
"syncRuleRemoved": "Synkroniseringsregel fjernet",
|
||||
"syncedNewEpisodes": "Synkroniserede ${count} nye episoder for ${title}",
|
||||
"activeSyncRules": "Synkroniseringsregler",
|
||||
"noSyncRules": "Ingen synkroniseringsregler",
|
||||
"manageSyncRule": "Administrer synkronisering",
|
||||
"editEpisodeCount": "Antal episoder",
|
||||
"editSyncFilter": "Synkroniseringsfilter",
|
||||
"syncAllItems": "Synkroniserer alle elementer",
|
||||
"syncUnwatchedItems": "Synkroniserer usete elementer",
|
||||
"syncRuleListCreated": "Synkroniseringsregel oprettet"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shadere",
|
||||
"noShaderDescription": "Ingen videoforbedring",
|
||||
"nvscalerDescription": "NVIDIA-billedskalering for skarpere video",
|
||||
"qualityFast": "Hurtig",
|
||||
"qualityHQ": "Høj kvalitet",
|
||||
"mode": "Tilstand",
|
||||
"importShader": "Importér shader",
|
||||
"customShaderDescription": "Brugerdefineret GLSL-shader",
|
||||
"shaderImported": "Shader importeret",
|
||||
"shaderImportFailed": "Kunne ikke importere shader",
|
||||
"deleteShader": "Slet shader",
|
||||
"deleteShaderConfirm": "Slet \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Fjernbetjening",
|
||||
"connectedTo": "Forbundet til ${name}",
|
||||
"session": {
|
||||
"startingServer": "Starter fjernserver...",
|
||||
"failedToCreate": "Kunne ikke starte fjernserver:",
|
||||
"hostAddress": "Værtsadresse",
|
||||
"connected": "Forbundet",
|
||||
"serverRunning": "Fjernserver aktiv",
|
||||
"serverStopped": "Fjernserver stoppet",
|
||||
"serverRunningDescription": "Mobilenheder på dit netværk kan finde og oprette forbindelse til denne app",
|
||||
"serverStoppedDescription": "Start serveren for at tillade mobilenheder at oprette forbindelse",
|
||||
"usePhoneToControl": "Brug din mobilenhed til at styre denne app",
|
||||
"startServer": "Start server",
|
||||
"stopServer": "Stop server",
|
||||
"minimize": "Minimér"
|
||||
},
|
||||
"pairing": {
|
||||
"discoveryDescription": "Enheder på dit netværk, der kører Plezy med den samme Plex-konto, vises automatisk",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"connecting": "Opretter forbindelse...",
|
||||
"searchingForDevices": "Søger efter enheder...",
|
||||
"noDevicesFound": "Ingen enheder fundet på dit netværk",
|
||||
"noDevicesHint": "Sørg for, at Plezy er åben på din computer, og at begge enheder er på det samme WiFi-netværk",
|
||||
"availableDevices": "Tilgængelige enheder",
|
||||
"manualConnection": "Manuel forbindelse",
|
||||
"cryptoInitFailed": "Kunne ikke initialisere sikker forbindelse. Sørg for, at du er logget ind på en Plex-konto.",
|
||||
"validationHostRequired": "Angiv venligst værtsadresse",
|
||||
"validationHostFormat": "Format skal være IP:port (f.eks. 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "Forbindelsen udløb. Sørg for, at begge enheder er på det samme netværk.",
|
||||
"sessionNotFound": "Kunne ikke finde enheden. Sørg for, at Plezy kører på værten.",
|
||||
"authFailed": "Godkendelse mislykkedes. Sørg for, at begge enheder bruger den samme Plex-konto.",
|
||||
"failedToConnect": "Kunne ikke oprette forbindelse: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
"disconnectConfirm": "Vil du afbryde fra fjernsessionen?",
|
||||
"reconnecting": "Genopretter forbindelse...",
|
||||
"attemptOf": "Forsøg ${current} af 5",
|
||||
"retryNow": "Prøv igen nu",
|
||||
"tabRemote": "Fjernbetjening",
|
||||
"tabPlay": "Afspil",
|
||||
"tabMore": "Mere",
|
||||
"menu": "Menu",
|
||||
"tabNavigation": "Fanenavigation",
|
||||
"tabDiscover": "Opdag",
|
||||
"tabLibraries": "Biblioteker",
|
||||
"tabSearch": "Søg",
|
||||
"tabDownloads": "Downloads",
|
||||
"tabSettings": "Indstillinger",
|
||||
"previous": "Forrige",
|
||||
"playPause": "Afspil/Pause",
|
||||
"next": "Næste",
|
||||
"seekBack": "Spol tilbage",
|
||||
"stop": "Stop",
|
||||
"seekForward": "Spol frem",
|
||||
"volume": "Lydstyrke",
|
||||
"volumeDown": "Ned",
|
||||
"volumeUp": "Op",
|
||||
"fullscreen": "Fuldskærm",
|
||||
"subtitles": "Undertekster",
|
||||
"audio": "Lyd",
|
||||
"searchHint": "Søg på desktop..."
|
||||
}
|
||||
},
|
||||
"videoSettings": {
|
||||
"playbackSettings": "Afspilningsindstillinger",
|
||||
"playbackSpeed": "Afspilningshastighed",
|
||||
"sleepTimer": "Sove-timer",
|
||||
"audioSync": "Lydsynkronisering",
|
||||
"subtitleSync": "Undertekstsynkronisering",
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Lydoutput",
|
||||
"performanceOverlay": "Ydelsesoverlay",
|
||||
"audioPassthrough": "Lyd-passthrough",
|
||||
"audioNormalization": "Lydnormalisering"
|
||||
},
|
||||
"externalPlayer": {
|
||||
"title": "Ekstern afspiller",
|
||||
"useExternalPlayer": "Brug ekstern afspiller",
|
||||
"useExternalPlayerDescription": "Åbn videoer i en ekstern app i stedet for den indbyggede afspiller",
|
||||
"selectPlayer": "Vælg afspiller",
|
||||
"customPlayers": "Brugerdefinerede afspillere",
|
||||
"systemDefault": "Systemstandard",
|
||||
"addCustomPlayer": "Tilføj brugerdefineret afspiller",
|
||||
"playerName": "Afspillernavn",
|
||||
"playerCommand": "Kommando",
|
||||
"playerPackage": "Pakkenavn",
|
||||
"playerUrlScheme": "URL-skema",
|
||||
"off": "Fra",
|
||||
"launchFailed": "Kunne ikke åbne ekstern afspiller",
|
||||
"appNotInstalled": "${name} er ikke installeret",
|
||||
"playInExternalPlayer": "Afspil i ekstern afspiller"
|
||||
},
|
||||
"metadataEdit": {
|
||||
"editMetadata": "Redigér...",
|
||||
"screenTitle": "Redigér metadata",
|
||||
"basicInfo": "Grundlæggende info",
|
||||
"artwork": "Grafik",
|
||||
"advancedSettings": "Avancerede indstillinger",
|
||||
"title": "Titel",
|
||||
"sortTitle": "Sorteringstitel",
|
||||
"originalTitle": "Originaltitel",
|
||||
"releaseDate": "Udgivelsesdato",
|
||||
"contentRating": "Aldersgrænse",
|
||||
"studio": "Studie",
|
||||
"tagline": "Tagline",
|
||||
"summary": "Resumé",
|
||||
"poster": "Plakat",
|
||||
"background": "Baggrund",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Kvadratisk billede",
|
||||
"selectPoster": "Vælg plakat",
|
||||
"selectBackground": "Vælg baggrund",
|
||||
"selectLogo": "Vælg logo",
|
||||
"selectSquareArt": "Vælg kvadratisk billede",
|
||||
"fromUrl": "Fra URL",
|
||||
"uploadFile": "Upload fil",
|
||||
"enterImageUrl": "Indtast billed-URL",
|
||||
"imageUrl": "Billed-URL",
|
||||
"metadataUpdated": "Metadata opdateret",
|
||||
"metadataUpdateFailed": "Kunne ikke opdatere metadata",
|
||||
"artworkUpdated": "Grafik opdateret",
|
||||
"artworkUpdateFailed": "Kunne ikke opdatere grafik",
|
||||
"noArtworkAvailable": "Ingen grafik tilgængelig",
|
||||
"notSet": "Ikke indstillet",
|
||||
"libraryDefault": "Biblioteksstandard",
|
||||
"accountDefault": "Kontostandard",
|
||||
"seriesDefault": "Seriestandard",
|
||||
"episodeSorting": "Episodesortering",
|
||||
"oldestFirst": "Ældste først",
|
||||
"newestFirst": "Nyeste først",
|
||||
"keep": "Behold",
|
||||
"allEpisodes": "Alle episoder",
|
||||
"latestEpisodes": "${count} seneste episoder",
|
||||
"latestEpisode": "Seneste episode",
|
||||
"episodesAddedPastDays": "Episoder tilføjet de seneste ${count} dage",
|
||||
"deleteAfterPlaying": "Slet episoder efter afspilning",
|
||||
"never": "Aldrig",
|
||||
"afterADay": "Efter en dag",
|
||||
"afterAWeek": "Efter en uge",
|
||||
"afterAMonth": "Efter en måned",
|
||||
"onNextRefresh": "Ved næste opdatering",
|
||||
"seasons": "Sæsoner",
|
||||
"show": "Vis",
|
||||
"hide": "Skjul",
|
||||
"episodeOrdering": "Episoderækkefølge",
|
||||
"tmdbAiring": "The Movie Database (Sendt)",
|
||||
"tvdbAiring": "TheTVDB (Sendt)",
|
||||
"tvdbAbsolute": "TheTVDB (Absolut)",
|
||||
"metadataLanguage": "Metadatasprog",
|
||||
"useOriginalTitle": "Brug originaltitel",
|
||||
"preferredAudioLanguage": "Foretrukket lydsprog",
|
||||
"preferredSubtitleLanguage": "Foretrukket undertekstsprog",
|
||||
"subtitleMode": "Auto-vælg underteksttilstand",
|
||||
"manuallySelected": "Manuelt valgt",
|
||||
"shownWithForeignAudio": "Vist med fremmedsproget lyd",
|
||||
"alwaysEnabled": "Altid aktiveret",
|
||||
"tags": "Tags",
|
||||
"addTag": "Tilføj tag",
|
||||
"genre": "Genre",
|
||||
"director": "Instruktør",
|
||||
"writer": "Forfatter",
|
||||
"producer": "Producer",
|
||||
"country": "Land",
|
||||
"collection": "Samling",
|
||||
"label": "Etiket",
|
||||
"style": "Stil",
|
||||
"mood": "Stemning"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Serveropgaver",
|
||||
"failedToLoad": "Kunne ikke indlæse opgaver",
|
||||
"noTasks": "Ingen opgaver kører"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Forbundet",
|
||||
"connectedAs": "Forbundet som @${username}",
|
||||
"disconnectConfirm": "Frakobl Trakt-konto?",
|
||||
"disconnectConfirmBody": "Plezy stopper med at sende afspilningsbegivenheder til Trakt. Du kan genoprette forbindelse når som helst.",
|
||||
"scrobble": "Realtids-scrobbling",
|
||||
"scrobbleDescription": "Send afspil-, pause- og stop-begivenheder til Trakt under afspilning.",
|
||||
"watchedSync": "Synkroniser sét-status",
|
||||
"watchedSyncDescription": "Når du markerer ting som sét i Plezy, markeres de også på Trakt.",
|
||||
"deviceCodeTitle": "Aktivér Plezy på Trakt",
|
||||
"deviceCodeBody": "Besøg ${url} og indtast denne kode:",
|
||||
"openTraktActivate": "Åbn Trakt for at aktivere",
|
||||
"waitingForAuthorization": "Venter på godkendelse…",
|
||||
"codeCopied": "Kode kopieret",
|
||||
"connectFailed": "Kunne ikke forbinde til Trakt. Prøv igen."
|
||||
}
|
||||
}
|
||||
+249
-166
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "Mit Plex anmelden",
|
||||
"showQRCode": "QR-Code anzeigen",
|
||||
"authenticate": "Authentifizieren",
|
||||
"debugEnterToken": "Debug: Plex-Token eingeben",
|
||||
"plexTokenLabel": "Plex-Auth-Token",
|
||||
"plexTokenHint": "Plex.tv-Token eingeben",
|
||||
"authenticationTimeout": "Authentifizierung abgelaufen. Bitte erneut versuchen.",
|
||||
"scanQRToSignIn": "QR-Code scannen zum Anmelden",
|
||||
"waitingForAuth": "Warte auf Authentifizierung...\nBitte Anmeldung im Browser abschließen.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "Löschen",
|
||||
"shuffle": "Zufall",
|
||||
"addTo": "Hinzufügen zu...",
|
||||
"remove": "Entfernen",
|
||||
"paste": "Einfügen",
|
||||
"createNew": "Neu erstellen",
|
||||
"connect": "Verbinden",
|
||||
"disconnect": "Trennen",
|
||||
"play": "Abspielen",
|
||||
@@ -41,12 +37,11 @@
|
||||
"resume": "Fortsetzen",
|
||||
"error": "Fehler",
|
||||
"search": "Suche",
|
||||
"home": "Startseite",
|
||||
"home": "Start",
|
||||
"back": "Zurück",
|
||||
"settings": "Einstellungen",
|
||||
"settings": "Optionen",
|
||||
"mute": "Stumm",
|
||||
"ok": "OK",
|
||||
"loading": "Laden...",
|
||||
"reconnect": "Erneut verbinden",
|
||||
"exitConfirmTitle": "App beenden?",
|
||||
"exitConfirmMessage": "Möchtest du die App wirklich beenden?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "Server werden aktualisiert...",
|
||||
"loadingServers": "Server werden geladen...",
|
||||
"connectingToServers": "Verbindung zu Servern...",
|
||||
"startingOfflineMode": "Offlinemodus wird gestartet..."
|
||||
"startingOfflineMode": "Offlinemodus wird gestartet...",
|
||||
"loading": "Laden..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Lizenzen",
|
||||
"switchProfile": "Profil wechseln",
|
||||
"subtitleStyling": "Untertitel-Stil",
|
||||
"mpvConfig": "MPV-Konfiguration",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Protokolle"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"supportDeveloper": "Plezy unterstützen",
|
||||
"supportDeveloperDescription": "Per Liberapay spenden, um die Entwicklung zu fördern",
|
||||
"language": "Sprache",
|
||||
"theme": "Design",
|
||||
"appearance": "Darstellung",
|
||||
"videoPlayback": "Videowiedergabe",
|
||||
"videoPlaybackDescription": "Wiedergabeverhalten konfigurieren",
|
||||
"advanced": "Erweitert",
|
||||
"episodePosterMode": "Episoden-Poster-Stil",
|
||||
"seriesPoster": "Serienposter",
|
||||
"seriesPosterDescription": "Zeige das Serienposter für alle Episoden",
|
||||
"seasonPoster": "Staffelposter",
|
||||
"seasonPosterDescription": "Zeige das staffelspezifische Poster für Episoden",
|
||||
"episodeThumbnail": "Episoden-Miniatur",
|
||||
"episodeThumbnailDescription": "Zeige 16:9 Episoden-Vorschaubilder",
|
||||
"episodeThumbnail": "Miniatur",
|
||||
"showHeroSectionDescription": "Bereich mit empfohlenen Inhalten auf der Startseite anzeigen",
|
||||
"secondsLabel": "Sekunden",
|
||||
"minutesLabel": "Minuten",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Dauer eingeben (${min}-${max})",
|
||||
"systemTheme": "System",
|
||||
"systemThemeDescription": "Systemeinstellungen folgen",
|
||||
"lightTheme": "Hell",
|
||||
"darkTheme": "Dunkel",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "Reines Schwarz für OLED-Bildschirme",
|
||||
"libraryDensity": "Mediathekdichte",
|
||||
"compact": "Kompakt",
|
||||
"compactDescription": "Kleinere Karten, mehr Elemente sichtbar",
|
||||
"normal": "Normal",
|
||||
"normalDescription": "Standardgröße",
|
||||
"comfortable": "Großzügig",
|
||||
"comfortableDescription": "Größere Karten, weniger Elemente sichtbar",
|
||||
"viewMode": "Ansichtsmodus",
|
||||
"gridView": "Raster",
|
||||
"gridViewDescription": "Elemente im Raster anzeigen",
|
||||
"listView": "Liste",
|
||||
"listViewDescription": "Elemente in Listenansicht anzeigen",
|
||||
"showHeroSection": "Hero-Bereich anzeigen",
|
||||
"useGlobalHubs": "Plex-Startseiten-Layout verwenden",
|
||||
"useGlobalHubsDescription": "Zeigt Startseiten-Hubs wie der offizielle Plex-Client. Wenn deaktiviert, werden stattdessen Empfehlungen pro Bibliothek angezeigt.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "Vorschaubilder unscharf machen und Beschreibungen für noch nicht gesehene Episoden ausblenden",
|
||||
"playerBackend": "Player-Backend",
|
||||
"exoPlayer": "ExoPlayer (Empfohlen)",
|
||||
"exoPlayerDescription": "Android-nativer Player mit besserer Hardware-Unterstützung",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "Erweiterter Player mit mehr Funktionen und ASS-Untertitel-Unterstützung",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Hardware-Decodierung",
|
||||
"hardwareDecodingDescription": "Hardwarebeschleunigung verwenden, sofern verfügbar",
|
||||
"bufferSize": "Puffergröße",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "Aussehen von Untertiteln anpassen",
|
||||
"smallSkipDuration": "Kleine Sprungdauer",
|
||||
"largeSkipDuration": "Große Sprungdauer",
|
||||
"rewindOnResume": "Zurückspulen bei Fortsetzung",
|
||||
"secondsUnit": "${seconds} Sekunden",
|
||||
"defaultSleepTimer": "Standard-Sleep-Timer",
|
||||
"minutesUnit": "${minutes} Minuten",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "Tastenkürzel anpassen",
|
||||
"videoPlayerNavigation": "Videoplayer-Navigation",
|
||||
"videoPlayerNavigationDescription": "Pfeiltasten zur Navigation der Videoplayer-Steuerung verwenden",
|
||||
"watchTogetherRelay": "Gemeinsam Schauen Relay",
|
||||
"watchTogetherRelayDescription": "Einen benutzerdefinierten Relay-Server für Gemeinsam Schauen festlegen. Alle Teilnehmer müssen denselben Server verwenden.",
|
||||
"watchTogetherRelayHint": "https://mein-relay.beispiel.de",
|
||||
"crashReporting": "Absturzberichte",
|
||||
"crashReportingDescription": "Absturzberichte senden, um die App zu verbessern",
|
||||
"debugLogging": "Debug-Protokollierung",
|
||||
"debugLoggingDescription": "Detaillierte Protokolle zur Fehleranalyse aktivieren",
|
||||
"viewLogs": "Protokolle anzeigen",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "Abspann automatisch überspringen und nächste Episode abspielen",
|
||||
"autoSkipDelay": "Verzögerung für automatisches Überspringen",
|
||||
"autoSkipDelayDescription": "${seconds} Sekunden vor dem automatischen Überspringen warten",
|
||||
"introPattern": "Intro-Markierungsmuster",
|
||||
"introPatternDescription": "Regulärer Ausdruck zum Erkennen von Intro-Markierungen in Kapiteltiteln",
|
||||
"creditsPattern": "Abspann-Markierungsmuster",
|
||||
"creditsPatternDescription": "Regulärer Ausdruck zum Erkennen von Abspann-Markierungen in Kapiteltiteln",
|
||||
"invalidRegex": "Ungültiger regulärer Ausdruck",
|
||||
"downloads": "Downloads",
|
||||
"downloadLocationDescription": "Speicherort für heruntergeladene Inhalte wählen",
|
||||
"downloadLocationDefault": "Standard (App-Speicher)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "Ordnerauswahl fehlgeschlagen",
|
||||
"downloadOnWifiOnly": "Nur über WLAN herunterladen",
|
||||
"downloadOnWifiOnlyDescription": "Downloads über mobile Daten verhindern",
|
||||
"autoRemoveWatchedDownloads": "Gesehene Downloads automatisch entfernen",
|
||||
"autoRemoveWatchedDownloadsDescription": "Heruntergeladene Episoden und Filme automatisch löschen, wenn sie als gesehen markiert werden",
|
||||
"cellularDownloadBlocked": "Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung.",
|
||||
"maxVolume": "Maximale Lautstärke",
|
||||
"maxVolumeDescription": "Lautstärke über 100% für leise Medien erlauben",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Zeige auf Discord, was du gerade schaust",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Wiedergabeverlauf mit Trakt synchronisieren",
|
||||
"companionRemoteServer": "Companion Remote Server",
|
||||
"companionRemoteServerDescription": "Mobilgeräten in deinem Netzwerk erlauben, diese App zu steuern",
|
||||
"autoPip": "Automatisches Bild-in-Bild",
|
||||
"autoPipDescription": "Automatisch Bild-in-Bild aktivieren, wenn die App während der Wiedergabe verlassen wird",
|
||||
"matchContentFrameRate": "Inhalts-Bildrate anpassen",
|
||||
"matchContentFrameRateDescription": "Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "Bildwiederholrate anpassen",
|
||||
"matchRefreshRateDescription": "Bildwiederholrate des Displays an den Videoinhalt im Vollbildmodus anpassen",
|
||||
"matchDynamicRange": "Dynamikumfang anpassen",
|
||||
"matchDynamicRangeDescription": "HDR für HDR-Inhalte automatisch aktivieren und beim Verlassen des Players auf SDR zurücksetzen",
|
||||
"displaySwitchDelay": "Verzögerung beim Displaywechsel",
|
||||
"tunneledPlayback": "Tunnelwiedergabe",
|
||||
"tunneledPlaybackDescription": "Hardware-beschleunigte Video-Tunnelierung verwenden. Deaktivieren, wenn bei HDR-Inhalten ein schwarzer Bildschirm mit Ton erscheint",
|
||||
"requireProfileSelectionOnOpen": "Profil beim Öffnen abfragen",
|
||||
"requireProfileSelectionOnOpenDescription": "Profilauswahl bei jedem Öffnen der App anzeigen",
|
||||
"confirmExitOnBack": "Vor dem Beenden bestätigen",
|
||||
"confirmExitOnBackDescription": "Bestätigungsdialog anzeigen, wenn Zurück zum Beenden der App gedrückt wird"
|
||||
"confirmExitOnBackDescription": "Bestätigungsdialog anzeigen, wenn Zurück zum Beenden der App gedrückt wird",
|
||||
"forceTvMode": "TV-Modus erzwingen",
|
||||
"forceTvModeDescription": "TV-Layout unabhängig von der automatischen Erkennung verwenden. Nützlich für Android-TV-Geräte, die das Leanback-Feature nicht melden. Startet die App bei Änderung neu.",
|
||||
"autoHidePerformanceOverlay": "Leistungsoverlay automatisch ausblenden",
|
||||
"autoHidePerformanceOverlayDescription": "Leistungsoverlay mit den Wiedergabesteuerungen ein-/ausblenden",
|
||||
"showNavBarLabels": "Navigationsleisten-Beschriftungen anzeigen",
|
||||
"showNavBarLabelsDescription": "Textbeschriftungen unter den Symbolen der Navigationsleiste anzeigen",
|
||||
"liveTvDefaultFavorites": "Standardmäßig Favoritenkanäle",
|
||||
"liveTvDefaultFavoritesDescription": "Beim Öffnen von Live TV nur Favoritenkanäle anzeigen",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Filme, Serien, Musik suchen...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "Intro/Abspann überspringen"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "PIN eingeben",
|
||||
"showPin": "PIN anzeigen",
|
||||
"hidePin": "PIN verbergen"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Dateiinfo",
|
||||
"video": "Video",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "Primärfarben",
|
||||
"chromaSubsampling": "Chroma-Subsampling",
|
||||
"channels": "Kanäle",
|
||||
"subtitles": "Untertitel",
|
||||
"overallBitrate": "Gesamt-Bitrate",
|
||||
"path": "Pfad",
|
||||
"size": "Größe",
|
||||
"container": "Container",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "Dies umfasst alle Episoden und deren Dateien.",
|
||||
"mediaDeletedSuccessfully": "Medienelement erfolgreich gelöscht",
|
||||
"mediaFailedToDelete": "Löschen des Medienelements fehlgeschlagen",
|
||||
"rate": "Bewerten"
|
||||
"rate": "Bewerten",
|
||||
"playFromBeginning": "Von Anfang an abspielen",
|
||||
"playVersion": "Version abspielen..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, Film",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "Rotation entsperren",
|
||||
"timerActive": "Schlaftimer aktiv",
|
||||
"playbackWillPauseIn": "Wiedergabe wird in ${duration} pausiert",
|
||||
"sleepTimerCompleted": "Schlaftimer abgelaufen – Wiedergabe pausiert",
|
||||
"stillWatching": "Schaust du noch?",
|
||||
"pausingIn": "Pause in ${seconds}s",
|
||||
"continueWatching": "Weiter",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "Stumm schalten",
|
||||
"unmuteButton": "Stummschaltung aufheben",
|
||||
"settingsButton": "Videoeinstellungen",
|
||||
"audioTrackButton": "Tonspuren",
|
||||
"subtitlesButton": "Untertitel",
|
||||
"tracksButton": "Audio und Untertitel",
|
||||
"chaptersButton": "Kapitel",
|
||||
"versionsButton": "Videoversionen",
|
||||
"pipButton": "Bild-in-Bild Modus",
|
||||
"aspectRatioButton": "Seitenverhältnis",
|
||||
"ambientLighting": "Umgebungsbeleuchtung",
|
||||
"ambientLightingOn": "Umgebungsbeleuchtung aktivieren",
|
||||
"ambientLightingOff": "Umgebungsbeleuchtung deaktivieren",
|
||||
"fullscreenButton": "Vollbild aktivieren",
|
||||
"exitFullscreenButton": "Vollbild verlassen",
|
||||
"alwaysOnTopButton": "Immer im Vordergrund",
|
||||
"rotationLockButton": "Drehsperre",
|
||||
"lockScreen": "Bildschirm sperren",
|
||||
"screenLockButton": "Bildschirmsperre",
|
||||
"longPressToUnlock": "Lange drücken zum Entsperren",
|
||||
"timelineSlider": "Video-Zeitleiste",
|
||||
"volumeSlider": "Lautstärkepegel",
|
||||
"endsAt": "Endet um ${time}",
|
||||
"pipActive": "Wiedergabe im Bild-in-Bild-Modus",
|
||||
"pipFailed": "Bild-in-Bild konnte nicht gestartet werden",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Erfordert Android 8.0 oder neuer",
|
||||
"iosVersion": "Erfordert iOS 15.0 oder neuer",
|
||||
"permissionDisabled": "Bild-in-Bild-Berechtigung ist deaktiviert. Aktiviere sie unter Einstellungen > Apps > Plezy > Bild-in-Bild",
|
||||
"notSupported": "Dieses Gerät unterstützt den Bild-in-Bild-Modus nicht",
|
||||
"voSwitchFailed": "Videoausgabe für Bild-in-Bild konnte nicht umgeschaltet werden",
|
||||
"failed": "Bild-in-Bild konnte nicht gestartet werden",
|
||||
"unknown": "Ein Fehler ist aufgetreten: ${error}"
|
||||
},
|
||||
"chapters": "Kapitel",
|
||||
"noChaptersAvailable": "Keine Kapitel verfügbar",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "Warteschlange",
|
||||
"noQueueItems": "Keine Elemente in der Warteschlange",
|
||||
"searchSubtitles": "Untertitel suchen",
|
||||
"language": "Sprache",
|
||||
"noSubtitlesFound": "Keine Untertitel gefunden",
|
||||
"subtitleDownloaded": "Untertitel heruntergeladen",
|
||||
"subtitleDownloadFailed": "Untertitel konnte nicht heruntergeladen werden",
|
||||
"searchLanguages": "Sprachen suchen..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Eigentümer",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "Als ungesehen markiert",
|
||||
"markedAsWatchedOffline": "Als gesehen markiert (wird synchronisiert, wenn online)",
|
||||
"markedAsUnwatchedOffline": "Als ungesehen markiert (wird synchronisiert, wenn online)",
|
||||
"autoRemovedWatchedDownload": "Automatisch entfernt: ${title}",
|
||||
"removedFromContinueWatching": "Aus ‚Weiterschauen' entfernt",
|
||||
"errorLoading": "Fehler: ${error}",
|
||||
"fileInfoNotAvailable": "Dateiinfo nicht verfügbar",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "Wiedergabewarteschlange konnte nicht erstellt werden – keine Elemente",
|
||||
"failedPlayback": "Wiedergabe für ${action} fehlgeschlagen: ${error}",
|
||||
"switchingToCompatiblePlayer": "Wechsle zu kompatiblem Player...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "Wiedergabe fehlgeschlagen",
|
||||
"serverLimitBody": "Der Server hat einen Fehler zurückgegeben (HTTP 500). Das bedeutet meist, dass der Serverbesitzer ein Bandbreiten- oder Transcoding-Limit festgelegt hat, das deine Sitzung ablehnt. Vom Client aus lässt sich nichts machen — der Serverbesitzer muss seine Einstellungen anpassen.",
|
||||
"logsUploaded": "Protokolle hochgeladen",
|
||||
"logsUploadFailed": "Protokolle konnten nicht hochgeladen werden",
|
||||
"logId": "Protokoll-ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "Stiloptionen",
|
||||
"text": "Text",
|
||||
"border": "Rahmen",
|
||||
"background": "Hintergrund",
|
||||
"fontSize": "Schriftgröße",
|
||||
"textColor": "Textfarbe",
|
||||
"borderSize": "Rahmengröße",
|
||||
"borderColor": "Rahmenfarbe",
|
||||
"backgroundOpacity": "Hintergrunddeckkraft",
|
||||
"backgroundColor": "Hintergrundfarbe",
|
||||
"position": "Position"
|
||||
"position": "Position",
|
||||
"assOverride": "ASS-Überschreibung",
|
||||
"bold": "Fett",
|
||||
"italic": "Kursiv"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV-Konfiguration",
|
||||
"title": "mpv-Konfiguration",
|
||||
"description": "Erweiterte Videoplayer-Einstellungen",
|
||||
"properties": "Eigenschaften",
|
||||
"presets": "Voreinstellungen",
|
||||
"noProperties": "Keine Eigenschaften konfiguriert",
|
||||
"noPresets": "Keine gespeicherten Voreinstellungen",
|
||||
"addProperty": "Eigenschaft hinzufügen",
|
||||
"editProperty": "Eigenschaft bearbeiten",
|
||||
"deleteProperty": "Eigenschaft löschen",
|
||||
"propertyKey": "Eigenschaftsschlüssel",
|
||||
"propertyKeyHint": "z.B. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Eigenschaftswert",
|
||||
"propertyValueHint": "z.B. auto, 256000000",
|
||||
"saveAsPreset": "Als Voreinstellung speichern...",
|
||||
"presetName": "Name der Voreinstellung",
|
||||
"presetNameHint": "Namen für diese Voreinstellung eingeben",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "Voreinstellung geladen",
|
||||
"presetDeleted": "Voreinstellung gelöscht",
|
||||
"confirmDeletePreset": "Möchten Sie diese Voreinstellung wirklich löschen?",
|
||||
"confirmDeleteProperty": "Möchten Sie diese Eigenschaft wirklich löschen?",
|
||||
"entriesCount": "${count} Einträge"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Aktion bestätigen"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "Übersicht",
|
||||
"cast": "Besetzung",
|
||||
"extras": "Trailer & Extras",
|
||||
"seasons": "Staffeln",
|
||||
"studio": "Studio",
|
||||
"rating": "Altersfreigabe",
|
||||
"episodeCount": "${count} Episoden",
|
||||
"watchedProgress": "${watched} von ${total} gesehen",
|
||||
"movie": "Film",
|
||||
"tvShow": "Serie",
|
||||
"minutesLeft": "${minutes} Min übrig"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "Wiedergabelisten"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Gruppierung",
|
||||
"all": "Alle",
|
||||
"movies": "Filme",
|
||||
"shows": "Serien",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "Protokolle löschen",
|
||||
"copyLogs": "Protokolle kopieren",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "Fehler:",
|
||||
"stackTrace": "Stacktrace:"
|
||||
"uploadLogs": "Protokolle hochladen"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Verwandte Pakete",
|
||||
@@ -559,26 +584,18 @@
|
||||
"licensesCount": "${count} Lizenzen"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Mediatheken",
|
||||
"libraries": "Medien",
|
||||
"downloads": "Downloads",
|
||||
"liveTv": "Live-TV"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "Live-TV",
|
||||
"channels": "Kanäle",
|
||||
"guide": "Programmführer",
|
||||
"noChannels": "Keine Kanäle verfügbar",
|
||||
"noDvr": "Kein DVR auf einem Server konfiguriert",
|
||||
"tuneFailed": "Kanal konnte nicht eingestellt werden",
|
||||
"loading": "Kanäle werden geladen...",
|
||||
"nowPlaying": "Läuft gerade",
|
||||
"noPrograms": "Keine Programmdaten verfügbar",
|
||||
"channelNumber": "Kanal ${number}",
|
||||
"live": "LIVE",
|
||||
"hd": "HD",
|
||||
"premiere": "NEU",
|
||||
"reloadGuide": "Programmführer neu laden",
|
||||
"allChannels": "Alle Kanäle",
|
||||
"now": "Jetzt",
|
||||
"today": "Heute",
|
||||
"midnight": "Mitternacht",
|
||||
@@ -588,30 +605,41 @@
|
||||
"evening": "Abend",
|
||||
"lateNight": "Spätnacht",
|
||||
"whatsOn": "Jetzt im TV",
|
||||
"watchChannel": "Kanal ansehen"
|
||||
"watchChannel": "Kanal ansehen",
|
||||
"favorites": "Favoriten",
|
||||
"reorderFavorites": "Favoriten sortieren",
|
||||
"joinSession": "Laufender Sitzung beitreten",
|
||||
"watchFromStart": "Von Anfang an ansehen (vor ${minutes} Min.)",
|
||||
"watchLive": "Live ansehen",
|
||||
"goToLive": "Zum Live-Bild"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"manage": "Verwalten",
|
||||
"tvShows": "Serien",
|
||||
"movies": "Filme",
|
||||
"noDownloads": "Noch keine Downloads",
|
||||
"noDownloadsDescription": "Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt",
|
||||
"downloadNow": "Herunterladen",
|
||||
"deleteDownload": "Download löschen",
|
||||
"retryDownload": "Download wiederholen",
|
||||
"downloadQueued": "Download in Warteschlange",
|
||||
"episodesQueued": "${count} Episoden zum Download hinzugefügt",
|
||||
"downloadDeleted": "Download gelöscht",
|
||||
"deleteConfirm": "Möchtest du \"${title}\" wirklich löschen? Die heruntergeladene Datei wird von deinem Gerät entfernt.",
|
||||
"deletingWithProgress": "Lösche ${title}... (${current} von ${total})",
|
||||
"noDownloadsTree": "Keine Downloads",
|
||||
"pauseAll": "Alle pausieren",
|
||||
"resumeAll": "Alle fortsetzen",
|
||||
"deleteAll": "Alle löschen"
|
||||
"collections": {
|
||||
"title": "Sammlungen",
|
||||
"collection": "Sammlung",
|
||||
"empty": "Sammlung ist leer",
|
||||
"unknownLibrarySection": "Löschen nicht möglich: Unbekannte Bibliothekssektion",
|
||||
"deleteCollection": "Sammlung löschen",
|
||||
"deleteConfirm": "Sind Sie sicher, dass Sie \"${title}\" löschen möchten? Dies kann nicht rückgängig gemacht werden.",
|
||||
"deleted": "Sammlung gelöscht",
|
||||
"deleteFailed": "Sammlung konnte nicht gelöscht werden",
|
||||
"deleteFailedWithError": "Sammlung konnte nicht gelöscht werden: ${error}",
|
||||
"failedToLoadItems": "Sammlungselemente konnten nicht geladen werden: ${error}",
|
||||
"selectCollection": "Sammlung auswählen",
|
||||
"collectionName": "Sammlungsname",
|
||||
"enterCollectionName": "Sammlungsnamen eingeben",
|
||||
"addedToCollection": "Zur Sammlung hinzugefügt",
|
||||
"errorAddingToCollection": "Fehler beim Hinzufügen zur Sammlung",
|
||||
"created": "Sammlung erstellt",
|
||||
"removeFromCollection": "Aus Sammlung entfernen",
|
||||
"removeFromCollectionConfirm": "\"${title}\" aus dieser Sammlung entfernen?",
|
||||
"removedFromCollection": "Aus Sammlung entfernt",
|
||||
"removeFromCollectionFailed": "Entfernen aus Sammlung fehlgeschlagen",
|
||||
"removeFromCollectionError": "Fehler beim Entfernen aus der Sammlung: ${error}",
|
||||
"searchCollections": "Sammlungen durchsuchen..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Wiedergabelisten",
|
||||
"playlist": "Wiedergabeliste",
|
||||
"noPlaylists": "Keine Wiedergabelisten gefunden",
|
||||
"create": "Wiedergabeliste erstellen",
|
||||
"playlistName": "Name der Wiedergabeliste",
|
||||
@@ -629,38 +657,12 @@
|
||||
"itemAdded": "Zur Wiedergabeliste hinzugefügt",
|
||||
"itemRemoved": "Aus Wiedergabeliste entfernt",
|
||||
"selectPlaylist": "Wiedergabeliste auswählen",
|
||||
"createNewPlaylist": "Neue Wiedergabeliste erstellen",
|
||||
"errorCreating": "Wiedergabeliste konnte nicht erstellt werden",
|
||||
"errorDeleting": "Wiedergabeliste konnte nicht gelöscht werden",
|
||||
"errorLoading": "Wiedergabelisten konnten nicht geladen werden",
|
||||
"errorAdding": "Konnte nicht zur Wiedergabeliste hinzugefügt werden",
|
||||
"errorReordering": "Element der Wiedergabeliste konnte nicht neu geordnet werden",
|
||||
"errorRemoving": "Konnte nicht aus der Wiedergabeliste entfernt werden",
|
||||
"playlist": "Wiedergabeliste"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Sammlungen",
|
||||
"collection": "Sammlung",
|
||||
"empty": "Sammlung ist leer",
|
||||
"unknownLibrarySection": "Löschen nicht möglich: Unbekannte Bibliothekssektion",
|
||||
"deleteCollection": "Sammlung löschen",
|
||||
"deleteConfirm": "Sind Sie sicher, dass Sie \"${title}\" löschen möchten? Dies kann nicht rückgängig gemacht werden.",
|
||||
"deleted": "Sammlung gelöscht",
|
||||
"deleteFailed": "Sammlung konnte nicht gelöscht werden",
|
||||
"deleteFailedWithError": "Sammlung konnte nicht gelöscht werden: ${error}",
|
||||
"failedToLoadItems": "Sammlungselemente konnten nicht geladen werden: ${error}",
|
||||
"selectCollection": "Sammlung auswählen",
|
||||
"createNewCollection": "Neue Sammlung erstellen",
|
||||
"collectionName": "Sammlungsname",
|
||||
"enterCollectionName": "Sammlungsnamen eingeben",
|
||||
"addedToCollection": "Zur Sammlung hinzugefügt",
|
||||
"errorAddingToCollection": "Fehler beim Hinzufügen zur Sammlung",
|
||||
"created": "Sammlung erstellt",
|
||||
"removeFromCollection": "Aus Sammlung entfernen",
|
||||
"removeFromCollectionConfirm": "\"${title}\" aus dieser Sammlung entfernen?",
|
||||
"removedFromCollection": "Aus Sammlung entfernt",
|
||||
"removeFromCollectionFailed": "Entfernen aus Sammlung fehlgeschlagen",
|
||||
"removeFromCollectionError": "Fehler beim Entfernen aus der Sammlung: ${error}"
|
||||
"errorRemoving": "Konnte nicht aus der Wiedergabeliste entfernt werden"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Gemeinsam Schauen",
|
||||
@@ -697,18 +699,76 @@
|
||||
"leave": "Verlassen",
|
||||
"syncing": "Synchronisieren...",
|
||||
"joinWatchSession": "Schausitzung Beitreten",
|
||||
"enterCodeHint": "8-stelligen Code eingeben",
|
||||
"enterCodeHint": "5-stelligen Code eingeben",
|
||||
"pasteFromClipboard": "Aus Zwischenablage einfügen",
|
||||
"pleaseEnterCode": "Bitte gib einen Sitzungscode ein",
|
||||
"codeMustBe8Chars": "Sitzungscode muss 8 Zeichen haben",
|
||||
"codeMustBe5Chars": "Sitzungscode muss 5 Zeichen haben",
|
||||
"joinInstructions": "Gib den vom Host geteilten Sitzungscode ein, um seiner Schausitzung beizutreten.",
|
||||
"failedToCreate": "Sitzung konnte nicht erstellt werden",
|
||||
"failedToJoin": "Sitzung konnte nicht beigetreten werden",
|
||||
"sessionCodeCopied": "Sitzungscode in Zwischenablage kopiert",
|
||||
"relayUnreachable": "Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht.",
|
||||
"reconnectingToHost": "Verbindung zum Host wird wiederhergestellt...",
|
||||
"currentPlayback": "Aktuelle Wiedergabe",
|
||||
"joinCurrentPlayback": "Aktuelle Wiedergabe beitreten",
|
||||
"joinCurrentPlaybackDescription": "Springe zurück zu dem, was der Host gerade ansieht",
|
||||
"failedToOpenCurrentPlayback": "Aktuelle Wiedergabe konnte nicht geöffnet werden",
|
||||
"participantJoined": "${name} ist beigetreten",
|
||||
"participantLeft": "${name} hat die Sitzung verlassen"
|
||||
"participantLeft": "${name} hat die Sitzung verlassen",
|
||||
"participantPaused": "${name} hat pausiert",
|
||||
"participantResumed": "${name} hat fortgesetzt",
|
||||
"participantSeeked": "${name} hat gespult",
|
||||
"participantBuffering": "${name} puffert",
|
||||
"waitingForParticipants": "Warte auf andere zum Laden...",
|
||||
"recentRooms": "Letzte Räume",
|
||||
"renameRoom": "Raum umbenennen",
|
||||
"removeRoom": "Entfernen"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"manage": "Verwalten",
|
||||
"tvShows": "Serien",
|
||||
"movies": "Filme",
|
||||
"noDownloads": "Noch keine Downloads",
|
||||
"noDownloadsDescription": "Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt",
|
||||
"downloadNow": "Herunterladen",
|
||||
"deleteDownload": "Download löschen",
|
||||
"retryDownload": "Download wiederholen",
|
||||
"downloadQueued": "Download in Warteschlange",
|
||||
"serverErrorBitrate": "Serverfehler — die Datei überschreitet möglicherweise das Remote-Streaming-Bitratenlimit",
|
||||
"episodesQueued": "${count} Episoden zum Download hinzugefügt",
|
||||
"downloadDeleted": "Download gelöscht",
|
||||
"deleteConfirm": "Möchtest du \"${title}\" wirklich löschen? Die heruntergeladene Datei wird von deinem Gerät entfernt.",
|
||||
"deletingWithProgress": "Lösche ${title}... (${current} von ${total})",
|
||||
"noDownloadsTree": "Keine Downloads",
|
||||
"pauseAll": "Alle pausieren",
|
||||
"resumeAll": "Alle fortsetzen",
|
||||
"deleteAll": "Alle löschen",
|
||||
"selectVersion": "Version auswählen",
|
||||
"allEpisodes": "Alle Episoden",
|
||||
"unwatchedOnly": "Nur ungesehene",
|
||||
"nextNUnwatched": "Nächste ${count} ungesehene",
|
||||
"customAmount": "Eigene Anzahl...",
|
||||
"howManyEpisodes": "Wie viele Episoden?",
|
||||
"itemsQueued": "${count} Elemente zum Download eingereiht",
|
||||
"keepSynced": "Synchronisiert halten",
|
||||
"downloadOnce": "Einmal herunterladen",
|
||||
"keepNUnwatched": "${count} ungesehene behalten",
|
||||
"editSyncRule": "Sync-Regel bearbeiten",
|
||||
"removeSyncRule": "Sync-Regel entfernen",
|
||||
"removeSyncRuleConfirm": "Synchronisierung von \"${title}\" beenden? Heruntergeladene Episoden werden behalten.",
|
||||
"syncRuleCreated": "Sync-Regel erstellt — ${count} ungesehene Episoden werden behalten",
|
||||
"syncRuleUpdated": "Sync-Regel aktualisiert",
|
||||
"syncRuleRemoved": "Sync-Regel entfernt",
|
||||
"syncedNewEpisodes": "${count} neue Episoden für ${title} synchronisiert",
|
||||
"activeSyncRules": "Sync-Regeln",
|
||||
"noSyncRules": "Keine Sync-Regeln",
|
||||
"manageSyncRule": "Synchronisierung verwalten",
|
||||
"editEpisodeCount": "Episodenanzahl",
|
||||
"editSyncFilter": "Synchronisierungsfilter",
|
||||
"syncAllItems": "Alle Einträge synchronisieren",
|
||||
"syncUnwatchedItems": "Ungesehene Einträge synchronisieren",
|
||||
"syncRuleListCreated": "Sync-Regel erstellt"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shader",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "NVIDIA-Bildskalierung für schärferes Video",
|
||||
"qualityFast": "Schnell",
|
||||
"qualityHQ": "Hohe Qualität",
|
||||
"mode": "Modus"
|
||||
"mode": "Modus",
|
||||
"importShader": "Shader importieren",
|
||||
"customShaderDescription": "Benutzerdefinierter GLSL-Shader",
|
||||
"shaderImported": "Shader importiert",
|
||||
"shaderImportFailed": "Shader konnte nicht importiert werden",
|
||||
"deleteShader": "Shader löschen",
|
||||
"deleteShaderConfirm": "\"${name}\" löschen?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "Mit Gerät verbinden",
|
||||
"hostRemoteSession": "Fernsteuerungssitzung starten",
|
||||
"controlThisDevice": "Dieses Gerät mit dem Handy steuern",
|
||||
"remoteControl": "Fernsteuerung",
|
||||
"controlDesktop": "Ein Desktop-Gerät steuern",
|
||||
"title": "Companion-Fernbedienung",
|
||||
"connectedTo": "Verbunden mit ${name}",
|
||||
"session": {
|
||||
"creatingSession": "Fernsteuerungssitzung wird erstellt...",
|
||||
"failedToCreate": "Fernsteuerungssitzung konnte nicht erstellt werden:",
|
||||
"noSession": "Keine Sitzung verfügbar",
|
||||
"scanQrCode": "QR-Code scannen",
|
||||
"orEnterManually": "Oder manuell eingeben",
|
||||
"startingServer": "Remote-Server wird gestartet...",
|
||||
"failedToCreate": "Remote-Server konnte nicht gestartet werden:",
|
||||
"hostAddress": "Host-Adresse",
|
||||
"sessionId": "Sitzungs-ID",
|
||||
"pin": "PIN",
|
||||
"connected": "Verbunden",
|
||||
"waitingForConnection": "Warte auf Verbindung...",
|
||||
"serverRunning": "Remote-Server aktiv",
|
||||
"serverStopped": "Remote-Server gestoppt",
|
||||
"serverRunningDescription": "Mobilgeräte in deinem Netzwerk können diese App finden und sich verbinden",
|
||||
"serverStoppedDescription": "Starte den Server, um Mobilgeräten die Verbindung zu ermöglichen",
|
||||
"usePhoneToControl": "Verwende dein Mobilgerät, um diese App zu steuern",
|
||||
"copiedToClipboard": "${label} in Zwischenablage kopiert",
|
||||
"copyToClipboard": "In Zwischenablage kopieren",
|
||||
"newSession": "Neue Sitzung",
|
||||
"startServer": "Server starten",
|
||||
"stopServer": "Server stoppen",
|
||||
"minimize": "Minimieren"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "Scannen",
|
||||
"manual": "Manuell",
|
||||
"pairWithDesktop": "Mit Desktop koppeln",
|
||||
"enterSessionDetails": "Gib die Sitzungsdetails ein, die auf deinem Desktop-Gerät angezeigt werden",
|
||||
"discoveryDescription": "Geräte in deinem Netzwerk, die Plezy mit demselben Plex-Konto nutzen, erscheinen automatisch",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "8-stellige Sitzungs-ID eingeben",
|
||||
"pinHint": "6-stellige PIN eingeben",
|
||||
"connecting": "Verbinde...",
|
||||
"tips": "Tipps",
|
||||
"tipDesktop": "Öffne Plezy auf deinem Desktop und aktiviere Companion Remote in den Einstellungen oder im Menü",
|
||||
"tipScan": "Verwende den Scannen-Tab, um dich schnell per QR-Code auf deinem Desktop zu koppeln",
|
||||
"tipWifi": "Stelle sicher, dass beide Geräte im selben WiFi-Netzwerk sind",
|
||||
"cameraPermissionRequired": "Kameraberechtigung wird zum Scannen von QR-Codes benötigt.\nBitte erteile den Kamerazugriff in den Geräteeinstellungen.",
|
||||
"cameraError": "Kamera konnte nicht gestartet werden: ${error}",
|
||||
"scanInstruction": "Richte deine Kamera auf den QR-Code auf deinem Desktop",
|
||||
"invalidQrCode": "Ungültiges QR-Code-Format",
|
||||
"connecting": "Verbinden...",
|
||||
"searchingForDevices": "Suche nach Geräten...",
|
||||
"noDevicesFound": "Keine Geräte in deinem Netzwerk gefunden",
|
||||
"noDevicesHint": "Stelle sicher, dass Plezy auf deinem Desktop geöffnet ist und beide Geräte im selben WLAN sind",
|
||||
"availableDevices": "Verfügbare Geräte",
|
||||
"manualConnection": "Manuelle Verbindung",
|
||||
"cryptoInitFailed": "Sichere Verbindung konnte nicht initialisiert werden. Stelle sicher, dass du bei einem Plex-Konto angemeldet bist.",
|
||||
"validationHostRequired": "Bitte Host-Adresse eingeben",
|
||||
"validationHostFormat": "Format muss IP:Port sein (z.B. 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "Bitte Sitzungs-ID eingeben",
|
||||
"validationSessionIdLength": "Sitzungs-ID muss 8 Zeichen lang sein",
|
||||
"validationPinRequired": "Bitte PIN eingeben",
|
||||
"validationPinLength": "PIN muss 6 Ziffern haben",
|
||||
"connectionTimedOut": "Zeitüberschreitung. Bitte Sitzungs-ID und PIN überprüfen.",
|
||||
"sessionNotFound": "Sitzung nicht gefunden. Bitte Zugangsdaten überprüfen.",
|
||||
"connectionTimedOut": "Verbindung abgelaufen. Stelle sicher, dass beide Geräte im selben Netzwerk sind.",
|
||||
"sessionNotFound": "Gerät nicht gefunden. Stelle sicher, dass Plezy auf dem Host läuft.",
|
||||
"authFailed": "Authentifizierung fehlgeschlagen. Stelle sicher, dass beide Geräte dasselbe Plex-Konto verwenden.",
|
||||
"failedToConnect": "Verbindung fehlgeschlagen: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "Verbindung wird wiederhergestellt...",
|
||||
"attemptOf": "Versuch ${current} von 5",
|
||||
"retryNow": "Jetzt wiederholen",
|
||||
"connectionError": "Verbindungsfehler",
|
||||
"notConnected": "Nicht verbunden",
|
||||
"tabRemote": "Fernbedienung",
|
||||
"tabPlay": "Wiedergabe",
|
||||
"tabMore": "Mehr",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "Externen Player verwenden",
|
||||
"useExternalPlayerDescription": "Videos in einer externen App statt im integrierten Player öffnen",
|
||||
"selectPlayer": "Player auswählen",
|
||||
"customPlayers": "Benutzerdefinierte Player",
|
||||
"systemDefault": "Systemstandard",
|
||||
"addCustomPlayer": "Benutzerdefinierten Player hinzufügen",
|
||||
"playerName": "Playername",
|
||||
"playerCommand": "Befehl",
|
||||
"playerPackage": "Paketname",
|
||||
"playerUrlScheme": "URL-Schema",
|
||||
"customPlayer": "Benutzerdefinierter Player",
|
||||
"off": "Aus",
|
||||
"launchFailed": "Externer Player konnte nicht geöffnet werden",
|
||||
"appNotInstalled": "${name} ist nicht installiert",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "Zusammenfassung",
|
||||
"poster": "Poster",
|
||||
"background": "Hintergrund",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Quadratisches Bild",
|
||||
"selectPoster": "Poster auswählen",
|
||||
"selectBackground": "Hintergrund auswählen",
|
||||
"selectLogo": "Logo auswählen",
|
||||
"selectSquareArt": "Quadratisches Bild auswählen",
|
||||
"fromUrl": "Von URL",
|
||||
"uploadFile": "Datei hochladen",
|
||||
"enterImageUrl": "Bild-URL eingeben",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "Automatische Untertitelauswahl",
|
||||
"manuallySelected": "Manuell ausgewählt",
|
||||
"shownWithForeignAudio": "Bei fremdsprachigem Audio anzeigen",
|
||||
"alwaysEnabled": "Immer aktiviert"
|
||||
"alwaysEnabled": "Immer aktiviert",
|
||||
"tags": "Tags",
|
||||
"addTag": "Tag hinzufügen",
|
||||
"genre": "Genre",
|
||||
"director": "Regisseur",
|
||||
"writer": "Autor",
|
||||
"producer": "Produzent",
|
||||
"country": "Land",
|
||||
"collection": "Sammlung",
|
||||
"label": "Label",
|
||||
"style": "Stil",
|
||||
"mood": "Stimmung"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Serveraufgaben",
|
||||
"failedToLoad": "Aufgaben konnten nicht geladen werden",
|
||||
"noTasks": "Keine laufenden Aufgaben"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Verbunden",
|
||||
"connectedAs": "Verbunden als @${username}",
|
||||
"disconnectConfirm": "Trakt-Konto trennen?",
|
||||
"disconnectConfirmBody": "Plezy sendet keine Wiedergabe-Ereignisse mehr an Trakt. Du kannst dich jederzeit erneut verbinden.",
|
||||
"scrobble": "Echtzeit-Scrobbling",
|
||||
"scrobbleDescription": "Sende Play-, Pause- und Stopp-Ereignisse während der Wiedergabe an Trakt.",
|
||||
"watchedSync": "Gesehen-Status synchronisieren",
|
||||
"watchedSyncDescription": "Wenn du Inhalte in Plezy als gesehen markierst, werden sie auch auf Trakt markiert.",
|
||||
"deviceCodeTitle": "Plezy auf Trakt aktivieren",
|
||||
"deviceCodeBody": "Öffne ${url} und gib diesen Code ein:",
|
||||
"openTraktActivate": "Trakt zum Aktivieren öffnen",
|
||||
"waitingForAuthorization": "Warte auf Autorisierung…",
|
||||
"codeCopied": "Code kopiert",
|
||||
"connectFailed": "Verbindung zu Trakt fehlgeschlagen. Versuche es erneut."
|
||||
}
|
||||
}
|
||||
|
||||
+196
-113
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "Sign in with Plex",
|
||||
"showQRCode": "Show QR Code",
|
||||
"authenticate": "Authenticate",
|
||||
"debugEnterToken": "Debug: Enter Plex Token",
|
||||
"plexTokenLabel": "Plex Auth Token",
|
||||
"plexTokenHint": "Enter your Plex.tv token",
|
||||
"authenticationTimeout": "Authentication timed out. Please try again.",
|
||||
"scanQRToSignIn": "Scan this QR code to sign in",
|
||||
"waitingForAuth": "Waiting for authentication...\nPlease complete sign-in in your browser.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "Delete",
|
||||
"shuffle": "Shuffle",
|
||||
"addTo": "Add to...",
|
||||
"remove": "Remove",
|
||||
"paste": "Paste",
|
||||
"createNew": "Create new",
|
||||
"connect": "Connect",
|
||||
"disconnect": "Disconnect",
|
||||
"play": "Play",
|
||||
@@ -46,7 +42,6 @@
|
||||
"settings": "Settings",
|
||||
"mute": "Mute",
|
||||
"ok": "OK",
|
||||
"loading": "Loading...",
|
||||
"reconnect": "Reconnect",
|
||||
"exitConfirmTitle": "Exit app?",
|
||||
"exitConfirmMessage": "Are you sure you want to exit?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "Refreshing servers...",
|
||||
"loadingServers": "Loading servers...",
|
||||
"connectingToServers": "Connecting to servers...",
|
||||
"startingOfflineMode": "Starting offline mode..."
|
||||
"startingOfflineMode": "Starting offline mode...",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licenses",
|
||||
"switchProfile": "Switch Profile",
|
||||
"subtitleStyling": "Subtitle Styling",
|
||||
"mpvConfig": "MPV Configuration",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"supportDeveloper": "Support Plezy",
|
||||
"supportDeveloperDescription": "Donate via Liberapay to fund development",
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"appearance": "Appearance",
|
||||
"videoPlayback": "Video Playback",
|
||||
"videoPlaybackDescription": "Configure playback behavior",
|
||||
"advanced": "Advanced",
|
||||
"episodePosterMode": "Episode Poster Style",
|
||||
"seriesPoster": "Series Poster",
|
||||
"seriesPosterDescription": "Show the series poster for all episodes",
|
||||
"seasonPoster": "Season Poster",
|
||||
"seasonPosterDescription": "Show the season-specific poster for episodes",
|
||||
"episodeThumbnail": "Episode Thumbnail",
|
||||
"episodeThumbnailDescription": "Show 16:9 episode screenshot thumbnails",
|
||||
"episodeThumbnail": "Thumbnail",
|
||||
"showHeroSectionDescription": "Display featured content carousel on home screen",
|
||||
"secondsLabel": "Seconds",
|
||||
"minutesLabel": "Minutes",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Enter duration (${min}-${max})",
|
||||
"systemTheme": "System",
|
||||
"systemThemeDescription": "Follow system settings",
|
||||
"lightTheme": "Light",
|
||||
"darkTheme": "Dark",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "Pure black for OLED screens",
|
||||
"libraryDensity": "Library Density",
|
||||
"compact": "Compact",
|
||||
"compactDescription": "Smaller cards, more items visible",
|
||||
"normal": "Normal",
|
||||
"normalDescription": "Default size",
|
||||
"comfortable": "Comfortable",
|
||||
"comfortableDescription": "Larger cards, fewer items visible",
|
||||
"viewMode": "View Mode",
|
||||
"gridView": "Grid",
|
||||
"gridViewDescription": "Display items in a grid layout",
|
||||
"listView": "List",
|
||||
"listViewDescription": "Display items in a list layout",
|
||||
"showHeroSection": "Show Hero Section",
|
||||
"useGlobalHubs": "Use Plex Home Layout",
|
||||
"useGlobalHubsDescription": "Show home page hubs like the official Plex client. When off, shows per-library recommendations instead.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "Blur thumbnails and hide descriptions for episodes you haven't watched yet",
|
||||
"playerBackend": "Player Backend",
|
||||
"exoPlayer": "ExoPlayer (Recommended)",
|
||||
"exoPlayerDescription": "Android native player with better hardware support",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "Advanced player with more features and ASS subtitle support",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Hardware Decoding",
|
||||
"hardwareDecodingDescription": "Use hardware acceleration when available",
|
||||
"bufferSize": "Buffer Size",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "Customize subtitle appearance",
|
||||
"smallSkipDuration": "Small Skip Duration",
|
||||
"largeSkipDuration": "Large Skip Duration",
|
||||
"rewindOnResume": "Rewind on Resume",
|
||||
"secondsUnit": "${seconds} seconds",
|
||||
"defaultSleepTimer": "Default Sleep Timer",
|
||||
"minutesUnit": "${minutes} minutes",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "Customize keyboard shortcuts",
|
||||
"videoPlayerNavigation": "Video Player Navigation",
|
||||
"videoPlayerNavigationDescription": "Use arrow keys to navigate video player controls",
|
||||
"watchTogetherRelay": "Watch Together Relay",
|
||||
"watchTogetherRelayDescription": "Set a custom relay server for Watch Together. All participants must use the same server.",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"crashReporting": "Crash Reporting",
|
||||
"crashReportingDescription": "Send crash reports to help improve the app",
|
||||
"debugLogging": "Debug Logging",
|
||||
"debugLoggingDescription": "Enable detailed logging for troubleshooting",
|
||||
"viewLogs": "View Logs",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "Automatically skip credits and play next episode",
|
||||
"autoSkipDelay": "Auto Skip Delay",
|
||||
"autoSkipDelayDescription": "Wait ${seconds} seconds before auto-skipping",
|
||||
"introPattern": "Intro Marker Pattern",
|
||||
"introPatternDescription": "Regex pattern to match intro markers in chapter titles",
|
||||
"creditsPattern": "Credits Marker Pattern",
|
||||
"creditsPatternDescription": "Regex pattern to match credits markers in chapter titles",
|
||||
"invalidRegex": "Invalid regular expression",
|
||||
"downloads": "Downloads",
|
||||
"downloadLocationDescription": "Choose where to store downloaded content",
|
||||
"downloadLocationDefault": "Default (App Storage)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "Failed to select folder",
|
||||
"downloadOnWifiOnly": "Download on WiFi only",
|
||||
"downloadOnWifiOnlyDescription": "Prevent downloads when on cellular data",
|
||||
"autoRemoveWatchedDownloads": "Auto-remove watched downloads",
|
||||
"autoRemoveWatchedDownloadsDescription": "Automatically delete downloaded episodes and movies when marked as watched",
|
||||
"cellularDownloadBlocked": "Downloads are disabled on cellular data. Connect to WiFi or change the setting.",
|
||||
"maxVolume": "Maximum Volume",
|
||||
"maxVolumeDescription": "Allow volume boost above 100% for quiet media",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Show what you're watching on Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Sync watch history with Trakt",
|
||||
"companionRemoteServer": "Companion Remote Server",
|
||||
"companionRemoteServerDescription": "Allow mobile devices on your network to control this app",
|
||||
"autoPip": "Auto Picture-in-Picture",
|
||||
"autoPipDescription": "Automatically enter picture-in-picture when leaving the app during playback",
|
||||
"matchContentFrameRate": "Match Content Frame Rate",
|
||||
"matchContentFrameRateDescription": "Adjust display refresh rate to match video content, reducing judder and saving battery",
|
||||
"matchRefreshRate": "Match Refresh Rate",
|
||||
"matchRefreshRateDescription": "Switch display refresh rate to match video content when in fullscreen",
|
||||
"matchDynamicRange": "Match Dynamic Range",
|
||||
"matchDynamicRangeDescription": "Auto-enable HDR for HDR content and revert to SDR when leaving the player",
|
||||
"displaySwitchDelay": "Display Switch Delay",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"requireProfileSelectionOnOpen": "Ask for profile on app open",
|
||||
"requireProfileSelectionOnOpenDescription": "Show profile selection every time the app is opened",
|
||||
"confirmExitOnBack": "Confirm before exiting",
|
||||
"confirmExitOnBackDescription": "Show a confirmation dialog when pressing back to exit the app"
|
||||
"confirmExitOnBackDescription": "Show a confirmation dialog when pressing back to exit the app",
|
||||
"forceTvMode": "Force TV mode",
|
||||
"forceTvModeDescription": "Use the TV layout regardless of auto-detection. Useful on Android TV devices that don't report the leanback feature. Restarts the app on change.",
|
||||
"autoHidePerformanceOverlay": "Auto-Hide Performance Overlay",
|
||||
"autoHidePerformanceOverlayDescription": "Fade the performance overlay with the playback controls",
|
||||
"showNavBarLabels": "Show Navigation Bar Labels",
|
||||
"showNavBarLabelsDescription": "Display text labels under navigation bar icons",
|
||||
"liveTvDefaultFavorites": "Default to Favorite Channels",
|
||||
"liveTvDefaultFavoritesDescription": "Show only favorite channels when opening Live TV",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Search movies, shows, music...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "Skip Intro/Credits"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "Enter PIN",
|
||||
"showPin": "Show PIN",
|
||||
"hidePin": "Hide PIN"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "File Info",
|
||||
"video": "Video",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "Color Primaries",
|
||||
"chromaSubsampling": "Chroma Subsampling",
|
||||
"channels": "Channels",
|
||||
"subtitles": "Subtitles",
|
||||
"overallBitrate": "Overall Bitrate",
|
||||
"path": "Path",
|
||||
"size": "Size",
|
||||
"container": "Container",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "This includes all episodes and their files.",
|
||||
"mediaDeletedSuccessfully": "Media item deleted successfully",
|
||||
"mediaFailedToDelete": "Failed to delete media item",
|
||||
"rate": "Rate"
|
||||
"rate": "Rate",
|
||||
"playFromBeginning": "Play from Beginning",
|
||||
"playVersion": "Play Version..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, movie",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "Unlock rotation",
|
||||
"timerActive": "Timer Active",
|
||||
"playbackWillPauseIn": "Playback will pause in ${duration}",
|
||||
"sleepTimerCompleted": "Sleep timer completed - playback paused",
|
||||
"stillWatching": "Still watching?",
|
||||
"pausingIn": "Pausing in ${seconds}s",
|
||||
"continueWatching": "Continue",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "Mute",
|
||||
"unmuteButton": "Unmute",
|
||||
"settingsButton": "Video settings",
|
||||
"audioTrackButton": "Audio tracks",
|
||||
"subtitlesButton": "Subtitles",
|
||||
"tracksButton": "Audio & Subtitles",
|
||||
"chaptersButton": "Chapters",
|
||||
"versionsButton": "Video versions",
|
||||
"pipButton": "Picture-in-Picture mode",
|
||||
"aspectRatioButton": "Aspect ratio",
|
||||
"ambientLighting": "Ambient lighting",
|
||||
"ambientLightingOn": "Enable ambient lighting",
|
||||
"ambientLightingOff": "Disable ambient lighting",
|
||||
"fullscreenButton": "Enter fullscreen",
|
||||
"exitFullscreenButton": "Exit fullscreen",
|
||||
"alwaysOnTopButton": "Always on top",
|
||||
"rotationLockButton": "Rotation lock",
|
||||
"lockScreen": "Lock screen",
|
||||
"screenLockButton": "Screen lock",
|
||||
"longPressToUnlock": "Long press to unlock",
|
||||
"timelineSlider": "Video timeline",
|
||||
"volumeSlider": "Volume level",
|
||||
"endsAt": "Ends at ${time}",
|
||||
"pipActive": "Playing in Picture-in-Picture",
|
||||
"pipFailed": "Picture-in-picture failed to start",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Requires Android 8.0 or newer",
|
||||
"iosVersion": "Requires iOS 15.0 or newer",
|
||||
"permissionDisabled": "Picture-in-picture permission is disabled. Enable it in Settings > Apps > Plezy > Picture-in-picture",
|
||||
"notSupported": "Device doesn't support picture-in-picture mode",
|
||||
"voSwitchFailed": "Failed to switch video output for picture-in-picture",
|
||||
"failed": "Picture-in-picture failed to start",
|
||||
"unknown": "An error occurred: ${error}"
|
||||
},
|
||||
"chapters": "Chapters",
|
||||
"noChaptersAvailable": "No chapters available",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"noQueueItems": "No items in queue",
|
||||
"searchSubtitles": "Search Subtitles",
|
||||
"language": "Language",
|
||||
"noSubtitlesFound": "No subtitles found",
|
||||
"subtitleDownloaded": "Subtitle downloaded",
|
||||
"subtitleDownloadFailed": "Failed to download subtitle",
|
||||
"searchLanguages": "Search languages..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "Marked as unwatched",
|
||||
"markedAsWatchedOffline": "Marked as watched (will sync when online)",
|
||||
"markedAsUnwatchedOffline": "Marked as unwatched (will sync when online)",
|
||||
"autoRemovedWatchedDownload": "Auto-removed: ${title}",
|
||||
"removedFromContinueWatching": "Removed from Continue Watching",
|
||||
"errorLoading": "Error: ${error}",
|
||||
"fileInfoNotAvailable": "File information not available",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "Failed to create play queue - no items",
|
||||
"failedPlayback": "Failed to ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Switching to compatible player...",
|
||||
"serverLimitTitle": "Playback failed",
|
||||
"serverLimitBody": "The server returned an error (HTTP 500). This usually means the server owner has set a bandwidth or transcoding limit that's rejecting your session. There's nothing to do from the client — the server owner needs to adjust their settings.",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "Styling Options",
|
||||
"text": "Text",
|
||||
"border": "Border",
|
||||
"background": "Background",
|
||||
"fontSize": "Font Size",
|
||||
"textColor": "Text Color",
|
||||
"borderSize": "Border Size",
|
||||
"borderColor": "Border Color",
|
||||
"backgroundOpacity": "Background Opacity",
|
||||
"backgroundColor": "Background Color",
|
||||
"position": "Position"
|
||||
"position": "Position",
|
||||
"assOverride": "ASS Override",
|
||||
"bold": "Bold",
|
||||
"italic": "Italic"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV Configuration",
|
||||
"title": "mpv.conf",
|
||||
"description": "Advanced video player settings",
|
||||
"properties": "Properties",
|
||||
"presets": "Presets",
|
||||
"noProperties": "No properties configured",
|
||||
"noPresets": "No saved presets",
|
||||
"addProperty": "Add Property",
|
||||
"editProperty": "Edit Property",
|
||||
"deleteProperty": "Delete Property",
|
||||
"propertyKey": "Property Key",
|
||||
"propertyKeyHint": "e.g., hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Property Value",
|
||||
"propertyValueHint": "e.g., auto, 256000000",
|
||||
"saveAsPreset": "Save as Preset...",
|
||||
"presetName": "Preset Name",
|
||||
"presetNameHint": "Enter a name for this preset",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "Preset loaded",
|
||||
"presetDeleted": "Preset deleted",
|
||||
"confirmDeletePreset": "Are you sure you want to delete this preset?",
|
||||
"confirmDeleteProperty": "Are you sure you want to delete this property?",
|
||||
"entriesCount": "${count} entries"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Confirm Action"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "Overview",
|
||||
"cast": "Cast",
|
||||
"extras": "Trailers & Extras",
|
||||
"seasons": "Seasons",
|
||||
"studio": "Studio",
|
||||
"rating": "Rating",
|
||||
"episodeCount": "${count} episodes",
|
||||
"watchedProgress": "${watched}/${total} watched",
|
||||
"movie": "Movie",
|
||||
"tvShow": "TV Show",
|
||||
"minutesLeft": "${minutes} min left"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "Playlists"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Grouping",
|
||||
"all": "All",
|
||||
"movies": "Movies",
|
||||
"shows": "TV Shows",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "Clear Logs",
|
||||
"copyLogs": "Copy Logs",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "Error:",
|
||||
"stackTrace": "Stack Trace:"
|
||||
"uploadLogs": "Upload Logs"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Related Packages",
|
||||
@@ -565,20 +590,12 @@
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "Live TV",
|
||||
"channels": "Channels",
|
||||
"guide": "Guide",
|
||||
"noChannels": "No channels available",
|
||||
"noDvr": "No DVR configured on any server",
|
||||
"tuneFailed": "Failed to tune channel",
|
||||
"loading": "Loading channels...",
|
||||
"nowPlaying": "Now Playing",
|
||||
"noPrograms": "No program data available",
|
||||
"channelNumber": "Ch. ${number}",
|
||||
"live": "LIVE",
|
||||
"hd": "HD",
|
||||
"premiere": "NEW",
|
||||
"reloadGuide": "Reload Guide",
|
||||
"allChannels": "All Channels",
|
||||
"now": "Now",
|
||||
"today": "Today",
|
||||
"midnight": "Midnight",
|
||||
@@ -588,7 +605,13 @@
|
||||
"evening": "Evening",
|
||||
"lateNight": "Late Night",
|
||||
"whatsOn": "What's On",
|
||||
"watchChannel": "Watch Channel"
|
||||
"watchChannel": "Watch Channel",
|
||||
"favorites": "Favorites",
|
||||
"reorderFavorites": "Reorder Favorites",
|
||||
"joinSession": "Join Session in Progress",
|
||||
"watchFromStart": "Watch from start (${minutes} min ago)",
|
||||
"watchLive": "Watch Live",
|
||||
"goToLive": "Go to Live"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collections",
|
||||
@@ -602,7 +625,6 @@
|
||||
"deleteFailedWithError": "Failed to delete collection: ${error}",
|
||||
"failedToLoadItems": "Failed to load collection items: ${error}",
|
||||
"selectCollection": "Select Collection",
|
||||
"createNewCollection": "Create New Collection",
|
||||
"collectionName": "Collection Name",
|
||||
"enterCollectionName": "Enter collection name",
|
||||
"addedToCollection": "Added to collection",
|
||||
@@ -612,7 +634,8 @@
|
||||
"removeFromCollectionConfirm": "Remove \"${title}\" from this collection?",
|
||||
"removedFromCollection": "Removed from collection",
|
||||
"removeFromCollectionFailed": "Failed to remove from collection",
|
||||
"removeFromCollectionError": "Error removing from collection: ${error}"
|
||||
"removeFromCollectionError": "Error removing from collection: ${error}",
|
||||
"searchCollections": "Search collections..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Playlists",
|
||||
@@ -634,7 +657,6 @@
|
||||
"itemAdded": "Added to playlist",
|
||||
"itemRemoved": "Removed from playlist",
|
||||
"selectPlaylist": "Select Playlist",
|
||||
"createNewPlaylist": "Create New Playlist",
|
||||
"errorCreating": "Failed to create playlist",
|
||||
"errorDeleting": "Failed to delete playlist",
|
||||
"errorLoading": "Failed to load playlists",
|
||||
@@ -677,18 +699,30 @@
|
||||
"leave": "Leave",
|
||||
"syncing": "Syncing...",
|
||||
"joinWatchSession": "Join Watch Session",
|
||||
"enterCodeHint": "Enter 8-character code",
|
||||
"enterCodeHint": "Enter 5-character code",
|
||||
"pasteFromClipboard": "Paste from clipboard",
|
||||
"pleaseEnterCode": "Please enter a session code",
|
||||
"codeMustBe8Chars": "Session code must be 8 characters",
|
||||
"codeMustBe5Chars": "Session code must be 5 characters",
|
||||
"joinInstructions": "Enter the session code shared by the host to join their watch session.",
|
||||
"failedToCreate": "Failed to create session",
|
||||
"failedToJoin": "Failed to join session",
|
||||
"sessionCodeCopied": "Session code copied to clipboard",
|
||||
"relayUnreachable": "The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.",
|
||||
"reconnectingToHost": "Reconnecting to host...",
|
||||
"currentPlayback": "Current Playback",
|
||||
"joinCurrentPlayback": "Join Current Playback",
|
||||
"joinCurrentPlaybackDescription": "Jump back into what the host is currently watching",
|
||||
"failedToOpenCurrentPlayback": "Failed to open current playback",
|
||||
"participantJoined": "${name} joined",
|
||||
"participantLeft": "${name} left"
|
||||
"participantLeft": "${name} left",
|
||||
"participantPaused": "${name} paused",
|
||||
"participantResumed": "${name} resumed",
|
||||
"participantSeeked": "${name} seeked",
|
||||
"participantBuffering": "${name} is buffering",
|
||||
"waitingForParticipants": "Waiting for others to load...",
|
||||
"recentRooms": "Recent Rooms",
|
||||
"renameRoom": "Rename Room",
|
||||
"removeRoom": "Remove"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
@@ -701,6 +735,7 @@
|
||||
"deleteDownload": "Delete download",
|
||||
"retryDownload": "Retry download",
|
||||
"downloadQueued": "Download queued",
|
||||
"serverErrorBitrate": "Server error — the file may exceed the remote streaming bitrate limit",
|
||||
"episodesQueued": "${count} episodes queued for download",
|
||||
"downloadDeleted": "Download deleted",
|
||||
"deleteConfirm": "Are you sure you want to delete \"${title}\"? This will remove the downloaded file from your device.",
|
||||
@@ -708,7 +743,32 @@
|
||||
"noDownloadsTree": "No downloads",
|
||||
"pauseAll": "Pause all",
|
||||
"resumeAll": "Resume all",
|
||||
"deleteAll": "Delete all"
|
||||
"deleteAll": "Delete all",
|
||||
"selectVersion": "Select Version",
|
||||
"allEpisodes": "All episodes",
|
||||
"unwatchedOnly": "Unwatched only",
|
||||
"nextNUnwatched": "Next ${count} unwatched",
|
||||
"customAmount": "Custom amount...",
|
||||
"howManyEpisodes": "How many episodes?",
|
||||
"itemsQueued": "${count} items queued for download",
|
||||
"keepSynced": "Keep synced",
|
||||
"downloadOnce": "Download once",
|
||||
"keepNUnwatched": "Keep ${count} unwatched",
|
||||
"editSyncRule": "Edit sync rule",
|
||||
"removeSyncRule": "Remove sync rule",
|
||||
"removeSyncRuleConfirm": "Stop syncing \"${title}\"? Downloaded episodes will be kept.",
|
||||
"syncRuleCreated": "Sync rule created — keeping ${count} unwatched episodes",
|
||||
"syncRuleUpdated": "Sync rule updated",
|
||||
"syncRuleRemoved": "Sync rule removed",
|
||||
"syncedNewEpisodes": "Synced ${count} new episodes for ${title}",
|
||||
"activeSyncRules": "Sync rules",
|
||||
"noSyncRules": "No sync rules",
|
||||
"manageSyncRule": "Manage sync",
|
||||
"editEpisodeCount": "Episode count",
|
||||
"editSyncFilter": "Sync filter",
|
||||
"syncAllItems": "Syncing all items",
|
||||
"syncUnwatchedItems": "Syncing unwatched items",
|
||||
"syncRuleListCreated": "Sync rule created"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shaders",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "NVIDIA image scaling for sharper video",
|
||||
"qualityFast": "Fast",
|
||||
"qualityHQ": "High Quality",
|
||||
"mode": "Mode"
|
||||
"mode": "Mode",
|
||||
"importShader": "Import Shader",
|
||||
"customShaderDescription": "Custom GLSL shader",
|
||||
"shaderImported": "Shader imported",
|
||||
"shaderImportFailed": "Failed to import shader",
|
||||
"deleteShader": "Delete Shader",
|
||||
"deleteShaderConfirm": "Delete \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "Connect to Device",
|
||||
"hostRemoteSession": "Host Remote Session",
|
||||
"controlThisDevice": "Control this device with your phone",
|
||||
"remoteControl": "Remote Control",
|
||||
"controlDesktop": "Control a desktop device",
|
||||
"connectedTo": "Connected to ${name}",
|
||||
"session": {
|
||||
"creatingSession": "Creating remote session...",
|
||||
"failedToCreate": "Failed to create remote session:",
|
||||
"noSession": "No session available",
|
||||
"scanQrCode": "Scan QR Code",
|
||||
"orEnterManually": "Or enter manually",
|
||||
"startingServer": "Starting remote server...",
|
||||
"failedToCreate": "Failed to start remote server:",
|
||||
"hostAddress": "Host Address",
|
||||
"sessionId": "Session ID",
|
||||
"pin": "PIN",
|
||||
"connected": "Connected",
|
||||
"waitingForConnection": "Waiting for connection...",
|
||||
"serverRunning": "Remote server active",
|
||||
"serverStopped": "Remote server stopped",
|
||||
"serverRunningDescription": "Mobile devices on your network can discover and connect to this app",
|
||||
"serverStoppedDescription": "Start the server to allow mobile devices to connect",
|
||||
"usePhoneToControl": "Use your mobile device to control this app",
|
||||
"copiedToClipboard": "${label} copied to clipboard",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"newSession": "New Session",
|
||||
"startServer": "Start Server",
|
||||
"stopServer": "Stop Server",
|
||||
"minimize": "Minimize"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "Scan",
|
||||
"manual": "Manual",
|
||||
"pairWithDesktop": "Pair with Desktop",
|
||||
"enterSessionDetails": "Enter the session details shown on your desktop device",
|
||||
"discoveryDescription": "Devices on your network running Plezy with the same Plex account will appear automatically",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "Enter 8-character session ID",
|
||||
"pinHint": "Enter 6-digit PIN",
|
||||
"connecting": "Connecting...",
|
||||
"tips": "Tips",
|
||||
"tipDesktop": "Open Plezy on your desktop and enable Companion Remote from settings or menu",
|
||||
"tipScan": "Use the Scan tab to quickly pair by scanning the QR code on your desktop",
|
||||
"tipWifi": "Make sure both devices are on the same WiFi network",
|
||||
"cameraPermissionRequired": "Camera permission is required to scan QR codes.\nPlease grant camera access in your device settings.",
|
||||
"cameraError": "Could not start camera: ${error}",
|
||||
"scanInstruction": "Point your camera at the QR code shown on your desktop",
|
||||
"invalidQrCode": "Invalid QR code format",
|
||||
"searchingForDevices": "Looking for devices...",
|
||||
"noDevicesFound": "No devices found on your network",
|
||||
"noDevicesHint": "Make sure Plezy is open on your desktop and both devices are on the same WiFi network",
|
||||
"availableDevices": "Available Devices",
|
||||
"manualConnection": "Manual Connection",
|
||||
"cryptoInitFailed": "Could not initialize secure connection. Make sure you are signed in to a Plex account.",
|
||||
"validationHostRequired": "Please enter host address",
|
||||
"validationHostFormat": "Format must be IP:port (e.g., 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "Please enter a session ID",
|
||||
"validationSessionIdLength": "Session ID must be 8 characters",
|
||||
"validationPinRequired": "Please enter a PIN",
|
||||
"validationPinLength": "PIN must be 6 digits",
|
||||
"connectionTimedOut": "Connection timed out. Please check the session ID and PIN.",
|
||||
"sessionNotFound": "Could not find the session. Please check your credentials.",
|
||||
"connectionTimedOut": "Connection timed out. Make sure both devices are on the same network.",
|
||||
"sessionNotFound": "Could not find the device. Make sure Plezy is running on the host.",
|
||||
"authFailed": "Authentication failed. Make sure both devices are on the same Plex account.",
|
||||
"failedToConnect": "Failed to connect: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "Reconnecting...",
|
||||
"attemptOf": "Attempt ${current} of 5",
|
||||
"retryNow": "Retry Now",
|
||||
"connectionError": "Connection error",
|
||||
"notConnected": "Not connected",
|
||||
"tabRemote": "Remote",
|
||||
"tabPlay": "Play",
|
||||
"tabMore": "More",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "Use External Player",
|
||||
"useExternalPlayerDescription": "Open videos in an external app instead of the built-in player",
|
||||
"selectPlayer": "Select Player",
|
||||
"customPlayers": "Custom Players",
|
||||
"systemDefault": "System Default",
|
||||
"addCustomPlayer": "Add Custom Player",
|
||||
"playerName": "Player Name",
|
||||
"playerCommand": "Command",
|
||||
"playerPackage": "Package Name",
|
||||
"playerUrlScheme": "URL Scheme",
|
||||
"customPlayer": "Custom Player",
|
||||
"off": "Off",
|
||||
"launchFailed": "Failed to open external player",
|
||||
"appNotInstalled": "${name} is not installed",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "Summary",
|
||||
"poster": "Poster",
|
||||
"background": "Background",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Square Art",
|
||||
"selectPoster": "Select Poster",
|
||||
"selectBackground": "Select Background",
|
||||
"selectLogo": "Select Logo",
|
||||
"selectSquareArt": "Select Square Art",
|
||||
"fromUrl": "From URL",
|
||||
"uploadFile": "Upload File",
|
||||
"enterImageUrl": "Enter image URL",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "Auto-Select Subtitle Mode",
|
||||
"manuallySelected": "Manually selected",
|
||||
"shownWithForeignAudio": "Shown with foreign audio",
|
||||
"alwaysEnabled": "Always enabled"
|
||||
"alwaysEnabled": "Always enabled",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add tag",
|
||||
"genre": "Genre",
|
||||
"director": "Director",
|
||||
"writer": "Writer",
|
||||
"producer": "Producer",
|
||||
"country": "Country",
|
||||
"collection": "Collection",
|
||||
"label": "Label",
|
||||
"style": "Style",
|
||||
"mood": "Mood"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Server Tasks",
|
||||
"failedToLoad": "Failed to load tasks",
|
||||
"noTasks": "No tasks running"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Connected",
|
||||
"connectedAs": "Connected as @${username}",
|
||||
"disconnectConfirm": "Disconnect Trakt account?",
|
||||
"disconnectConfirmBody": "Plezy will stop sending playback events to Trakt. You can reconnect at any time.",
|
||||
"scrobble": "Real-time scrobbling",
|
||||
"scrobbleDescription": "Send play, pause, and stop events to Trakt during playback.",
|
||||
"watchedSync": "Sync watched status",
|
||||
"watchedSyncDescription": "When you mark items watched in Plezy, mark them on Trakt.",
|
||||
"deviceCodeTitle": "Activate Plezy on Trakt",
|
||||
"deviceCodeBody": "Visit ${url} and enter this code:",
|
||||
"openTraktActivate": "Open Trakt to activate",
|
||||
"waitingForAuthorization": "Waiting for authorization…",
|
||||
"codeCopied": "Code copied",
|
||||
"connectFailed": "Couldn't connect to Trakt. Try again."
|
||||
}
|
||||
}
|
||||
|
||||
+207
-124
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "Inicia sesión con Plex",
|
||||
"showQRCode": "Mostrar código QR",
|
||||
"authenticate": "Autenticar",
|
||||
"debugEnterToken": "Debug: Ingresar Token de Plex",
|
||||
"plexTokenLabel": "Token de Autenticación de Plex",
|
||||
"plexTokenHint": "Ingresa tu token de Plex.tv",
|
||||
"authenticationTimeout": "Tiempo de autenticación agotado. Por favor, intenta de nuevo.",
|
||||
"scanQRToSignIn": "Escanea este código QR para iniciar sesión",
|
||||
"waitingForAuth": "Esperando autenticación...\nPor favor completa el inicio de sesión en tu navegador.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "Eliminar",
|
||||
"shuffle": "Aleatorio",
|
||||
"addTo": "Añadir a...",
|
||||
"remove": "Eliminar",
|
||||
"paste": "Pegar",
|
||||
"createNew": "Crear",
|
||||
"connect": "Conectar",
|
||||
"disconnect": "Desconectar",
|
||||
"play": "Reproducir",
|
||||
@@ -46,7 +42,6 @@
|
||||
"settings": "Ajustes",
|
||||
"mute": "Silencio",
|
||||
"ok": "OK",
|
||||
"loading": "Cargando...",
|
||||
"reconnect": "Reconectar",
|
||||
"exitConfirmTitle": "¿Salir de la app?",
|
||||
"exitConfirmMessage": "¿Estás seguro de que quieres salir?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "Actualizando servidores...",
|
||||
"loadingServers": "Cargando servidores...",
|
||||
"connectingToServers": "Conectando a servidores...",
|
||||
"startingOfflineMode": "Iniciando modo sin conexión..."
|
||||
"startingOfflineMode": "Iniciando modo sin conexión...",
|
||||
"loading": "Cargando..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licencias",
|
||||
"switchProfile": "Cambiar Perfil",
|
||||
"subtitleStyling": "Estilo de Subtítulos",
|
||||
"mpvConfig": "Configuración de MPV",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"supportDeveloper": "Apoya Plezy",
|
||||
"supportDeveloperDescription": "Dona vía Liberapay para financiar el desarrollo",
|
||||
"language": "Idioma",
|
||||
"theme": "Tema",
|
||||
"appearance": "Apariencia",
|
||||
"videoPlayback": "Reproducción de Video",
|
||||
"videoPlaybackDescription": "Configurar el comportamiento de reproducción",
|
||||
"advanced": "Avanzado",
|
||||
"episodePosterMode": "Estilo de Póster de Episodio",
|
||||
"seriesPoster": "Póster de Serie",
|
||||
"seriesPosterDescription": "Mostrar el póster de la serie para todos los episodios",
|
||||
"seasonPoster": "Póster de Temporada",
|
||||
"seasonPosterDescription": "Mostrar el póster de la temporada para los episodios",
|
||||
"episodeThumbnail": "Miniatura de Episodio",
|
||||
"episodeThumbnailDescription": "Mostrar miniaturas de capturas de pantalla de episodios en 16:9",
|
||||
"episodeThumbnail": "Miniatura",
|
||||
"showHeroSectionDescription": "Mostrar carrusel de contenido destacado en la pantalla de inicio",
|
||||
"secondsLabel": "Segundos",
|
||||
"minutesLabel": "Minutos",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Ingresa la duración (${min}-${max})",
|
||||
"systemTheme": "Sistema",
|
||||
"systemThemeDescription": "Sigue la configuración del sistema",
|
||||
"lightTheme": "Claro",
|
||||
"darkTheme": "Oscuro",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "Negro puro para pantallas OLED",
|
||||
"libraryDensity": "Densidad de Biblioteca",
|
||||
"compact": "Compacto",
|
||||
"compactDescription": "Tarjetas más pequeñas, más elementos visibles",
|
||||
"normal": "Normal",
|
||||
"normalDescription": "Tamaño predeterminado",
|
||||
"comfortable": "Cómodo",
|
||||
"comfortableDescription": "Tarjetas más grandes, menos elementos visibles",
|
||||
"viewMode": "Modo de Vista",
|
||||
"gridView": "Cuadrícula",
|
||||
"gridViewDescription": "Mostrar elementos en un diseño de cuadrícula",
|
||||
"listView": "Lista",
|
||||
"listViewDescription": "Mostrar elementos en un diseño de lista",
|
||||
"showHeroSection": "Mostrar Sección Destacada",
|
||||
"useGlobalHubs": "Usar Diseño de Inicio de Plex",
|
||||
"useGlobalHubsDescription": "Mostrar los hubs de la página de inicio como el cliente oficial de Plex. Cuando está desactivado, muestra recomendaciones por biblioteca en su lugar.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "Difuminar miniaturas y ocultar descripciones de episodios que aún no has visto",
|
||||
"playerBackend": "Reproductor",
|
||||
"exoPlayer": "ExoPlayer (Recomendado)",
|
||||
"exoPlayerDescription": "Reproductor nativo de Android con mejor soporte de hardware",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "Reproductor avanzado con más funciones y soporte de subtítulos ASS",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Decodificación por Hardware",
|
||||
"hardwareDecodingDescription": "Usar aceleración por hardware cuando esté disponible",
|
||||
"bufferSize": "Tamaño del Buffer",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "Personalizar la apariencia de los subtítulos",
|
||||
"smallSkipDuration": "Salto pequeño",
|
||||
"largeSkipDuration": "Salto grande",
|
||||
"rewindOnResume": "Rebobinar al reanudar",
|
||||
"secondsUnit": "${seconds} segundos",
|
||||
"defaultSleepTimer": "Temporizador de apagado",
|
||||
"minutesUnit": "${minutes} minutos",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "Personalizar los atajos de teclado",
|
||||
"videoPlayerNavigation": "Navegación del Reproductor de Video",
|
||||
"videoPlayerNavigationDescription": "Usar las teclas de flecha para navegar por los controles del reproductor",
|
||||
"watchTogetherRelay": "Relay de Ver Juntos",
|
||||
"watchTogetherRelayDescription": "Configurar un servidor relay personalizado para Ver Juntos. Todos los participantes deben usar el mismo servidor.",
|
||||
"watchTogetherRelayHint": "https://mi-relay.ejemplo.com",
|
||||
"crashReporting": "Informes de Errores",
|
||||
"crashReportingDescription": "Enviar informes de errores para mejorar la aplicación",
|
||||
"debugLogging": "Registro de Depuración",
|
||||
"debugLoggingDescription": "Habilitar registros detallados para resolución de problemas",
|
||||
"viewLogs": "Ver Logs",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "Saltar automáticamente los créditos y reproducir el siguiente episodio",
|
||||
"autoSkipDelay": "Retraso de Salto automático",
|
||||
"autoSkipDelayDescription": "Esperar ${seconds} segundos antes de saltar automáticamente",
|
||||
"introPattern": "Patrón de marcador de intro",
|
||||
"introPatternDescription": "Expresión regular para reconocer marcadores de intro en los títulos de capítulos",
|
||||
"creditsPattern": "Patrón de marcador de créditos",
|
||||
"creditsPatternDescription": "Expresión regular para reconocer marcadores de créditos en los títulos de capítulos",
|
||||
"invalidRegex": "Expresión regular no válida",
|
||||
"downloads": "Descargas",
|
||||
"downloadLocationDescription": "Elegir dónde almacenar el contenido descargado",
|
||||
"downloadLocationDefault": "Predeterminado (Almacenamiento de la App)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "Error al seleccionar la carpeta",
|
||||
"downloadOnWifiOnly": "Descargar solo con WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Evitar descargas cuando se usan datos móviles",
|
||||
"autoRemoveWatchedDownloads": "Eliminar descargas vistas automáticamente",
|
||||
"autoRemoveWatchedDownloadsDescription": "Eliminar automáticamente episodios y películas descargados cuando se marquen como vistos",
|
||||
"cellularDownloadBlocked": "Las descargas están desactivadas en datos móviles. Conéctate a una red WiFi o cambia la configuración.",
|
||||
"maxVolume": "Volumen Máximo",
|
||||
"maxVolumeDescription": "Permitir aumento de volumen por encima del 100% para medios con sonido bajo",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Presencia de Discord",
|
||||
"discordRichPresenceDescription": "Mostrar lo que estás viendo en Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Sincronizar historial de visualización con Trakt",
|
||||
"companionRemoteServer": "Servidor de control remoto",
|
||||
"companionRemoteServerDescription": "Permitir que dispositivos móviles en tu red controlen esta aplicación",
|
||||
"autoPip": "Imagen en imagen automática",
|
||||
"autoPipDescription": "Activar automáticamente imagen en imagen al salir de la app durante la reproducción",
|
||||
"matchContentFrameRate": "Ajustar frecuencia de actualización",
|
||||
"matchContentFrameRateDescription": "Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "Ajustar frecuencia de refresco",
|
||||
"matchRefreshRateDescription": "Cambiar la frecuencia de refresco de la pantalla para coincidir con el contenido de video en pantalla completa",
|
||||
"matchDynamicRange": "Ajustar rango dinámico",
|
||||
"matchDynamicRangeDescription": "Activar HDR automáticamente para contenido HDR y volver a SDR al salir del reproductor",
|
||||
"displaySwitchDelay": "Retraso de cambio de pantalla",
|
||||
"tunneledPlayback": "Reproducción tunelizada",
|
||||
"tunneledPlaybackDescription": "Usar tunelización de video acelerada por hardware. Desactivar si ves una pantalla negra con audio en contenido HDR",
|
||||
"requireProfileSelectionOnOpen": "Pedir perfil al abrir la app",
|
||||
"requireProfileSelectionOnOpenDescription": "Mostrar selección de perfil cada vez que se abre la aplicación",
|
||||
"confirmExitOnBack": "Confirmar antes de salir",
|
||||
"confirmExitOnBackDescription": "Mostrar un diálogo de confirmación al presionar atrás para salir de la app"
|
||||
"confirmExitOnBackDescription": "Mostrar un diálogo de confirmación al presionar atrás para salir de la app",
|
||||
"forceTvMode": "Forzar modo TV",
|
||||
"forceTvModeDescription": "Usar el diseño de TV independientemente de la detección automática. Útil en dispositivos Android TV que no informan la característica leanback. Reinicia la app al cambiar.",
|
||||
"autoHidePerformanceOverlay": "Ocultar superposición de rendimiento automáticamente",
|
||||
"autoHidePerformanceOverlayDescription": "Desvanecer la superposición de rendimiento con los controles de reproducción",
|
||||
"showNavBarLabels": "Mostrar etiquetas de la barra de navegación",
|
||||
"showNavBarLabelsDescription": "Mostrar etiquetas de texto bajo los iconos de la barra de navegación",
|
||||
"liveTvDefaultFavorites": "Canales favoritos por defecto",
|
||||
"liveTvDefaultFavoritesDescription": "Mostrar solo canales favoritos al abrir TV en vivo",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Buscar películas, series, música...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "Saltar Intro/Créditos"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "Introduce el PIN",
|
||||
"showPin": "Mostrar PIN",
|
||||
"hidePin": "Ocultar PIN"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Información del Archivo",
|
||||
"video": "Video",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "Primarias de color",
|
||||
"chromaSubsampling": "Submuestreo de croma",
|
||||
"channels": "Canales",
|
||||
"subtitles": "Subtítulos",
|
||||
"overallBitrate": "Bitrate total",
|
||||
"path": "Ruta",
|
||||
"size": "Tamaño",
|
||||
"container": "Contenedor",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "Esto incluye todos los episodios y sus archivos.",
|
||||
"mediaDeletedSuccessfully": "Elemento multimedia eliminado con éxito",
|
||||
"mediaFailedToDelete": "Error al eliminar el elemento multimedia",
|
||||
"rate": "Calificar"
|
||||
"rate": "Calificar",
|
||||
"playFromBeginning": "Reproducir desde el inicio",
|
||||
"playVersion": "Reproducir versión..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, película",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "Desbloquear rotación",
|
||||
"timerActive": "Temporizador Activo",
|
||||
"playbackWillPauseIn": "La reproducción se pausará en ${duration}",
|
||||
"sleepTimerCompleted": "Temporizador completado - reproducción pausada",
|
||||
"stillWatching": "¿Sigues viendo?",
|
||||
"pausingIn": "Pausa en ${seconds}s",
|
||||
"continueWatching": "Continuar",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "Silenciar",
|
||||
"unmuteButton": "Activar sonido",
|
||||
"settingsButton": "Ajustes de video",
|
||||
"audioTrackButton": "Pistas de audio",
|
||||
"subtitlesButton": "Subtítulos",
|
||||
"tracksButton": "Audio y subtítulos",
|
||||
"chaptersButton": "Capítulos",
|
||||
"versionsButton": "Versiones de video",
|
||||
"pipButton": "Modo PiP (Imagen en Imagen)",
|
||||
"aspectRatioButton": "Relación de aspecto",
|
||||
"ambientLighting": "Iluminación ambiental",
|
||||
"ambientLightingOn": "Activar iluminación ambiental",
|
||||
"ambientLightingOff": "Desactivar iluminación ambiental",
|
||||
"fullscreenButton": "Entrar en pantalla completa",
|
||||
"exitFullscreenButton": "Salir de pantalla completa",
|
||||
"alwaysOnTopButton": "Siempre visible",
|
||||
"rotationLockButton": "Bloqueo de rotación",
|
||||
"lockScreen": "Bloquear pantalla",
|
||||
"screenLockButton": "Bloqueo de pantalla",
|
||||
"longPressToUnlock": "Mantén pulsado para desbloquear",
|
||||
"timelineSlider": "Línea de tiempo del video",
|
||||
"volumeSlider": "Nivel de volumen",
|
||||
"endsAt": "Termina a las ${time}",
|
||||
"pipActive": "Reproduciendo en Imagen en Imagen",
|
||||
"pipFailed": "Error al iniciar Imagen en Imagen",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Requiere Android 8.0 o más reciente",
|
||||
"iosVersion": "Requiere iOS 15.0 o más reciente",
|
||||
"permissionDisabled": "El permiso de Imagen en Imagen está desactivado. Actívalo en Ajustes > Aplicaciones > Plezy > Imagen en Imagen",
|
||||
"notSupported": "El dispositivo no soporta el modo Imagen en Imagen",
|
||||
"voSwitchFailed": "Error al cambiar la salida de video para Imagen en Imagen",
|
||||
"failed": "Error al iniciar Imagen en Imagen",
|
||||
"unknown": "Ocurrió un error: ${error}"
|
||||
},
|
||||
"chapters": "Capítulos",
|
||||
"noChaptersAvailable": "No hay capítulos disponibles",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "Cola",
|
||||
"noQueueItems": "No hay elementos en la cola",
|
||||
"searchSubtitles": "Buscar subtítulos",
|
||||
"language": "Idioma",
|
||||
"noSubtitlesFound": "No se encontraron subtítulos",
|
||||
"subtitleDownloaded": "Subtítulo descargado",
|
||||
"subtitleDownloadFailed": "Error al descargar subtítulo",
|
||||
"searchLanguages": "Buscar idiomas..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrador",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "Marcado como no visto",
|
||||
"markedAsWatchedOffline": "Marcado como visto (se sincronizará al estar en línea)",
|
||||
"markedAsUnwatchedOffline": "Marcado como no visto (se sincronizará al estar en línea)",
|
||||
"autoRemovedWatchedDownload": "Eliminado automáticamente: ${title}",
|
||||
"removedFromContinueWatching": "Eliminado de Seguir Viendo",
|
||||
"errorLoading": "Error: ${error}",
|
||||
"fileInfoNotAvailable": "Información de archivo no disponible",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "Error al crear la cola de reproducción - no hay elementos",
|
||||
"failedPlayback": "Error al ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Cambiando a reproductor compatible...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "Error de reproducción",
|
||||
"serverLimitBody": "El servidor devolvió un error (HTTP 500). Normalmente esto significa que el propietario del servidor ha establecido un límite de ancho de banda o transcodificación que está rechazando tu sesión. No hay nada que puedas hacer desde el cliente — el propietario del servidor debe ajustar su configuración.",
|
||||
"logsUploaded": "Registros subidos",
|
||||
"logsUploadFailed": "Error al subir registros",
|
||||
"logId": "ID de registro"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "Opciones de Estilo",
|
||||
"text": "Texto",
|
||||
"border": "Borde",
|
||||
"background": "Fondo",
|
||||
"fontSize": "Tamaño de Fuente",
|
||||
"textColor": "Color de Texto",
|
||||
"borderSize": "Tamaño de Borde",
|
||||
"borderColor": "Color de Borde",
|
||||
"backgroundOpacity": "Opacidad de Fondo",
|
||||
"backgroundColor": "Color de Fondo",
|
||||
"position": "Position"
|
||||
"position": "Posición",
|
||||
"assOverride": "Sobreescritura ASS",
|
||||
"bold": "Negrita",
|
||||
"italic": "Cursiva"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "Configuración de MPV",
|
||||
"title": "Configuración de mpv",
|
||||
"description": "Ajustes avanzados del reproductor de video",
|
||||
"properties": "Propiedades",
|
||||
"presets": "Ajustes preestablecidos",
|
||||
"noProperties": "No hay propiedades configuradas",
|
||||
"noPresets": "No hay ajustes guardados",
|
||||
"addProperty": "Añadir Propiedad",
|
||||
"editProperty": "Editar Propiedad",
|
||||
"deleteProperty": "Eliminar Propiedad",
|
||||
"propertyKey": "Clave de la Propiedad",
|
||||
"propertyKeyHint": "ej., hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Valor de la Propiedad",
|
||||
"propertyValueHint": "ej., auto, 256000000",
|
||||
"saveAsPreset": "Guardar como Ajuste...",
|
||||
"presetName": "Nombre del Ajuste",
|
||||
"presetNameHint": "Introduce un nombre para este ajuste",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "Ajuste cargado",
|
||||
"presetDeleted": "Ajuste eliminado",
|
||||
"confirmDeletePreset": "¿Estás seguro de que quieres eliminar este ajuste?",
|
||||
"confirmDeleteProperty": "¿Estás seguro de que quieres eliminar esta propiedad?",
|
||||
"entriesCount": "${count} entradas"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Confirmar Acción"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "Resumen",
|
||||
"cast": "Reparto",
|
||||
"extras": "Tráilers y Extras",
|
||||
"seasons": "Temporadas",
|
||||
"studio": "Estudio",
|
||||
"rating": "Calificación",
|
||||
"episodeCount": "${count} episodios",
|
||||
"watchedProgress": "${watched}/${total} vistos",
|
||||
"movie": "Película",
|
||||
"tvShow": "Serie de TV",
|
||||
"minutesLeft": "quedan ${minutes} min"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "Listas"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Agrupación",
|
||||
"all": "Todo",
|
||||
"movies": "Películas",
|
||||
"shows": "Series",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "Borrar Logs",
|
||||
"copyLogs": "Copiar Logs",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "Error:",
|
||||
"stackTrace": "Traza de la pila (Stack Trace):"
|
||||
"uploadLogs": "Subir registros"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Paquetes relacionados",
|
||||
@@ -559,26 +584,18 @@
|
||||
"licensesCount": "${count} licencias"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Bibliotecas",
|
||||
"libraries": "Medios",
|
||||
"downloads": "Descargas",
|
||||
"liveTv": "TV en vivo"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "TV en vivo",
|
||||
"channels": "Canales",
|
||||
"guide": "Guía",
|
||||
"noChannels": "No hay canales disponibles",
|
||||
"noDvr": "No hay DVR configurado en ningún servidor",
|
||||
"tuneFailed": "Error al sintonizar el canal",
|
||||
"loading": "Cargando canales...",
|
||||
"nowPlaying": "Reproduciendo ahora",
|
||||
"noPrograms": "No hay datos de programación disponibles",
|
||||
"channelNumber": "Canal ${number}",
|
||||
"live": "EN VIVO",
|
||||
"hd": "HD",
|
||||
"premiere": "NUEVO",
|
||||
"reloadGuide": "Recargar guía",
|
||||
"allChannels": "Todos los canales",
|
||||
"now": "Ahora",
|
||||
"today": "Hoy",
|
||||
"midnight": "Medianoche",
|
||||
@@ -588,7 +605,13 @@
|
||||
"evening": "Noche",
|
||||
"lateNight": "Trasnoche",
|
||||
"whatsOn": "En emisión",
|
||||
"watchChannel": "Ver canal"
|
||||
"watchChannel": "Ver canal",
|
||||
"favorites": "Favoritos",
|
||||
"reorderFavorites": "Reordenar favoritos",
|
||||
"joinSession": "Unirse a sesión en curso",
|
||||
"watchFromStart": "Ver desde el inicio (hace ${minutes} min)",
|
||||
"watchLive": "Ver en vivo",
|
||||
"goToLive": "Ir a en vivo"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Colecciones",
|
||||
@@ -602,7 +625,6 @@
|
||||
"deleteFailedWithError": "Error al eliminar la colección: ${error}",
|
||||
"failedToLoadItems": "Error al cargar los elementos de la colección: ${error}",
|
||||
"selectCollection": "Seleccionar Colección",
|
||||
"createNewCollection": "Crear Nueva Colección",
|
||||
"collectionName": "Nombre de la Colección",
|
||||
"enterCollectionName": "Introduce el nombre de la colección",
|
||||
"addedToCollection": "Añadido a la colección",
|
||||
@@ -612,7 +634,8 @@
|
||||
"removeFromCollectionConfirm": "¿Eliminar \"${title}\" de esta colección?",
|
||||
"removedFromCollection": "Eliminado de la colección",
|
||||
"removeFromCollectionFailed": "Error al eliminar de la colección",
|
||||
"removeFromCollectionError": "Error al eliminar de la colección: ${error}"
|
||||
"removeFromCollectionError": "Error al eliminar de la colección: ${error}",
|
||||
"searchCollections": "Buscar colecciones..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Listas de reproducción",
|
||||
@@ -634,7 +657,6 @@
|
||||
"itemAdded": "Añadido a la lista",
|
||||
"itemRemoved": "Eliminado de la lista",
|
||||
"selectPlaylist": "Seleccionar Lista",
|
||||
"createNewPlaylist": "Crear Nueva Lista",
|
||||
"errorCreating": "Error al crear la lista",
|
||||
"errorDeleting": "Error al eliminar la lista",
|
||||
"errorLoading": "Error al cargar las listas",
|
||||
@@ -677,18 +699,30 @@
|
||||
"leave": "Salir",
|
||||
"syncing": "Sincronizando...",
|
||||
"joinWatchSession": "Unirse a Sesión de Visualización",
|
||||
"enterCodeHint": "Introduce el código de 8 caracteres",
|
||||
"enterCodeHint": "Introduce el código de 5 caracteres",
|
||||
"pasteFromClipboard": "Pegar desde el portapapeles",
|
||||
"pleaseEnterCode": "Por favor, introduce un código de sesión",
|
||||
"codeMustBe8Chars": "El código de sesión debe tener 8 caracteres",
|
||||
"codeMustBe5Chars": "El código de sesión debe tener 5 caracteres",
|
||||
"joinInstructions": "Introduce el código de sesión compartido por el anfitrión para unirte a su sesión.",
|
||||
"failedToCreate": "Error al crear la sesión",
|
||||
"failedToJoin": "Error al unirse a la sesión",
|
||||
"sessionCodeCopied": "Código de sesión copiado al portapapeles",
|
||||
"relayUnreachable": "El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar.",
|
||||
"reconnectingToHost": "Reconectando con el anfitrión...",
|
||||
"currentPlayback": "Reproducción actual",
|
||||
"joinCurrentPlayback": "Unirse a la reproducción actual",
|
||||
"joinCurrentPlaybackDescription": "Vuelve a lo que el anfitrión está viendo ahora mismo",
|
||||
"failedToOpenCurrentPlayback": "No se pudo abrir la reproducción actual",
|
||||
"participantJoined": "${name} se unió",
|
||||
"participantLeft": "${name} se fue"
|
||||
"participantLeft": "${name} se fue",
|
||||
"participantPaused": "${name} pausó",
|
||||
"participantResumed": "${name} reanudó",
|
||||
"participantSeeked": "${name} avanzó",
|
||||
"participantBuffering": "${name} está cargando",
|
||||
"waitingForParticipants": "Esperando a que otros carguen...",
|
||||
"recentRooms": "Salas recientes",
|
||||
"renameRoom": "Renombrar sala",
|
||||
"removeRoom": "Eliminar"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Descargas",
|
||||
@@ -701,6 +735,7 @@
|
||||
"deleteDownload": "Eliminar descarga",
|
||||
"retryDownload": "Reintentar descarga",
|
||||
"downloadQueued": "Descarga en cola",
|
||||
"serverErrorBitrate": "Error del servidor — el archivo puede exceder el límite de bitrate de transmisión remota",
|
||||
"episodesQueued": "${count} episodios en cola para descargar",
|
||||
"downloadDeleted": "Descarga eliminada",
|
||||
"deleteConfirm": "¿Estás seguro de que quieres eliminar \"${title}\"? Esto borrará el archivo descargado de tu dispositivo.",
|
||||
@@ -708,7 +743,32 @@
|
||||
"noDownloadsTree": "Sin descargas",
|
||||
"pauseAll": "Pausar todo",
|
||||
"resumeAll": "Reanudar todo",
|
||||
"deleteAll": "Eliminar todo"
|
||||
"deleteAll": "Eliminar todo",
|
||||
"selectVersion": "Seleccionar versión",
|
||||
"allEpisodes": "Todos los episodios",
|
||||
"unwatchedOnly": "Solo no vistos",
|
||||
"nextNUnwatched": "Próximos ${count} no vistos",
|
||||
"customAmount": "Cantidad personalizada...",
|
||||
"howManyEpisodes": "¿Cuántos episodios?",
|
||||
"itemsQueued": "${count} elementos en cola de descarga",
|
||||
"keepSynced": "Mantener sincronizado",
|
||||
"downloadOnce": "Descargar una vez",
|
||||
"keepNUnwatched": "Mantener ${count} sin ver",
|
||||
"editSyncRule": "Editar regla de sincronización",
|
||||
"removeSyncRule": "Eliminar regla de sincronización",
|
||||
"removeSyncRuleConfirm": "¿Dejar de sincronizar \"${title}\"? Los episodios descargados se conservarán.",
|
||||
"syncRuleCreated": "Regla de sincronización creada — conservando ${count} episodios sin ver",
|
||||
"syncRuleUpdated": "Regla de sincronización actualizada",
|
||||
"syncRuleRemoved": "Regla de sincronización eliminada",
|
||||
"syncedNewEpisodes": "${count} nuevos episodios sincronizados para ${title}",
|
||||
"activeSyncRules": "Reglas de sincronización",
|
||||
"noSyncRules": "Sin reglas de sincronización",
|
||||
"manageSyncRule": "Gestionar sincronización",
|
||||
"editEpisodeCount": "Número de episodios",
|
||||
"editSyncFilter": "Filtro de sincronización",
|
||||
"syncAllItems": "Sincronizando todos los elementos",
|
||||
"syncUnwatchedItems": "Sincronizando elementos no vistos",
|
||||
"syncRuleListCreated": "Regla de sincronización creada"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shaders",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "Escalado de imagen NVIDIA para un video más nítido",
|
||||
"qualityFast": "Rápido",
|
||||
"qualityHQ": "Alta Calidad",
|
||||
"mode": "Modo"
|
||||
"mode": "Modo",
|
||||
"importShader": "Importar shader",
|
||||
"customShaderDescription": "Shader GLSL personalizado",
|
||||
"shaderImported": "Shader importado",
|
||||
"shaderImportFailed": "Error al importar shader",
|
||||
"deleteShader": "Eliminar shader",
|
||||
"deleteShaderConfirm": "¿Eliminar \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "Conectar a dispositivo",
|
||||
"hostRemoteSession": "Iniciar sesión remota",
|
||||
"controlThisDevice": "Controla este dispositivo con tu teléfono",
|
||||
"remoteControl": "Control remoto",
|
||||
"controlDesktop": "Controlar un dispositivo de escritorio",
|
||||
"title": "Control remoto",
|
||||
"connectedTo": "Conectado a ${name}",
|
||||
"session": {
|
||||
"creatingSession": "Creando sesión remota...",
|
||||
"failedToCreate": "Error al crear la sesión remota:",
|
||||
"noSession": "No hay sesión disponible",
|
||||
"scanQrCode": "Escanear código QR",
|
||||
"orEnterManually": "O introducir manualmente",
|
||||
"startingServer": "Iniciando servidor remoto...",
|
||||
"failedToCreate": "Error al iniciar el servidor remoto:",
|
||||
"hostAddress": "Dirección del host",
|
||||
"sessionId": "ID de sesión",
|
||||
"pin": "PIN",
|
||||
"connected": "Conectado",
|
||||
"waitingForConnection": "Esperando conexión...",
|
||||
"usePhoneToControl": "Usa tu dispositivo móvil para controlar esta app",
|
||||
"copiedToClipboard": "${label} copiado al portapapeles",
|
||||
"copyToClipboard": "Copiar al portapapeles",
|
||||
"newSession": "Nueva sesión",
|
||||
"serverRunning": "Servidor remoto activo",
|
||||
"serverStopped": "Servidor remoto detenido",
|
||||
"serverRunningDescription": "Los dispositivos móviles en tu red pueden descubrir y conectarse a esta aplicación",
|
||||
"serverStoppedDescription": "Inicia el servidor para permitir que los dispositivos móviles se conecten",
|
||||
"usePhoneToControl": "Usa tu dispositivo móvil para controlar esta aplicación",
|
||||
"startServer": "Iniciar servidor",
|
||||
"stopServer": "Detener servidor",
|
||||
"minimize": "Minimizar"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "Escanear",
|
||||
"manual": "Manual",
|
||||
"pairWithDesktop": "Emparejar con escritorio",
|
||||
"enterSessionDetails": "Introduce los datos de la sesión que aparecen en tu dispositivo de escritorio",
|
||||
"discoveryDescription": "Los dispositivos en tu red que ejecutan Plezy con la misma cuenta de Plex aparecerán automáticamente",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "Introduce ID de sesión de 8 caracteres",
|
||||
"pinHint": "Introduce PIN de 6 dígitos",
|
||||
"connecting": "Conectando...",
|
||||
"tips": "Consejos",
|
||||
"tipDesktop": "Abre Plezy en tu escritorio y activa Companion Remote desde los ajustes o el menú",
|
||||
"tipScan": "Usa la pestaña Escanear para emparejar rápidamente escaneando el código QR de tu escritorio",
|
||||
"tipWifi": "Asegúrate de que ambos dispositivos estén en la misma red WiFi",
|
||||
"cameraPermissionRequired": "Se necesita permiso de cámara para escanear códigos QR.\nPor favor, concede acceso a la cámara en los ajustes de tu dispositivo.",
|
||||
"cameraError": "No se pudo iniciar la cámara: ${error}",
|
||||
"scanInstruction": "Apunta tu cámara al código QR que aparece en tu escritorio",
|
||||
"invalidQrCode": "Formato de código QR no válido",
|
||||
"validationHostRequired": "Por favor, introduce la dirección del host",
|
||||
"validationHostFormat": "El formato debe ser IP:puerto (ej., 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "Por favor, introduce un ID de sesión",
|
||||
"validationSessionIdLength": "El ID de sesión debe tener 8 caracteres",
|
||||
"validationPinRequired": "Por favor, introduce un PIN",
|
||||
"validationPinLength": "El PIN debe tener 6 dígitos",
|
||||
"connectionTimedOut": "Tiempo de conexión agotado. Verifica el ID de sesión y el PIN.",
|
||||
"sessionNotFound": "No se encontró la sesión. Verifica tus credenciales.",
|
||||
"searchingForDevices": "Buscando dispositivos...",
|
||||
"noDevicesFound": "No se encontraron dispositivos en tu red",
|
||||
"noDevicesHint": "Asegúrate de que Plezy esté abierto en tu escritorio y que ambos dispositivos estén en la misma red WiFi",
|
||||
"availableDevices": "Dispositivos disponibles",
|
||||
"manualConnection": "Conexión manual",
|
||||
"cryptoInitFailed": "No se pudo inicializar la conexión segura. Asegúrate de haber iniciado sesión en una cuenta de Plex.",
|
||||
"validationHostRequired": "Ingresa la dirección del host",
|
||||
"validationHostFormat": "El formato debe ser IP:puerto (ej. 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "Tiempo de conexión agotado. Asegúrate de que ambos dispositivos estén en la misma red.",
|
||||
"sessionNotFound": "No se encontró el dispositivo. Asegúrate de que Plezy esté ejecutándose en el host.",
|
||||
"authFailed": "Autenticación fallida. Asegúrate de que ambos dispositivos usen la misma cuenta de Plex.",
|
||||
"failedToConnect": "Error al conectar: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "Reconectando...",
|
||||
"attemptOf": "Intento ${current} de 5",
|
||||
"retryNow": "Reintentar ahora",
|
||||
"connectionError": "Error de conexión",
|
||||
"notConnected": "No conectado",
|
||||
"tabRemote": "Remoto",
|
||||
"tabPlay": "Reproducir",
|
||||
"tabMore": "Más",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "Usar reproductor externo",
|
||||
"useExternalPlayerDescription": "Abrir vídeos en una app externa en lugar del reproductor integrado",
|
||||
"selectPlayer": "Seleccionar reproductor",
|
||||
"customPlayers": "Reproductores personalizados",
|
||||
"systemDefault": "Predeterminado del sistema",
|
||||
"addCustomPlayer": "Añadir reproductor personalizado",
|
||||
"playerName": "Nombre del reproductor",
|
||||
"playerCommand": "Comando",
|
||||
"playerPackage": "Nombre del paquete",
|
||||
"playerUrlScheme": "Esquema URL",
|
||||
"customPlayer": "Reproductor personalizado",
|
||||
"off": "Desactivado",
|
||||
"launchFailed": "No se pudo abrir el reproductor externo",
|
||||
"appNotInstalled": "${name} no está instalado",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "Resumen",
|
||||
"poster": "Póster",
|
||||
"background": "Fondo",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Imagen cuadrada",
|
||||
"selectPoster": "Seleccionar póster",
|
||||
"selectBackground": "Seleccionar fondo",
|
||||
"selectLogo": "Seleccionar logo",
|
||||
"selectSquareArt": "Seleccionar imagen cuadrada",
|
||||
"fromUrl": "Desde URL",
|
||||
"uploadFile": "Subir archivo",
|
||||
"enterImageUrl": "Introducir URL de imagen",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "Selección automática de subtítulos",
|
||||
"manuallySelected": "Seleccionado manualmente",
|
||||
"shownWithForeignAudio": "Mostrar con audio extranjero",
|
||||
"alwaysEnabled": "Siempre activado"
|
||||
"alwaysEnabled": "Siempre activado",
|
||||
"tags": "Etiquetas",
|
||||
"addTag": "Añadir etiqueta",
|
||||
"genre": "Género",
|
||||
"director": "Director",
|
||||
"writer": "Guionista",
|
||||
"producer": "Productor",
|
||||
"country": "País",
|
||||
"collection": "Colección",
|
||||
"label": "Etiqueta",
|
||||
"style": "Estilo",
|
||||
"mood": "Estado de ánimo"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Tareas del servidor",
|
||||
"failedToLoad": "Error al cargar tareas",
|
||||
"noTasks": "No hay tareas en ejecución"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Conectado",
|
||||
"connectedAs": "Conectado como @${username}",
|
||||
"disconnectConfirm": "¿Desconectar cuenta de Trakt?",
|
||||
"disconnectConfirmBody": "Plezy dejará de enviar eventos de reproducción a Trakt. Puedes volver a conectar cuando quieras.",
|
||||
"scrobble": "Scrobbling en tiempo real",
|
||||
"scrobbleDescription": "Enviar eventos de reproducción, pausa y parada a Trakt durante la reproducción.",
|
||||
"watchedSync": "Sincronizar estado de visto",
|
||||
"watchedSyncDescription": "Cuando marques elementos como vistos en Plezy, también se marcarán en Trakt.",
|
||||
"deviceCodeTitle": "Activa Plezy en Trakt",
|
||||
"deviceCodeBody": "Visita ${url} e introduce este código:",
|
||||
"openTraktActivate": "Abrir Trakt para activar",
|
||||
"waitingForAuthorization": "Esperando autorización…",
|
||||
"codeCopied": "Código copiado",
|
||||
"connectFailed": "No se pudo conectar a Trakt. Inténtalo de nuevo."
|
||||
}
|
||||
}
|
||||
|
||||
+216
-133
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "S'inscrire avec Plex",
|
||||
"showQRCode": "Afficher le QR Code",
|
||||
"authenticate": "S'authentifier",
|
||||
"debugEnterToken": "Debug: Entrez votre token Plex",
|
||||
"plexTokenLabel": "Token d'authentification Plex",
|
||||
"plexTokenHint": "Entrez votre token Plex.tv",
|
||||
"authenticationTimeout": "Délai d'authentification expiré. Veuillez réessayer.",
|
||||
"scanQRToSignIn": "Scannez ce QR code pour vous connecter",
|
||||
"waitingForAuth": "En attente d'authentification...\nVeuillez vous connecter dans votre navigateur.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "Supprimer",
|
||||
"shuffle": "Mélanger",
|
||||
"addTo": "Ajouter à...",
|
||||
"remove": "Supprimer",
|
||||
"paste": "Coller",
|
||||
"createNew": "Créer",
|
||||
"connect": "Connecter",
|
||||
"disconnect": "Déconnecter",
|
||||
"play": "Lire",
|
||||
@@ -43,10 +39,9 @@
|
||||
"search": "Recherche",
|
||||
"home": "Accueil",
|
||||
"back": "Retour",
|
||||
"settings": "Paramètres",
|
||||
"settings": "Réglages",
|
||||
"mute": "Muet",
|
||||
"ok": "OK",
|
||||
"loading": "Chargement...",
|
||||
"reconnect": "Reconnecter",
|
||||
"exitConfirmTitle": "Quitter l'application ?",
|
||||
"exitConfirmMessage": "Êtes-vous sûr de vouloir quitter ?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "Actualisation des serveurs...",
|
||||
"loadingServers": "Chargement des serveurs...",
|
||||
"connectingToServers": "Connexion aux serveurs...",
|
||||
"startingOfflineMode": "Démarrage en mode hors-ligne..."
|
||||
"startingOfflineMode": "Démarrage en mode hors-ligne...",
|
||||
"loading": "Chargement..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licenses",
|
||||
"switchProfile": "Changer de profil",
|
||||
"subtitleStyling": "Configuration des sous-titres",
|
||||
"mpvConfig": "Configuration MPV",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"supportDeveloper": "Soutenir Plezy",
|
||||
"supportDeveloperDescription": "Faites un don via Liberapay pour financer le développement",
|
||||
"language": "Langue",
|
||||
"theme": "Thème",
|
||||
"appearance": "Apparence",
|
||||
"videoPlayback": "Lecture vidéo",
|
||||
"videoPlaybackDescription": "Configurer le comportement de lecture",
|
||||
"advanced": "Avancé",
|
||||
"episodePosterMode": "Style du Poster d'épisode",
|
||||
"seriesPoster": "Poster de série",
|
||||
"seriesPosterDescription": "Afficher le poster de série pour tous les épisodes",
|
||||
"seasonPoster": "Poster de saison",
|
||||
"seasonPosterDescription": "Afficher le poster spécifique à la saison pour les épisodes",
|
||||
"episodeThumbnail": "Mignature d'épisode",
|
||||
"episodeThumbnailDescription": "Afficher les vignettes des captures d'écran des épisodes au format 16:9",
|
||||
"episodeThumbnail": "Miniature",
|
||||
"showHeroSectionDescription": "Afficher le carrousel de contenu en vedette sur l'écran d'accueil",
|
||||
"secondsLabel": "Secondes",
|
||||
"minutesLabel": "Minutes",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Entrez la durée (${min}-${max})",
|
||||
"systemTheme": "Système",
|
||||
"systemThemeDescription": "Suivre les paramètres système",
|
||||
"lightTheme": "Light",
|
||||
"darkTheme": "Dark",
|
||||
"lightTheme": "Clair",
|
||||
"darkTheme": "Sombre",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "Noir pur pour les écrans OLED",
|
||||
"libraryDensity": "Densité des bibliothèques",
|
||||
"compact": "Compact",
|
||||
"compactDescription": "Cartes plus petites, plus d'éléments visibles",
|
||||
"normal": "Normal",
|
||||
"normalDescription": "Taille par défaut",
|
||||
"comfortable": "Confortable",
|
||||
"comfortableDescription": "Cartes plus grandes, moins d'éléments visibles",
|
||||
"viewMode": "Mode d'affichage",
|
||||
"gridView": "Grille",
|
||||
"gridViewDescription": "Afficher les éléments dans une disposition en grille",
|
||||
"listView": "Liste",
|
||||
"listViewDescription": "Afficher les éléments dans une liste",
|
||||
"showHeroSection": "Afficher la section Hero",
|
||||
"useGlobalHubs": "Utiliser la disposition Plex Home",
|
||||
"useGlobalHubsDescription": "Afficher les hubs de la page d'accueil comme le client Plex officiel. Lorsque cette option est désactivée, affiche à la place les recommandations par bibliothèque.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "Flouter les miniatures et masquer les descriptions des épisodes que vous n'avez pas encore regardés",
|
||||
"playerBackend": "Moteur de lecture",
|
||||
"exoPlayer": "ExoPlayer (Recommandé)",
|
||||
"exoPlayerDescription": "Lecteur natif Android avec meilleur support matériel",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "Lecteur avancé avec plus de fonctionnalités et support des sous-titres ASS",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Décodage matériel",
|
||||
"hardwareDecodingDescription": "Utilisez l'accélération matérielle lorsqu'elle est disponible.",
|
||||
"bufferSize": "Taille du Buffer",
|
||||
@@ -137,8 +123,9 @@
|
||||
"bufferSizeWarning": "Votre appareil dispose de ${heap}MB de mémoire. Un tampon de ${size}MB peut causer des problèmes de lecture.",
|
||||
"subtitleStyling": "Stylisation des sous-titres",
|
||||
"subtitleStylingDescription": "Personnaliser l'apparence des sous-titres",
|
||||
"smallSkipDuration": "Small Skip Duration",
|
||||
"largeSkipDuration": "Large Skip Duration",
|
||||
"smallSkipDuration": "Durée du petit saut",
|
||||
"largeSkipDuration": "Durée du grand saut",
|
||||
"rewindOnResume": "Rembobiner à la reprise",
|
||||
"secondsUnit": "${seconds} secondes",
|
||||
"defaultSleepTimer": "Minuterie de mise en veille par défaut",
|
||||
"minutesUnit": "${minutes} minutes",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "Personnaliser les raccourcis clavier",
|
||||
"videoPlayerNavigation": "Navigation dans le lecteur vidéo",
|
||||
"videoPlayerNavigationDescription": "Utilisez les touches fléchées pour naviguer dans les commandes du lecteur vidéo.",
|
||||
"watchTogetherRelay": "Relais Regarder Ensemble",
|
||||
"watchTogetherRelayDescription": "Définir un serveur relais personnalisé pour Regarder Ensemble. Tous les participants doivent utiliser le même serveur.",
|
||||
"watchTogetherRelayHint": "https://mon-relais.exemple.fr",
|
||||
"crashReporting": "Rapports de plantage",
|
||||
"crashReportingDescription": "Envoyer des rapports de plantage pour améliorer l'application",
|
||||
"debugLogging": "Journalisation de débogage",
|
||||
"debugLoggingDescription": "Activer la journalisation détaillée pour le dépannage",
|
||||
"viewLogs": "Voir les logs",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "Passer les crédits et passer à l'épisode suivant automatiquement",
|
||||
"autoSkipDelay": "Délai avant skip automatique",
|
||||
"autoSkipDelayDescription": "Attendre ${seconds} secondes avant l'auto-skip",
|
||||
"introPattern": "Modèle de marqueur d'intro",
|
||||
"introPatternDescription": "Expression régulière pour reconnaître les marqueurs d'intro dans les titres de chapitres",
|
||||
"creditsPattern": "Modèle de marqueur de générique",
|
||||
"creditsPatternDescription": "Expression régulière pour reconnaître les marqueurs de générique dans les titres de chapitres",
|
||||
"invalidRegex": "Expression régulière invalide",
|
||||
"downloads": "Téléchargement",
|
||||
"downloadLocationDescription": "Choisissez où stocker le contenu téléchargé",
|
||||
"downloadLocationDefault": "Par défaut (stockage de l'application)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "Échec de la sélection du dossier",
|
||||
"downloadOnWifiOnly": "Télécharger uniquement via WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Empêcher les téléchargements lorsque vous utilisez les données cellulaires",
|
||||
"autoRemoveWatchedDownloads": "Supprimer automatiquement les téléchargements vus",
|
||||
"autoRemoveWatchedDownloadsDescription": "Supprimer automatiquement les épisodes et films téléchargés lorsqu'ils sont marqués comme vus",
|
||||
"cellularDownloadBlocked": "Les téléchargements sont désactivés sur les données cellulaires. Connectez-vous au Wi-Fi ou modifiez le paramètre.",
|
||||
"maxVolume": "Volume maximal",
|
||||
"maxVolumeDescription": "Autoriser l'augmentation du volume au-delà de 100 % pour les médias silencieux",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Montrez ce que vous regardez sur Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Synchroniser l'historique de visionnage avec Trakt",
|
||||
"companionRemoteServer": "Serveur de télécommande",
|
||||
"companionRemoteServerDescription": "Autoriser les appareils mobiles de votre réseau à contrôler cette application",
|
||||
"autoPip": "Image dans l'image automatique",
|
||||
"autoPipDescription": "Activer automatiquement l'image dans l'image en quittant l'application pendant la lecture",
|
||||
"matchContentFrameRate": "Fréquence d'images du contenu correspondant",
|
||||
"matchContentFrameRateDescription": "Ajustez la fréquence de rafraîchissement de l'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "Adapter la fréquence de rafraîchissement",
|
||||
"matchRefreshRateDescription": "Changer la fréquence de rafraîchissement de l'écran pour correspondre au contenu vidéo en plein écran",
|
||||
"matchDynamicRange": "Adapter la plage dynamique",
|
||||
"matchDynamicRangeDescription": "Activer automatiquement le HDR pour le contenu HDR et revenir en SDR en quittant le lecteur",
|
||||
"displaySwitchDelay": "Délai de changement d'affichage",
|
||||
"tunneledPlayback": "Lecture tunnelée",
|
||||
"tunneledPlaybackDescription": "Utiliser le tunnelage vidéo accéléré par matériel. Désactiver si vous voyez un écran noir avec du son sur du contenu HDR",
|
||||
"requireProfileSelectionOnOpen": "Demander le profil à l'ouverture",
|
||||
"requireProfileSelectionOnOpenDescription": "Afficher la sélection de profil à chaque ouverture de l'application",
|
||||
"confirmExitOnBack": "Confirmer avant de quitter",
|
||||
"confirmExitOnBackDescription": "Afficher une boîte de dialogue de confirmation en appuyant sur retour pour quitter"
|
||||
"confirmExitOnBackDescription": "Afficher une boîte de dialogue de confirmation en appuyant sur retour pour quitter",
|
||||
"forceTvMode": "Forcer le mode TV",
|
||||
"forceTvModeDescription": "Utiliser la mise en page TV quelle que soit la détection automatique. Utile sur les appareils Android TV qui ne signalent pas la fonctionnalité leanback. Redémarre l'application lors du changement.",
|
||||
"autoHidePerformanceOverlay": "Masquer auto. superposition performances",
|
||||
"autoHidePerformanceOverlayDescription": "Faire apparaître/disparaître la superposition avec les contrôles de lecture",
|
||||
"showNavBarLabels": "Afficher les libellés de la barre de navigation",
|
||||
"showNavBarLabelsDescription": "Afficher les libellés sous les icônes de la barre de navigation",
|
||||
"liveTvDefaultFavorites": "Chaînes favorites par défaut",
|
||||
"liveTvDefaultFavoritesDescription": "Afficher uniquement les chaînes favorites à l'ouverture de la TV en direct",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Rechercher des films, des séries, de la musique...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "Passer l'intro/le générique"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "Entrer le code PIN",
|
||||
"showPin": "Afficher le code PIN",
|
||||
"hidePin": "Masquer le code PIN"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Informations sur le fichier",
|
||||
"video": "Vidéo",
|
||||
@@ -261,7 +280,9 @@
|
||||
"colorRange": "Gamme de couleurs",
|
||||
"colorPrimaries": "Couleurs primaires",
|
||||
"chromaSubsampling": "Sous-échantillonnage chromatique",
|
||||
"channels": "Channels",
|
||||
"channels": "Canaux",
|
||||
"subtitles": "Sous-titres",
|
||||
"overallBitrate": "Débit global",
|
||||
"path": "Chemin",
|
||||
"size": "Taille",
|
||||
"container": "Conteneur",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "Cela inclut tous les épisodes et leurs fichiers.",
|
||||
"mediaDeletedSuccessfully": "Élément média supprimé avec succès",
|
||||
"mediaFailedToDelete": "Échec de la suppression de l'élément média",
|
||||
"rate": "Noter"
|
||||
"rate": "Noter",
|
||||
"playFromBeginning": "Lire depuis le début",
|
||||
"playVersion": "Lire la version..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, film",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "Déverrouiller la rotation",
|
||||
"timerActive": "Minuterie active",
|
||||
"playbackWillPauseIn": "La lecture sera mise en pause dans ${duration}",
|
||||
"sleepTimerCompleted": "Minuterie de mise en veille terminée - lecture en pause",
|
||||
"stillWatching": "Toujours en train de regarder ?",
|
||||
"pausingIn": "Pause dans ${seconds}s",
|
||||
"continueWatching": "Continuer",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "Mute",
|
||||
"unmuteButton": "Dé-mute",
|
||||
"settingsButton": "Paramètres vidéo",
|
||||
"audioTrackButton": "Pistes audio",
|
||||
"subtitlesButton": "Sous-titres",
|
||||
"tracksButton": "Audio et sous-titres",
|
||||
"chaptersButton": "Chapitres",
|
||||
"versionsButton": "Versions vidéo",
|
||||
"pipButton": "Mode PiP (Picture-in-Picture)",
|
||||
"aspectRatioButton": "Format d'image",
|
||||
"ambientLighting": "Éclairage ambiant",
|
||||
"ambientLightingOn": "Activer l'éclairage ambiant",
|
||||
"ambientLightingOff": "Désactiver l'éclairage ambiant",
|
||||
"fullscreenButton": "Passer en mode plein écran",
|
||||
"exitFullscreenButton": "Quitter le mode plein écran",
|
||||
"alwaysOnTopButton": "Always on top",
|
||||
"alwaysOnTopButton": "Toujours au premier plan",
|
||||
"rotationLockButton": "Verrouillage de rotation",
|
||||
"lockScreen": "Verrouiller l'écran",
|
||||
"screenLockButton": "Verrouillage de l'écran",
|
||||
"longPressToUnlock": "Appui long pour déverrouiller",
|
||||
"timelineSlider": "Timeline vidéo",
|
||||
"volumeSlider": "Niveau sonore",
|
||||
"endsAt": "Fin à ${time}",
|
||||
"pipActive": "Lecture en mode image dans l'image",
|
||||
"pipFailed": "Échec du démarrage du mode image dans l'image",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Nécessite Android 8.0 ou plus récent",
|
||||
"iosVersion": "Nécessite iOS 15.0 ou plus récent",
|
||||
"permissionDisabled": "L'autorisation Image dans l'image est désactivée. Activez-la dans Paramètres > Applications > Plezy > Image dans l'image",
|
||||
"notSupported": "Cet appareil ne prend pas en charge le mode image dans l'image",
|
||||
"voSwitchFailed": "Échec du changement de sortie vidéo pour l'image dans l'image",
|
||||
"failed": "Échec du démarrage du mode image dans l'image",
|
||||
"unknown": "Une erreur s'est produite : ${error}"
|
||||
},
|
||||
"chapters": "Chapitres",
|
||||
"noChaptersAvailable": "Aucun chapitre disponible",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "File d'attente",
|
||||
"noQueueItems": "Aucun élément dans la file d'attente",
|
||||
"searchSubtitles": "Rechercher des sous-titres",
|
||||
"language": "Langue",
|
||||
"noSubtitlesFound": "Aucun sous-titre trouvé",
|
||||
"subtitleDownloaded": "Sous-titre téléchargé",
|
||||
"subtitleDownloadFailed": "Échec du téléchargement du sous-titre",
|
||||
"searchLanguages": "Rechercher des langues..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "Marqué comme non vu",
|
||||
"markedAsWatchedOffline": "Marqué comme vu (se synchronisera lorsque vous serez en ligne)",
|
||||
"markedAsUnwatchedOffline": "Marqué comme non vu (sera synchronisé lorsque vous serez en ligne)",
|
||||
"autoRemovedWatchedDownload": "Supprimé automatiquement : ${title}",
|
||||
"removedFromContinueWatching": "Supprimer de \"Continuer à regarder\"",
|
||||
"errorLoading": "Erreur: ${error}",
|
||||
"fileInfoNotAvailable": "Informations sur le fichier non disponibles",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "Échec de la création de la file d'attente de lecture - aucun élément",
|
||||
"failedPlayback": "Echec de ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Passage au lecteur compatible...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "Échec de la lecture",
|
||||
"serverLimitBody": "Le serveur a renvoyé une erreur (HTTP 500). Cela signifie généralement que le propriétaire du serveur a défini une limite de bande passante ou de transcodage qui rejette ta session. Rien à faire côté client — le propriétaire du serveur doit ajuster ses paramètres.",
|
||||
"logsUploaded": "Logs envoyés",
|
||||
"logsUploadFailed": "Échec de l'envoi des logs",
|
||||
"logId": "ID du log"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "Options de style",
|
||||
"text": "Texte",
|
||||
"border": "Bordure",
|
||||
"background": "Arrière-plan",
|
||||
"fontSize": "Taille de la police",
|
||||
"textColor": "Couleur du texte",
|
||||
"borderSize": "Taille de la bordure",
|
||||
"borderColor": "Couleur de la bordure",
|
||||
"backgroundOpacity": "Opacité d'arrière-plan",
|
||||
"backgroundColor": "Couleur d'arrière-plan",
|
||||
"position": "Position"
|
||||
"position": "Position",
|
||||
"assOverride": "Remplacement ASS",
|
||||
"bold": "Gras",
|
||||
"italic": "Italique"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "Configuration MPV",
|
||||
"title": "Configuration mpv",
|
||||
"description": "Paramètres avancés du lecteur vidéo",
|
||||
"properties": "Propriétés",
|
||||
"presets": "Préréglages",
|
||||
"noProperties": "Aucune propriété configurée",
|
||||
"noPresets": "Aucun préréglage enregistré",
|
||||
"addProperty": "Ajouter une propriété",
|
||||
"editProperty": "Modifier la propriété",
|
||||
"deleteProperty": "Supprimer la propriété",
|
||||
"propertyKey": "Clé",
|
||||
"propertyKeyHint": "e.g., hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Valeur",
|
||||
"propertyValueHint": "e.g., auto, 256000000",
|
||||
"saveAsPreset": "Enregistrer comme préréglage...",
|
||||
"presetName": "Nom du préréglage",
|
||||
"presetNameHint": "Entrez un nom pour ce préréglage",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "Préréglage chargé",
|
||||
"presetDeleted": "Préréglage supprimé",
|
||||
"confirmDeletePreset": "Êtes-vous sûr de vouloir supprimer ce préréglage ?",
|
||||
"confirmDeleteProperty": "Êtes-vous sûr de vouloir supprimer cette propriété ?",
|
||||
"entriesCount": "${count} entrées"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Confirmer l'action"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "Aperçu",
|
||||
"cast": "Cast",
|
||||
"extras": "Bandes-annonces et Extras",
|
||||
"seasons": "Saisons",
|
||||
"studio": "Studio",
|
||||
"rating": "Évaluation",
|
||||
"episodeCount": "${count} épisodes",
|
||||
"watchedProgress": "${watched}/${total} vu",
|
||||
"movie": "Film",
|
||||
"tvShow": "Show TV",
|
||||
"minutesLeft": "${minutes} min restantes"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "Playlists"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Regroupement",
|
||||
"all": "Tous",
|
||||
"movies": "Films",
|
||||
"shows": "Show TV",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "Effacer les logs",
|
||||
"copyLogs": "Copier les logs",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "Erreur:",
|
||||
"stackTrace": "Liste des appels:"
|
||||
"uploadLogs": "Envoyer les logs"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Package associés",
|
||||
@@ -559,26 +584,18 @@
|
||||
"licensesCount": "${count} licences"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Bibliothèques",
|
||||
"downloads": "Téléchargements",
|
||||
"liveTv": "TV en direct"
|
||||
"libraries": "Medias",
|
||||
"downloads": "Téléch.",
|
||||
"liveTv": "TV direct"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "TV en direct",
|
||||
"channels": "Chaînes",
|
||||
"guide": "Guide",
|
||||
"noChannels": "Aucune chaîne disponible",
|
||||
"noDvr": "Aucun DVR configuré sur les serveurs",
|
||||
"tuneFailed": "Impossible de syntoniser la chaîne",
|
||||
"loading": "Chargement des chaînes...",
|
||||
"nowPlaying": "En cours de lecture",
|
||||
"noPrograms": "Aucune donnée de programme disponible",
|
||||
"channelNumber": "Ch. ${number}",
|
||||
"live": "EN DIRECT",
|
||||
"hd": "HD",
|
||||
"premiere": "NOUVEAU",
|
||||
"reloadGuide": "Recharger le guide",
|
||||
"allChannels": "Toutes les chaînes",
|
||||
"now": "Maintenant",
|
||||
"today": "Aujourd'hui",
|
||||
"midnight": "Minuit",
|
||||
@@ -588,7 +605,13 @@
|
||||
"evening": "Soirée",
|
||||
"lateNight": "Nuit tardive",
|
||||
"whatsOn": "En ce moment",
|
||||
"watchChannel": "Regarder la chaîne"
|
||||
"watchChannel": "Regarder la chaîne",
|
||||
"favorites": "Favoris",
|
||||
"reorderFavorites": "Réorganiser les favoris",
|
||||
"joinSession": "Rejoindre la session en cours",
|
||||
"watchFromStart": "Regarder depuis le début (il y a ${minutes} min)",
|
||||
"watchLive": "Regarder en direct",
|
||||
"goToLive": "Aller au direct"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collections",
|
||||
@@ -602,7 +625,6 @@
|
||||
"deleteFailedWithError": "Échec de la suppression de la collection: ${error}",
|
||||
"failedToLoadItems": "Échec du chargement des éléments de la collection: ${error}",
|
||||
"selectCollection": "Sélectionner une collection",
|
||||
"createNewCollection": "Créer une nouvelle collection",
|
||||
"collectionName": "Nom de la collection",
|
||||
"enterCollectionName": "Entrez le nom de la collection",
|
||||
"addedToCollection": "Ajouté à la collection",
|
||||
@@ -612,7 +634,8 @@
|
||||
"removeFromCollectionConfirm": "Retirer \"${title}\" de cette collection ?",
|
||||
"removedFromCollection": "Retiré de la collection",
|
||||
"removeFromCollectionFailed": "Impossible de supprimer de la collection",
|
||||
"removeFromCollectionError": "Erreur lors de la suppression de la collection: ${error}"
|
||||
"removeFromCollectionError": "Erreur lors de la suppression de la collection: ${error}",
|
||||
"searchCollections": "Rechercher des collections..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Playlists",
|
||||
@@ -633,8 +656,7 @@
|
||||
"deleted": "Playlist supprimée",
|
||||
"itemAdded": "Ajouté à la playlist",
|
||||
"itemRemoved": "Retiré de la playlist",
|
||||
"selectPlaylist": "Select Playlist",
|
||||
"createNewPlaylist": "Créer une nouvelle playlist",
|
||||
"selectPlaylist": "Sélectionner une playlist",
|
||||
"errorCreating": "Échec de la création de playlist",
|
||||
"errorDeleting": "Échec de suppression de playlist",
|
||||
"errorLoading": "Échec de chargement de playlists",
|
||||
@@ -677,18 +699,30 @@
|
||||
"leave": "Fin",
|
||||
"syncing": "Synchronisation...",
|
||||
"joinWatchSession": "Rejoindre la session de visionnage",
|
||||
"enterCodeHint": "Entrez le code à 8 caractères",
|
||||
"enterCodeHint": "Entrez le code à 5 caractères",
|
||||
"pasteFromClipboard": "Coller depuis le presse-papiers",
|
||||
"pleaseEnterCode": "Veuillez saisir un code de session",
|
||||
"codeMustBe8Chars": "Le code de session doit comporter 8 caractères",
|
||||
"codeMustBe5Chars": "Le code de session doit comporter 5 caractères",
|
||||
"joinInstructions": "Entrez le code de session partagé par l'hôte pour rejoindre sa session de visionnage.",
|
||||
"failedToCreate": "Échec de la création de la session",
|
||||
"failedToJoin": "Échec de la connexion à la session",
|
||||
"sessionCodeCopied": "Code de session copié dans le presse-papiers",
|
||||
"relayUnreachable": "Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner.",
|
||||
"reconnectingToHost": "Reconnexion à l'hôte...",
|
||||
"currentPlayback": "Lecture en cours",
|
||||
"joinCurrentPlayback": "Rejoindre la lecture en cours",
|
||||
"joinCurrentPlaybackDescription": "Revenez à ce que l'hôte regarde actuellement",
|
||||
"failedToOpenCurrentPlayback": "Impossible d'ouvrir la lecture en cours",
|
||||
"participantJoined": "${name} a rejoint",
|
||||
"participantLeft": "${name} est parti"
|
||||
"participantLeft": "${name} est parti",
|
||||
"participantPaused": "${name} a mis en pause",
|
||||
"participantResumed": "${name} a repris",
|
||||
"participantSeeked": "${name} a avancé",
|
||||
"participantBuffering": "${name} met en mémoire tampon",
|
||||
"waitingForParticipants": "En attente du chargement des autres...",
|
||||
"recentRooms": "Salons récents",
|
||||
"renameRoom": "Renommer le salon",
|
||||
"removeRoom": "Supprimer"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Téléchargements",
|
||||
@@ -701,6 +735,7 @@
|
||||
"deleteDownload": "Supprimer le téléchargement",
|
||||
"retryDownload": "Réessayer le téléchargement",
|
||||
"downloadQueued": "Téléchargement en attente",
|
||||
"serverErrorBitrate": "Erreur serveur — le fichier dépasse peut-être la limite de débit du streaming à distance",
|
||||
"episodesQueued": "${count} épisodes en attente de téléchargement",
|
||||
"downloadDeleted": "Télécharger supprimé",
|
||||
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer \"${title}\" ? Cela supprimera le fichier téléchargé de votre appareil.",
|
||||
@@ -708,7 +743,32 @@
|
||||
"noDownloadsTree": "Aucun téléchargement",
|
||||
"pauseAll": "Tout mettre en pause",
|
||||
"resumeAll": "Tout reprendre",
|
||||
"deleteAll": "Tout supprimer"
|
||||
"deleteAll": "Tout supprimer",
|
||||
"selectVersion": "Sélectionner la version",
|
||||
"allEpisodes": "Tous les épisodes",
|
||||
"unwatchedOnly": "Non vus uniquement",
|
||||
"nextNUnwatched": "${count} prochains non vus",
|
||||
"customAmount": "Quantité personnalisée...",
|
||||
"howManyEpisodes": "Combien d'épisodes ?",
|
||||
"itemsQueued": "${count} éléments mis en file d'attente",
|
||||
"keepSynced": "Garder synchronisé",
|
||||
"downloadOnce": "Télécharger une fois",
|
||||
"keepNUnwatched": "Garder ${count} non vus",
|
||||
"editSyncRule": "Modifier la règle de synchronisation",
|
||||
"removeSyncRule": "Supprimer la règle de synchronisation",
|
||||
"removeSyncRuleConfirm": "Arrêter la synchronisation de « ${title} » ? Les épisodes téléchargés seront conservés.",
|
||||
"syncRuleCreated": "Règle de synchronisation créée — ${count} épisodes non vus conservés",
|
||||
"syncRuleUpdated": "Règle de synchronisation mise à jour",
|
||||
"syncRuleRemoved": "Règle de synchronisation supprimée",
|
||||
"syncedNewEpisodes": "${count} nouveaux épisodes synchronisés pour ${title}",
|
||||
"activeSyncRules": "Règles de synchronisation",
|
||||
"noSyncRules": "Aucune règle de synchronisation",
|
||||
"manageSyncRule": "Gérer la synchronisation",
|
||||
"editEpisodeCount": "Nombre d’épisodes",
|
||||
"editSyncFilter": "Filtre de synchronisation",
|
||||
"syncAllItems": "Synchronisation de tous les éléments",
|
||||
"syncUnwatchedItems": "Synchronisation des éléments non vus",
|
||||
"syncRuleListCreated": "Règle de synchronisation créée"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shaders",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "Mise à l'échelle NVIDIA pour une vidéo plus nette",
|
||||
"qualityFast": "Rapide",
|
||||
"qualityHQ": "Haute qualité",
|
||||
"mode": "Mode"
|
||||
"mode": "Mode",
|
||||
"importShader": "Importer un shader",
|
||||
"customShaderDescription": "Shader GLSL personnalisé",
|
||||
"shaderImported": "Shader importé",
|
||||
"shaderImportFailed": "Échec de l'importation du shader",
|
||||
"deleteShader": "Supprimer le shader",
|
||||
"deleteShaderConfirm": "Supprimer \"${name}\" ?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "Se connecter à un appareil",
|
||||
"hostRemoteSession": "Héberger une session distante",
|
||||
"controlThisDevice": "Contrôlez cet appareil avec votre téléphone",
|
||||
"remoteControl": "Télécommande",
|
||||
"controlDesktop": "Contrôler un appareil de bureau",
|
||||
"title": "Télécommande compagnon",
|
||||
"connectedTo": "Connecté à ${name}",
|
||||
"session": {
|
||||
"creatingSession": "Création de la session distante...",
|
||||
"failedToCreate": "Échec de la création de la session distante :",
|
||||
"noSession": "Aucune session disponible",
|
||||
"scanQrCode": "Scanner le QR Code",
|
||||
"orEnterManually": "Ou saisir manuellement",
|
||||
"startingServer": "Démarrage du serveur distant...",
|
||||
"failedToCreate": "Échec du démarrage du serveur distant :",
|
||||
"hostAddress": "Adresse de l'hôte",
|
||||
"sessionId": "ID de session",
|
||||
"pin": "PIN",
|
||||
"connected": "Connecté",
|
||||
"waitingForConnection": "En attente de connexion...",
|
||||
"serverRunning": "Serveur distant actif",
|
||||
"serverStopped": "Serveur distant arrêté",
|
||||
"serverRunningDescription": "Les appareils mobiles de votre réseau peuvent découvrir et se connecter à cette application",
|
||||
"serverStoppedDescription": "Démarrez le serveur pour permettre aux appareils mobiles de se connecter",
|
||||
"usePhoneToControl": "Utilisez votre appareil mobile pour contrôler cette application",
|
||||
"copiedToClipboard": "${label} copié dans le presse-papiers",
|
||||
"copyToClipboard": "Copier dans le presse-papiers",
|
||||
"newSession": "Nouvelle session",
|
||||
"startServer": "Démarrer le serveur",
|
||||
"stopServer": "Arrêter le serveur",
|
||||
"minimize": "Réduire"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "Scanner",
|
||||
"manual": "Manuel",
|
||||
"pairWithDesktop": "Jumeler avec un bureau",
|
||||
"enterSessionDetails": "Saisissez les détails de la session affichés sur votre appareil de bureau",
|
||||
"discoveryDescription": "Les appareils de votre réseau exécutant Plezy avec le même compte Plex apparaîtront automatiquement",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "Saisissez l'ID de session à 8 caractères",
|
||||
"pinHint": "Saisissez le PIN à 6 chiffres",
|
||||
"connecting": "Connexion...",
|
||||
"tips": "Conseils",
|
||||
"tipDesktop": "Ouvrez Plezy sur votre bureau et activez Companion Remote depuis les paramètres ou le menu",
|
||||
"tipScan": "Utilisez l'onglet Scanner pour jumeler rapidement en scannant le QR code sur votre bureau",
|
||||
"tipWifi": "Assurez-vous que les deux appareils sont sur le même réseau WiFi",
|
||||
"cameraPermissionRequired": "L'autorisation de la caméra est requise pour scanner les QR codes.\nVeuillez accorder l'accès à la caméra dans les paramètres de votre appareil.",
|
||||
"cameraError": "Impossible de démarrer la caméra : ${error}",
|
||||
"scanInstruction": "Pointez votre caméra vers le QR code affiché sur votre bureau",
|
||||
"invalidQrCode": "Format de QR code invalide",
|
||||
"validationHostRequired": "Veuillez saisir l'adresse de l'hôte",
|
||||
"validationHostFormat": "Le format doit être IP:port (ex : 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "Veuillez saisir un ID de session",
|
||||
"validationSessionIdLength": "L'ID de session doit contenir 8 caractères",
|
||||
"validationPinRequired": "Veuillez saisir un PIN",
|
||||
"validationPinLength": "Le PIN doit contenir 6 chiffres",
|
||||
"connectionTimedOut": "Délai de connexion expiré. Veuillez vérifier l'ID de session et le PIN.",
|
||||
"sessionNotFound": "Session introuvable. Veuillez vérifier vos identifiants.",
|
||||
"searchingForDevices": "Recherche d'appareils...",
|
||||
"noDevicesFound": "Aucun appareil trouvé sur votre réseau",
|
||||
"noDevicesHint": "Assurez-vous que Plezy est ouvert sur votre bureau et que les deux appareils sont sur le même réseau WiFi",
|
||||
"availableDevices": "Appareils disponibles",
|
||||
"manualConnection": "Connexion manuelle",
|
||||
"cryptoInitFailed": "Impossible d'initialiser la connexion sécurisée. Assurez-vous d'être connecté à un compte Plex.",
|
||||
"validationHostRequired": "Veuillez entrer l'adresse de l'hôte",
|
||||
"validationHostFormat": "Le format doit être IP:port (ex. 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "Délai de connexion dépassé. Assurez-vous que les deux appareils sont sur le même réseau.",
|
||||
"sessionNotFound": "Appareil introuvable. Assurez-vous que Plezy est en cours d'exécution sur l'hôte.",
|
||||
"authFailed": "Échec de l'authentification. Assurez-vous que les deux appareils utilisent le même compte Plex.",
|
||||
"failedToConnect": "Échec de la connexion : ${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "Reconnexion...",
|
||||
"attemptOf": "Tentative ${current} sur 5",
|
||||
"retryNow": "Réessayer maintenant",
|
||||
"connectionError": "Erreur de connexion",
|
||||
"notConnected": "Non connecté",
|
||||
"tabRemote": "Télécommande",
|
||||
"tabPlay": "Lecture",
|
||||
"tabMore": "Plus",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "Utiliser un lecteur externe",
|
||||
"useExternalPlayerDescription": "Ouvrir les vidéos dans une application externe au lieu du lecteur intégré",
|
||||
"selectPlayer": "Sélectionner le lecteur",
|
||||
"customPlayers": "Lecteurs personnalisés",
|
||||
"systemDefault": "Par défaut du système",
|
||||
"addCustomPlayer": "Ajouter un lecteur personnalisé",
|
||||
"playerName": "Nom du lecteur",
|
||||
"playerCommand": "Commande",
|
||||
"playerPackage": "Nom du paquet",
|
||||
"playerUrlScheme": "Schéma URL",
|
||||
"customPlayer": "Lecteur personnalisé",
|
||||
"off": "Désactivé",
|
||||
"launchFailed": "Impossible d'ouvrir le lecteur externe",
|
||||
"appNotInstalled": "${name} n'est pas installé",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "Résumé",
|
||||
"poster": "Affiche",
|
||||
"background": "Arrière-plan",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Image carrée",
|
||||
"selectPoster": "Sélectionner l'affiche",
|
||||
"selectBackground": "Sélectionner l'arrière-plan",
|
||||
"selectLogo": "Sélectionner le logo",
|
||||
"selectSquareArt": "Sélectionner l'image carrée",
|
||||
"fromUrl": "Depuis une URL",
|
||||
"uploadFile": "Importer un fichier",
|
||||
"enterImageUrl": "Entrer l'URL de l'image",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "Sélection automatique des sous-titres",
|
||||
"manuallySelected": "Sélectionné manuellement",
|
||||
"shownWithForeignAudio": "Affichés avec audio étranger",
|
||||
"alwaysEnabled": "Toujours activé"
|
||||
"alwaysEnabled": "Toujours activé",
|
||||
"tags": "Tags",
|
||||
"addTag": "Ajouter un tag",
|
||||
"genre": "Genre",
|
||||
"director": "Réalisateur",
|
||||
"writer": "Scénariste",
|
||||
"producer": "Producteur",
|
||||
"country": "Pays",
|
||||
"collection": "Collection",
|
||||
"label": "Label",
|
||||
"style": "Style",
|
||||
"mood": "Ambiance"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Tâches du serveur",
|
||||
"failedToLoad": "Échec du chargement des tâches",
|
||||
"noTasks": "Aucune tâche en cours"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Connecté",
|
||||
"connectedAs": "Connecté en tant que @${username}",
|
||||
"disconnectConfirm": "Déconnecter le compte Trakt ?",
|
||||
"disconnectConfirmBody": "Plezy n'enverra plus d'événements de lecture à Trakt. Vous pouvez vous reconnecter à tout moment.",
|
||||
"scrobble": "Scrobbling en temps réel",
|
||||
"scrobbleDescription": "Envoyer les événements de lecture, pause et arrêt à Trakt pendant la lecture.",
|
||||
"watchedSync": "Synchroniser le statut « vu »",
|
||||
"watchedSyncDescription": "Lorsque vous marquez un élément comme vu dans Plezy, il l'est aussi sur Trakt.",
|
||||
"deviceCodeTitle": "Activer Plezy sur Trakt",
|
||||
"deviceCodeBody": "Rendez-vous sur ${url} et entrez ce code :",
|
||||
"openTraktActivate": "Ouvrir Trakt pour activer",
|
||||
"waitingForAuthorization": "En attente d'autorisation…",
|
||||
"codeCopied": "Code copié",
|
||||
"connectFailed": "Échec de la connexion à Trakt. Réessayez."
|
||||
}
|
||||
}
|
||||
|
||||
+247
-164
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "Accedi con Plex",
|
||||
"showQRCode": "Mostra QR Code",
|
||||
"authenticate": "Autenticazione",
|
||||
"debugEnterToken": "Debug: Inserisci Token Plex",
|
||||
"plexTokenLabel": "Token Auth Plex",
|
||||
"plexTokenHint": "Inserisci il tuo token di Plex.tv",
|
||||
"authenticationTimeout": "Autenticazione scaduta. Riprova.",
|
||||
"scanQRToSignIn": "Scansiona il QR code per accedere",
|
||||
"waitingForAuth": "In attesa di autenticazione...\nCompleta l'accesso dal tuo browser.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "Elimina",
|
||||
"shuffle": "Casuale",
|
||||
"addTo": "Aggiungi a...",
|
||||
"remove": "Rimuovi",
|
||||
"paste": "Incolla",
|
||||
"createNew": "Crea",
|
||||
"connect": "Connetti",
|
||||
"disconnect": "Disconnetti",
|
||||
"play": "Riproduci",
|
||||
@@ -43,10 +39,9 @@
|
||||
"search": "Cerca",
|
||||
"home": "Home",
|
||||
"back": "Indietro",
|
||||
"settings": "Impostazioni",
|
||||
"settings": "Opzioni",
|
||||
"mute": "Muto",
|
||||
"ok": "OK",
|
||||
"loading": "Caricamento...",
|
||||
"reconnect": "Riconnetti",
|
||||
"exitConfirmTitle": "Uscire dall'app?",
|
||||
"exitConfirmMessage": "Sei sicuro di voler uscire?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "Aggiornamento server...",
|
||||
"loadingServers": "Caricamento server...",
|
||||
"connectingToServers": "Connessione ai server...",
|
||||
"startingOfflineMode": "Avvio modalità offline..."
|
||||
"startingOfflineMode": "Avvio modalità offline...",
|
||||
"loading": "Caricamento..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licenze",
|
||||
"switchProfile": "Cambia profilo",
|
||||
"subtitleStyling": "Stile sottotitoli",
|
||||
"mpvConfig": "Configurazione MPV",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Registro"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
"supportDeveloper": "Supporta Plezy",
|
||||
"supportDeveloperDescription": "Dona tramite Liberapay per finanziare lo sviluppo",
|
||||
"language": "Lingua",
|
||||
"theme": "Tema",
|
||||
"appearance": "Aspetto",
|
||||
"videoPlayback": "Riproduzione video",
|
||||
"videoPlaybackDescription": "Configura il comportamento di riproduzione",
|
||||
"advanced": "Avanzate",
|
||||
"episodePosterMode": "Stile poster episodio",
|
||||
"seriesPoster": "Poster della serie",
|
||||
"seriesPosterDescription": "Mostra il poster della serie per tutti gli episodi",
|
||||
"seasonPoster": "Poster della stagione",
|
||||
"seasonPosterDescription": "Mostra il poster specifico della stagione per gli episodi",
|
||||
"episodeThumbnail": "Miniatura episodio",
|
||||
"episodeThumbnailDescription": "Mostra miniature 16:9 degli episodi",
|
||||
"episodeThumbnail": "Miniatura",
|
||||
"showHeroSectionDescription": "Visualizza il carosello dei contenuti in primo piano sulla schermata iniziale",
|
||||
"secondsLabel": "Secondi",
|
||||
"minutesLabel": "Minuti",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Inserisci durata (${min}-${max})",
|
||||
"systemTheme": "Sistema",
|
||||
"systemThemeDescription": "Segui le impostazioni di sistema",
|
||||
"lightTheme": "Chiaro",
|
||||
"darkTheme": "Scuro",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "Nero puro per schermi OLED",
|
||||
"libraryDensity": "Densità libreria",
|
||||
"compact": "Compatta",
|
||||
"compactDescription": "Schede più piccole, più elementi visibili",
|
||||
"normal": "Normale",
|
||||
"normalDescription": "Dimensione predefinita",
|
||||
"comfortable": "Comoda",
|
||||
"comfortableDescription": "Schede più grandi, meno elementi visibili",
|
||||
"viewMode": "Modalità di visualizzazione",
|
||||
"gridView": "Griglia",
|
||||
"gridViewDescription": "Visualizza gli elementi in un layout a griglia",
|
||||
"listView": "Elenco",
|
||||
"listViewDescription": "Visualizza gli elementi in un layout a elenco",
|
||||
"showHeroSection": "Mostra sezione principale",
|
||||
"useGlobalHubs": "Usa layout Home di Plex",
|
||||
"useGlobalHubsDescription": "Mostra gli hub della home page come il client Plex ufficiale. Se disattivato, mostra invece i suggerimenti per libreria.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "Sfoca le miniature e nascondi le descrizioni degli episodi che non hai ancora guardato",
|
||||
"playerBackend": "Motore di riproduzione",
|
||||
"exoPlayer": "ExoPlayer (Consigliato)",
|
||||
"exoPlayerDescription": "Lettore nativo Android con migliore supporto hardware",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "Lettore avanzato con più funzionalità e supporto sottotitoli ASS",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Decodifica Hardware",
|
||||
"hardwareDecodingDescription": "Utilizza l'accelerazione hardware quando disponibile",
|
||||
"bufferSize": "Dimensione buffer",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "Personalizza l'aspetto dei sottotitoli",
|
||||
"smallSkipDuration": "Durata skip breve",
|
||||
"largeSkipDuration": "Durata skip lungo",
|
||||
"rewindOnResume": "Riavvolgi alla ripresa",
|
||||
"secondsUnit": "${seconds} secondi",
|
||||
"defaultSleepTimer": "Timer spegnimento predefinito",
|
||||
"minutesUnit": "${minutes} minuti",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "Personalizza le scorciatoie da tastiera",
|
||||
"videoPlayerNavigation": "Navigazione del lettore video",
|
||||
"videoPlayerNavigationDescription": "Usa i tasti freccia per navigare nei controlli del lettore video",
|
||||
"watchTogetherRelay": "Relay Guarda Insieme",
|
||||
"watchTogetherRelayDescription": "Imposta un server relay personalizzato per Guarda Insieme. Tutti i partecipanti devono utilizzare lo stesso server.",
|
||||
"watchTogetherRelayHint": "https://mio-relay.esempio.it",
|
||||
"crashReporting": "Segnalazione errori",
|
||||
"crashReportingDescription": "Invia segnalazioni di errori per migliorare l'app",
|
||||
"debugLogging": "Log di debug",
|
||||
"debugLoggingDescription": "Abilita il logging dettagliato per la risoluzione dei problemi",
|
||||
"viewLogs": "Visualizza log",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "Salta automaticamente i crediti e riproduci l'episodio successivo",
|
||||
"autoSkipDelay": "Ritardo Salto Automatico",
|
||||
"autoSkipDelayDescription": "Aspetta ${seconds} secondi prima del salto automatico",
|
||||
"introPattern": "Pattern marcatore intro",
|
||||
"introPatternDescription": "Espressione regolare per riconoscere i marcatori intro nei titoli dei capitoli",
|
||||
"creditsPattern": "Pattern marcatore titoli di coda",
|
||||
"creditsPatternDescription": "Espressione regolare per riconoscere i marcatori dei titoli di coda nei capitoli",
|
||||
"invalidRegex": "Espressione regolare non valida",
|
||||
"downloads": "Download",
|
||||
"downloadLocationDescription": "Scegli dove salvare i contenuti scaricati",
|
||||
"downloadLocationDefault": "Predefinita (Archiviazione App)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "Impossibile selezionare la cartella",
|
||||
"downloadOnWifiOnly": "Scarica solo con WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Impedisci i download quando si utilizza la rete dati cellulare",
|
||||
"autoRemoveWatchedDownloads": "Rimuovi automaticamente i download visti",
|
||||
"autoRemoveWatchedDownloadsDescription": "Elimina automaticamente episodi e film scaricati quando vengono segnati come visti",
|
||||
"cellularDownloadBlocked": "I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l'impostazione.",
|
||||
"maxVolume": "Volume massimo",
|
||||
"maxVolumeDescription": "Consenti volume superiore al 100% per contenuti audio bassi",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Mostra su Discord cosa stai guardando",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Sincronizza la cronologia di visione con Trakt",
|
||||
"companionRemoteServer": "Server telecomando",
|
||||
"companionRemoteServerDescription": "Consenti ai dispositivi mobili nella tua rete di controllare questa app",
|
||||
"autoPip": "Picture-in-Picture automatico",
|
||||
"autoPipDescription": "Attiva automaticamente il picture-in-picture quando si esce dall'app durante la riproduzione",
|
||||
"matchContentFrameRate": "Adatta frequenza fotogrammi",
|
||||
"matchContentFrameRateDescription": "Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "Adatta frequenza di aggiornamento",
|
||||
"matchRefreshRateDescription": "Cambia la frequenza di aggiornamento del display per adattarla al contenuto video a schermo intero",
|
||||
"matchDynamicRange": "Adatta gamma dinamica",
|
||||
"matchDynamicRangeDescription": "Attiva automaticamente l'HDR per i contenuti HDR e torna in SDR all'uscita dal lettore",
|
||||
"displaySwitchDelay": "Ritardo cambio display",
|
||||
"tunneledPlayback": "Riproduzione tunnelizzata",
|
||||
"tunneledPlaybackDescription": "Usa il tunneling video accelerato dall'hardware. Disattiva se vedi uno schermo nero con audio su contenuti HDR",
|
||||
"requireProfileSelectionOnOpen": "Chiedi profilo all'apertura",
|
||||
"requireProfileSelectionOnOpenDescription": "Mostra la selezione del profilo ogni volta che l'app viene aperta",
|
||||
"confirmExitOnBack": "Conferma prima di uscire",
|
||||
"confirmExitOnBackDescription": "Mostra una finestra di conferma quando si preme indietro per uscire dall'app"
|
||||
"confirmExitOnBackDescription": "Mostra una finestra di conferma quando si preme indietro per uscire dall'app",
|
||||
"forceTvMode": "Forza modalità TV",
|
||||
"forceTvModeDescription": "Usa il layout TV indipendentemente dal rilevamento automatico. Utile sui dispositivi Android TV che non segnalano la funzionalità leanback. Riavvia l'app al cambiamento.",
|
||||
"autoHidePerformanceOverlay": "Nascondi automaticamente overlay prestazioni",
|
||||
"autoHidePerformanceOverlayDescription": "Dissolvi l'overlay prestazioni con i controlli di riproduzione",
|
||||
"showNavBarLabels": "Mostra etichette barra di navigazione",
|
||||
"showNavBarLabelsDescription": "Mostra le etichette sotto le icone della barra di navigazione",
|
||||
"liveTvDefaultFavorites": "Canali preferiti predefiniti",
|
||||
"liveTvDefaultFavoritesDescription": "Mostra solo i canali preferiti all'apertura della TV dal vivo",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Cerca film. spettacoli, musica...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "Salta intro/titoli di coda"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "Inserisci PIN",
|
||||
"showPin": "Mostra PIN",
|
||||
"hidePin": "Nascondi PIN"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Info sul file",
|
||||
"video": "Video",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "Colori primari",
|
||||
"chromaSubsampling": "Sottocampionamento cromatico",
|
||||
"channels": "Canali",
|
||||
"subtitles": "Sottotitoli",
|
||||
"overallBitrate": "Bitrate complessivo",
|
||||
"path": "Percorso",
|
||||
"size": "Dimensione",
|
||||
"container": "Contenitore",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "Questo include tutti gli episodi e i loro file.",
|
||||
"mediaDeletedSuccessfully": "Elemento multimediale eliminato con successo",
|
||||
"mediaFailedToDelete": "Impossibile eliminare l'elemento multimediale",
|
||||
"rate": "Valuta"
|
||||
"rate": "Valuta",
|
||||
"playFromBeginning": "Riproduci dall'inizio",
|
||||
"playVersion": "Riproduci versione..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, film",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "Sblocca rotazione",
|
||||
"timerActive": "Timer attivo",
|
||||
"playbackWillPauseIn": "La riproduzione si interromperà tra ${duration}",
|
||||
"sleepTimerCompleted": "Timer di spegnimento completato - riproduzione in pausa",
|
||||
"stillWatching": "Stai ancora guardando?",
|
||||
"pausingIn": "Pausa tra ${seconds}s",
|
||||
"continueWatching": "Continua",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "Silenzia",
|
||||
"unmuteButton": "Riattiva audio",
|
||||
"settingsButton": "Impostazioni video",
|
||||
"audioTrackButton": "Tracce audio",
|
||||
"subtitlesButton": "Sottotitoli",
|
||||
"tracksButton": "Audio e sottotitoli",
|
||||
"chaptersButton": "Capitoli",
|
||||
"versionsButton": "Versioni video",
|
||||
"pipButton": "Modalità Picture-in-Picture",
|
||||
"aspectRatioButton": "Proporzioni",
|
||||
"ambientLighting": "Illuminazione ambientale",
|
||||
"ambientLightingOn": "Attiva illuminazione ambientale",
|
||||
"ambientLightingOff": "Disattiva illuminazione ambientale",
|
||||
"fullscreenButton": "Attiva schermo intero",
|
||||
"exitFullscreenButton": "Esci da schermo intero",
|
||||
"alwaysOnTopButton": "Sempre in primo piano",
|
||||
"rotationLockButton": "Blocco rotazione",
|
||||
"lockScreen": "Blocca schermo",
|
||||
"screenLockButton": "Blocco schermo",
|
||||
"longPressToUnlock": "Premi a lungo per sbloccare",
|
||||
"timelineSlider": "Timeline video",
|
||||
"volumeSlider": "Livello volume",
|
||||
"endsAt": "Finisce alle ${time}",
|
||||
"pipActive": "Riproduzione in Picture-in-Picture",
|
||||
"pipFailed": "Impossibile avviare la modalità Picture-in-Picture",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Richiede Android 8.0 o versioni successive",
|
||||
"iosVersion": "Richiede iOS 15.0 o versioni successive",
|
||||
"permissionDisabled": "L'autorizzazione Picture-in-Picture è disabilitata. Abilitala in Impostazioni > App > Plezy > Picture-in-Picture",
|
||||
"notSupported": "Questo dispositivo non supporta la modalità Picture-in-Picture",
|
||||
"voSwitchFailed": "Impossibile cambiare l'uscita video per Picture-in-Picture",
|
||||
"failed": "Impossibile avviare la modalità Picture-in-Picture",
|
||||
"unknown": "Si è verificato un errore: ${error}"
|
||||
},
|
||||
"chapters": "Capitoli",
|
||||
"noChaptersAvailable": "Nessun capitolo disponibile",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "Coda",
|
||||
"noQueueItems": "Nessun elemento in coda",
|
||||
"searchSubtitles": "Cerca sottotitoli",
|
||||
"language": "Lingua",
|
||||
"noSubtitlesFound": "Nessun sottotitolo trovato",
|
||||
"subtitleDownloaded": "Sottotitolo scaricato",
|
||||
"subtitleDownloadFailed": "Impossibile scaricare il sottotitolo",
|
||||
"searchLanguages": "Cerca lingue..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "Segna come non visto",
|
||||
"markedAsWatchedOffline": "Segnato come visto (sincronizzato online)",
|
||||
"markedAsUnwatchedOffline": "Segnato come non visto (sincronizzato online)",
|
||||
"autoRemovedWatchedDownload": "Rimosso automaticamente: ${title}",
|
||||
"removedFromContinueWatching": "Rimosso da Continua a guardare",
|
||||
"errorLoading": "Errore: ${error}",
|
||||
"fileInfoNotAvailable": "Informazioni sul file non disponibili",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "Impossibile creare la coda di riproduzione - nessun elemento",
|
||||
"failedPlayback": "Impossibile ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Passaggio al lettore compatibile...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "Riproduzione non riuscita",
|
||||
"serverLimitBody": "Il server ha restituito un errore (HTTP 500). Di solito significa che il proprietario del server ha impostato un limite di banda o di transcodifica che sta rifiutando la tua sessione. Non c'è nulla da fare dal client — il proprietario del server deve modificare le sue impostazioni.",
|
||||
"logsUploaded": "Log caricati",
|
||||
"logsUploadFailed": "Caricamento log fallito",
|
||||
"logId": "ID log"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "Opzioni stile",
|
||||
"text": "Testo",
|
||||
"border": "Bordo",
|
||||
"background": "Sfondo",
|
||||
"fontSize": "Dimensione",
|
||||
"textColor": "Colore testo",
|
||||
"borderSize": "Dimensione bordo",
|
||||
"borderColor": "Colore bordo",
|
||||
"backgroundOpacity": "Opacità sfondo",
|
||||
"backgroundColor": "Colore sfondo",
|
||||
"position": "Position"
|
||||
"position": "Posizione",
|
||||
"assOverride": "Sovrascrittura ASS",
|
||||
"bold": "Grassetto",
|
||||
"italic": "Corsivo"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "Configurazione MPV",
|
||||
"title": "Configurazione mpv",
|
||||
"description": "Impostazioni avanzate del lettore video",
|
||||
"properties": "Proprietà",
|
||||
"presets": "Preset",
|
||||
"noProperties": "Nessuna proprietà configurata",
|
||||
"noPresets": "Nessun preset salvato",
|
||||
"addProperty": "Aggiungi proprietà",
|
||||
"editProperty": "Modifica proprietà",
|
||||
"deleteProperty": "Elimina proprietà",
|
||||
"propertyKey": "Chiave proprietà",
|
||||
"propertyKeyHint": "es. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Valore proprietà",
|
||||
"propertyValueHint": "es. auto, 256000000",
|
||||
"saveAsPreset": "Salva come preset...",
|
||||
"presetName": "Nome preset",
|
||||
"presetNameHint": "Inserisci un nome per questo preset",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "Preset caricato",
|
||||
"presetDeleted": "Preset eliminato",
|
||||
"confirmDeletePreset": "Sei sicuro di voler eliminare questo preset?",
|
||||
"confirmDeleteProperty": "Sei sicuro di voler eliminare questa proprietà?",
|
||||
"entriesCount": "${count} voci"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Conferma azione"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "Panoramica",
|
||||
"cast": "Attori",
|
||||
"extras": "Trailer ed Extra",
|
||||
"seasons": "Stagioni",
|
||||
"studio": "Studio",
|
||||
"rating": "Classificazione",
|
||||
"episodeCount": "${count} episodi",
|
||||
"watchedProgress": "${watched}/${total} guardati",
|
||||
"movie": "Film",
|
||||
"tvShow": "Serie TV",
|
||||
"minutesLeft": "${minutes} minuti rimanenti"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "Playlist"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Raggruppamento",
|
||||
"all": "Tutti",
|
||||
"movies": "Film",
|
||||
"shows": "Serie TV",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "Cancella log",
|
||||
"copyLogs": "Copia log",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "Errore:",
|
||||
"stackTrace": "Traccia dello stack:"
|
||||
"uploadLogs": "Carica log"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Pacchetti correlati",
|
||||
@@ -561,24 +586,16 @@
|
||||
"navigation": {
|
||||
"libraries": "Librerie",
|
||||
"downloads": "Download",
|
||||
"liveTv": "TV in diretta"
|
||||
"liveTv": "TV Live"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "TV in diretta",
|
||||
"channels": "Canali",
|
||||
"guide": "Guida",
|
||||
"noChannels": "Nessun canale disponibile",
|
||||
"noDvr": "Nessun DVR configurato su nessun server",
|
||||
"tuneFailed": "Impossibile sintonizzare il canale",
|
||||
"loading": "Caricamento canali...",
|
||||
"nowPlaying": "In riproduzione",
|
||||
"noPrograms": "Nessun dato di programma disponibile",
|
||||
"channelNumber": "Canale ${number}",
|
||||
"live": "IN DIRETTA",
|
||||
"hd": "HD",
|
||||
"premiere": "NUOVO",
|
||||
"reloadGuide": "Ricarica guida",
|
||||
"allChannels": "Tutti i canali",
|
||||
"now": "Ora",
|
||||
"today": "Oggi",
|
||||
"midnight": "Mezzanotte",
|
||||
@@ -588,30 +605,41 @@
|
||||
"evening": "Sera",
|
||||
"lateNight": "Notte tarda",
|
||||
"whatsOn": "In onda ora",
|
||||
"watchChannel": "Guarda canale"
|
||||
"watchChannel": "Guarda canale",
|
||||
"favorites": "Preferiti",
|
||||
"reorderFavorites": "Riordina preferiti",
|
||||
"joinSession": "Partecipa alla sessione in corso",
|
||||
"watchFromStart": "Guarda dall'inizio (${minutes} min fa)",
|
||||
"watchLive": "Guarda in diretta",
|
||||
"goToLive": "Vai al live"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Download",
|
||||
"manage": "Gestisci",
|
||||
"tvShows": "Serie TV",
|
||||
"movies": "Film",
|
||||
"noDownloads": "Nessun download",
|
||||
"noDownloadsDescription": "I contenuti scaricati appariranno qui per la visualizzazione offline",
|
||||
"downloadNow": "Scarica",
|
||||
"deleteDownload": "Elimina download",
|
||||
"retryDownload": "Riprova download",
|
||||
"downloadQueued": "Download in coda",
|
||||
"episodesQueued": "${count} episodi in coda per il download",
|
||||
"downloadDeleted": "Download eliminato",
|
||||
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Il file scaricato verrà rimosso dal tuo dispositivo.",
|
||||
"deletingWithProgress": "Eliminazione di ${title}... (${current} di ${total})",
|
||||
"noDownloadsTree": "Nessun download",
|
||||
"pauseAll": "Metti tutto in pausa",
|
||||
"resumeAll": "Riprendi tutto",
|
||||
"deleteAll": "Elimina tutto"
|
||||
"collections": {
|
||||
"title": "Raccolte",
|
||||
"collection": "Raccolta",
|
||||
"empty": "La raccolta è vuota",
|
||||
"unknownLibrarySection": "Impossibile eliminare: sezione libreria sconosciuta",
|
||||
"deleteCollection": "Elimina raccolta",
|
||||
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Questa azione non può essere annullata.",
|
||||
"deleted": "Raccolta eliminata",
|
||||
"deleteFailed": "Impossibile eliminare la raccolta",
|
||||
"deleteFailedWithError": "Impossibile eliminare la raccolta: ${error}",
|
||||
"failedToLoadItems": "Impossibile caricare gli elementi della raccolta: ${error}",
|
||||
"selectCollection": "Seleziona raccolta",
|
||||
"collectionName": "Nome raccolta",
|
||||
"enterCollectionName": "Inserisci nome raccolta",
|
||||
"addedToCollection": "Aggiunto alla raccolta",
|
||||
"errorAddingToCollection": "Errore nell'aggiunta alla raccolta",
|
||||
"created": "Raccolta creata",
|
||||
"removeFromCollection": "Rimuovi dalla raccolta",
|
||||
"removeFromCollectionConfirm": "Rimuovere \"${title}\" da questa raccolta?",
|
||||
"removedFromCollection": "Rimosso dalla raccolta",
|
||||
"removeFromCollectionFailed": "Impossibile rimuovere dalla raccolta",
|
||||
"removeFromCollectionError": "Errore durante la rimozione dalla raccolta: ${error}",
|
||||
"searchCollections": "Cerca raccolte..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Playlist",
|
||||
"playlist": "Playlist",
|
||||
"noPlaylists": "Nessuna playlist trovata",
|
||||
"create": "Crea playlist",
|
||||
"playlistName": "Nome playlist",
|
||||
@@ -629,38 +657,12 @@
|
||||
"itemAdded": "Aggiunto alla playlist",
|
||||
"itemRemoved": "Rimosso dalla playlist",
|
||||
"selectPlaylist": "Seleziona playlist",
|
||||
"createNewPlaylist": "Crea nuova playlist",
|
||||
"errorCreating": "Errore durante la creazione della playlist",
|
||||
"errorDeleting": "Errore durante l'eliminazione della playlist",
|
||||
"errorLoading": "Errore durante il caricamento delle playlist",
|
||||
"errorAdding": "Errore durante l'aggiunta alla playlist",
|
||||
"errorReordering": "Errore durante il riordino dell'elemento della playlist",
|
||||
"errorRemoving": "Errore durante la rimozione dalla playlist",
|
||||
"playlist": "Playlist"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Raccolte",
|
||||
"collection": "Raccolta",
|
||||
"empty": "La raccolta è vuota",
|
||||
"unknownLibrarySection": "Impossibile eliminare: sezione libreria sconosciuta",
|
||||
"deleteCollection": "Elimina raccolta",
|
||||
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Questa azione non può essere annullata.",
|
||||
"deleted": "Raccolta eliminata",
|
||||
"deleteFailed": "Impossibile eliminare la raccolta",
|
||||
"deleteFailedWithError": "Impossibile eliminare la raccolta: ${error}",
|
||||
"failedToLoadItems": "Impossibile caricare gli elementi della raccolta: ${error}",
|
||||
"selectCollection": "Seleziona raccolta",
|
||||
"createNewCollection": "Crea nuova raccolta",
|
||||
"collectionName": "Nome raccolta",
|
||||
"enterCollectionName": "Inserisci nome raccolta",
|
||||
"addedToCollection": "Aggiunto alla raccolta",
|
||||
"errorAddingToCollection": "Errore nell'aggiunta alla raccolta",
|
||||
"created": "Raccolta creata",
|
||||
"removeFromCollection": "Rimuovi dalla raccolta",
|
||||
"removeFromCollectionConfirm": "Rimuovere \"${title}\" da questa raccolta?",
|
||||
"removedFromCollection": "Rimosso dalla raccolta",
|
||||
"removeFromCollectionFailed": "Impossibile rimuovere dalla raccolta",
|
||||
"removeFromCollectionError": "Errore durante la rimozione dalla raccolta: ${error}"
|
||||
"errorRemoving": "Errore durante la rimozione dalla playlist"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Guarda Insieme",
|
||||
@@ -697,18 +699,76 @@
|
||||
"leave": "Lascia",
|
||||
"syncing": "Sincronizzazione...",
|
||||
"joinWatchSession": "Unisciti alla Sessione di Visione",
|
||||
"enterCodeHint": "Inserisci codice di 8 caratteri",
|
||||
"enterCodeHint": "Inserisci codice di 5 caratteri",
|
||||
"pasteFromClipboard": "Incolla dagli appunti",
|
||||
"pleaseEnterCode": "Inserisci un codice sessione",
|
||||
"codeMustBe8Chars": "Il codice sessione deve essere di 8 caratteri",
|
||||
"codeMustBe5Chars": "Il codice sessione deve essere di 5 caratteri",
|
||||
"joinInstructions": "Inserisci il codice sessione condiviso dall'host per unirti alla loro sessione di visione.",
|
||||
"failedToCreate": "Impossibile creare la sessione",
|
||||
"failedToJoin": "Impossibile unirsi alla sessione",
|
||||
"sessionCodeCopied": "Codice sessione copiato negli appunti",
|
||||
"relayUnreachable": "Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare.",
|
||||
"reconnectingToHost": "Riconnessione all'host...",
|
||||
"currentPlayback": "Riproduzione corrente",
|
||||
"joinCurrentPlayback": "Unisciti alla riproduzione corrente",
|
||||
"joinCurrentPlaybackDescription": "Torna a ciò che l'host sta guardando in questo momento",
|
||||
"failedToOpenCurrentPlayback": "Impossibile aprire la riproduzione corrente",
|
||||
"participantJoined": "${name} si è unito",
|
||||
"participantLeft": "${name} se ne è andato"
|
||||
"participantLeft": "${name} se ne è andato",
|
||||
"participantPaused": "${name} ha messo in pausa",
|
||||
"participantResumed": "${name} ha ripreso",
|
||||
"participantSeeked": "${name} ha cercato",
|
||||
"participantBuffering": "${name} sta caricando",
|
||||
"waitingForParticipants": "In attesa che gli altri carichino...",
|
||||
"recentRooms": "Stanze recenti",
|
||||
"renameRoom": "Rinomina stanza",
|
||||
"removeRoom": "Rimuovi"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Download",
|
||||
"manage": "Gestisci",
|
||||
"tvShows": "Serie TV",
|
||||
"movies": "Film",
|
||||
"noDownloads": "Nessun download",
|
||||
"noDownloadsDescription": "I contenuti scaricati appariranno qui per la visualizzazione offline",
|
||||
"downloadNow": "Scarica",
|
||||
"deleteDownload": "Elimina download",
|
||||
"retryDownload": "Riprova download",
|
||||
"downloadQueued": "Download in coda",
|
||||
"serverErrorBitrate": "Errore del server — il file potrebbe superare il limite di bitrate dello streaming remoto",
|
||||
"episodesQueued": "${count} episodi in coda per il download",
|
||||
"downloadDeleted": "Download eliminato",
|
||||
"deleteConfirm": "Sei sicuro di voler eliminare \"${title}\"? Il file scaricato verrà rimosso dal tuo dispositivo.",
|
||||
"deletingWithProgress": "Eliminazione di ${title}... (${current} di ${total})",
|
||||
"noDownloadsTree": "Nessun download",
|
||||
"pauseAll": "Metti tutto in pausa",
|
||||
"resumeAll": "Riprendi tutto",
|
||||
"deleteAll": "Elimina tutto",
|
||||
"selectVersion": "Seleziona versione",
|
||||
"allEpisodes": "Tutti gli episodi",
|
||||
"unwatchedOnly": "Solo non visti",
|
||||
"nextNUnwatched": "Prossimi ${count} non visti",
|
||||
"customAmount": "Quantità personalizzata...",
|
||||
"howManyEpisodes": "Quanti episodi?",
|
||||
"itemsQueued": "${count} elementi in coda per il download",
|
||||
"keepSynced": "Mantieni sincronizzato",
|
||||
"downloadOnce": "Scarica una volta",
|
||||
"keepNUnwatched": "Mantieni ${count} non visti",
|
||||
"editSyncRule": "Modifica regola di sincronizzazione",
|
||||
"removeSyncRule": "Rimuovi regola di sincronizzazione",
|
||||
"removeSyncRuleConfirm": "Interrompere la sincronizzazione di \"${title}\"? Gli episodi scaricati verranno mantenuti.",
|
||||
"syncRuleCreated": "Regola di sincronizzazione creata — ${count} episodi non visti mantenuti",
|
||||
"syncRuleUpdated": "Regola di sincronizzazione aggiornata",
|
||||
"syncRuleRemoved": "Regola di sincronizzazione rimossa",
|
||||
"syncedNewEpisodes": "${count} nuovi episodi sincronizzati per ${title}",
|
||||
"activeSyncRules": "Regole di sincronizzazione",
|
||||
"noSyncRules": "Nessuna regola di sincronizzazione",
|
||||
"manageSyncRule": "Gestisci sincronizzazione",
|
||||
"editEpisodeCount": "Numero di episodi",
|
||||
"editSyncFilter": "Filtro di sincronizzazione",
|
||||
"syncAllItems": "Sincronizzazione di tutti gli elementi",
|
||||
"syncUnwatchedItems": "Sincronizzazione degli elementi non visti",
|
||||
"syncRuleListCreated": "Regola di sincronizzazione creata"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shader",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "Ridimensionamento NVIDIA per video più nitido",
|
||||
"qualityFast": "Veloce",
|
||||
"qualityHQ": "Alta qualità",
|
||||
"mode": "Modalità"
|
||||
"mode": "Modalità",
|
||||
"importShader": "Importa shader",
|
||||
"customShaderDescription": "Shader GLSL personalizzato",
|
||||
"shaderImported": "Shader importato",
|
||||
"shaderImportFailed": "Importazione shader fallita",
|
||||
"deleteShader": "Elimina shader",
|
||||
"deleteShaderConfirm": "Eliminare \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "Connetti a un dispositivo",
|
||||
"hostRemoteSession": "Ospita sessione remota",
|
||||
"controlThisDevice": "Controlla questo dispositivo con il tuo telefono",
|
||||
"remoteControl": "Telecomando",
|
||||
"controlDesktop": "Controlla un dispositivo desktop",
|
||||
"title": "Telecomando",
|
||||
"connectedTo": "Connesso a ${name}",
|
||||
"session": {
|
||||
"creatingSession": "Creazione sessione remota...",
|
||||
"failedToCreate": "Impossibile creare la sessione remota:",
|
||||
"noSession": "Nessuna sessione disponibile",
|
||||
"scanQrCode": "Scansiona QR Code",
|
||||
"orEnterManually": "Oppure inserisci manualmente",
|
||||
"startingServer": "Avvio del server remoto...",
|
||||
"failedToCreate": "Impossibile avviare il server remoto:",
|
||||
"hostAddress": "Indirizzo host",
|
||||
"sessionId": "ID sessione",
|
||||
"pin": "PIN",
|
||||
"connected": "Connesso",
|
||||
"waitingForConnection": "In attesa di connessione...",
|
||||
"serverRunning": "Server remoto attivo",
|
||||
"serverStopped": "Server remoto arrestato",
|
||||
"serverRunningDescription": "I dispositivi mobili nella tua rete possono scoprire e connettersi a questa app",
|
||||
"serverStoppedDescription": "Avvia il server per consentire ai dispositivi mobili di connettersi",
|
||||
"usePhoneToControl": "Usa il tuo dispositivo mobile per controllare questa app",
|
||||
"copiedToClipboard": "${label} copiato negli appunti",
|
||||
"copyToClipboard": "Copia negli appunti",
|
||||
"newSession": "Nuova sessione",
|
||||
"startServer": "Avvia server",
|
||||
"stopServer": "Arresta server",
|
||||
"minimize": "Riduci"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "Scansiona",
|
||||
"manual": "Manuale",
|
||||
"pairWithDesktop": "Associa con desktop",
|
||||
"enterSessionDetails": "Inserisci i dettagli della sessione mostrati sul tuo dispositivo desktop",
|
||||
"discoveryDescription": "I dispositivi nella tua rete che eseguono Plezy con lo stesso account Plex appariranno automaticamente",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "Inserisci ID sessione di 8 caratteri",
|
||||
"pinHint": "Inserisci PIN di 6 cifre",
|
||||
"connecting": "Connessione...",
|
||||
"tips": "Suggerimenti",
|
||||
"tipDesktop": "Apri Plezy sul tuo desktop e abilita Companion Remote dalle impostazioni o dal menu",
|
||||
"tipScan": "Usa la scheda Scansiona per associare rapidamente scansionando il QR code sul tuo desktop",
|
||||
"tipWifi": "Assicurati che entrambi i dispositivi siano sulla stessa rete WiFi",
|
||||
"cameraPermissionRequired": "L'autorizzazione della fotocamera è necessaria per scansionare i QR code.\nConcedi l'accesso alla fotocamera nelle impostazioni del dispositivo.",
|
||||
"cameraError": "Impossibile avviare la fotocamera: ${error}",
|
||||
"scanInstruction": "Punta la fotocamera verso il QR code mostrato sul tuo desktop",
|
||||
"invalidQrCode": "Formato QR code non valido",
|
||||
"searchingForDevices": "Ricerca dispositivi...",
|
||||
"noDevicesFound": "Nessun dispositivo trovato nella tua rete",
|
||||
"noDevicesHint": "Assicurati che Plezy sia aperto sul tuo desktop e che entrambi i dispositivi siano sulla stessa rete WiFi",
|
||||
"availableDevices": "Dispositivi disponibili",
|
||||
"manualConnection": "Connessione manuale",
|
||||
"cryptoInitFailed": "Impossibile inizializzare la connessione sicura. Assicurati di aver effettuato l'accesso a un account Plex.",
|
||||
"validationHostRequired": "Inserisci l'indirizzo host",
|
||||
"validationHostFormat": "Il formato deve essere IP:porta (es. 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "Inserisci un ID sessione",
|
||||
"validationSessionIdLength": "L'ID sessione deve essere di 8 caratteri",
|
||||
"validationPinRequired": "Inserisci un PIN",
|
||||
"validationPinLength": "Il PIN deve essere di 6 cifre",
|
||||
"connectionTimedOut": "Connessione scaduta. Verifica l'ID sessione e il PIN.",
|
||||
"sessionNotFound": "Sessione non trovata. Verifica le tue credenziali.",
|
||||
"connectionTimedOut": "Connessione scaduta. Assicurati che entrambi i dispositivi siano sulla stessa rete.",
|
||||
"sessionNotFound": "Dispositivo non trovato. Assicurati che Plezy sia in esecuzione sull'host.",
|
||||
"authFailed": "Autenticazione fallita. Assicurati che entrambi i dispositivi utilizzino lo stesso account Plex.",
|
||||
"failedToConnect": "Connessione fallita: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "Riconnessione...",
|
||||
"attemptOf": "Tentativo ${current} di 5",
|
||||
"retryNow": "Riprova ora",
|
||||
"connectionError": "Errore di connessione",
|
||||
"notConnected": "Non connesso",
|
||||
"tabRemote": "Telecomando",
|
||||
"tabPlay": "Riproduci",
|
||||
"tabMore": "Altro",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "Usa lettore esterno",
|
||||
"useExternalPlayerDescription": "Apri i video in un'app esterna invece del lettore integrato",
|
||||
"selectPlayer": "Seleziona lettore",
|
||||
"customPlayers": "Lettori personalizzati",
|
||||
"systemDefault": "Predefinito di sistema",
|
||||
"addCustomPlayer": "Aggiungi lettore personalizzato",
|
||||
"playerName": "Nome lettore",
|
||||
"playerCommand": "Comando",
|
||||
"playerPackage": "Nome pacchetto",
|
||||
"playerUrlScheme": "Schema URL",
|
||||
"customPlayer": "Lettore personalizzato",
|
||||
"off": "Disattivato",
|
||||
"launchFailed": "Impossibile aprire il lettore esterno",
|
||||
"appNotInstalled": "${name} non è installato",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "Trama",
|
||||
"poster": "Poster",
|
||||
"background": "Sfondo",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Immagine quadrata",
|
||||
"selectPoster": "Seleziona poster",
|
||||
"selectBackground": "Seleziona sfondo",
|
||||
"selectLogo": "Seleziona logo",
|
||||
"selectSquareArt": "Seleziona immagine quadrata",
|
||||
"fromUrl": "Da URL",
|
||||
"uploadFile": "Carica file",
|
||||
"enterImageUrl": "Inserisci URL immagine",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "Selezione automatica sottotitoli",
|
||||
"manuallySelected": "Selezionato manualmente",
|
||||
"shownWithForeignAudio": "Mostrati con audio straniero",
|
||||
"alwaysEnabled": "Sempre attivo"
|
||||
"alwaysEnabled": "Sempre attivo",
|
||||
"tags": "Tag",
|
||||
"addTag": "Aggiungi tag",
|
||||
"genre": "Genere",
|
||||
"director": "Regista",
|
||||
"writer": "Sceneggiatore",
|
||||
"producer": "Produttore",
|
||||
"country": "Paese",
|
||||
"collection": "Collezione",
|
||||
"label": "Etichetta",
|
||||
"style": "Stile",
|
||||
"mood": "Atmosfera"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Attività del server",
|
||||
"failedToLoad": "Impossibile caricare le attività",
|
||||
"noTasks": "Nessuna attività in corso"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Connesso",
|
||||
"connectedAs": "Connesso come @${username}",
|
||||
"disconnectConfirm": "Disconnettere l'account Trakt?",
|
||||
"disconnectConfirmBody": "Plezy smetterà di inviare eventi di riproduzione a Trakt. Puoi riconnetterti in qualsiasi momento.",
|
||||
"scrobble": "Scrobbling in tempo reale",
|
||||
"scrobbleDescription": "Invia eventi di riproduzione, pausa e arresto a Trakt durante la riproduzione.",
|
||||
"watchedSync": "Sincronizza stato visualizzato",
|
||||
"watchedSyncDescription": "Quando segni elementi come visti in Plezy, vengono segnati anche su Trakt.",
|
||||
"deviceCodeTitle": "Attiva Plezy su Trakt",
|
||||
"deviceCodeBody": "Visita ${url} e inserisci questo codice:",
|
||||
"openTraktActivate": "Apri Trakt per attivare",
|
||||
"waitingForAuthorization": "In attesa di autorizzazione…",
|
||||
"codeCopied": "Codice copiato",
|
||||
"connectFailed": "Impossibile connettersi a Trakt. Riprova."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,978 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Plezy"
|
||||
},
|
||||
"auth": {
|
||||
"signInWithPlex": "Plexでサインイン",
|
||||
"showQRCode": "QRコードを表示",
|
||||
"authenticate": "認証",
|
||||
"authenticationTimeout": "認証がタイムアウトしました。もう一度お試しください。",
|
||||
"scanQRToSignIn": "このQRコードをスキャンしてサインイン",
|
||||
"waitingForAuth": "認証を待機中...\nブラウザでサインインを完了してください。",
|
||||
"useBrowser": "ブラウザを使用"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "キャンセル",
|
||||
"save": "保存",
|
||||
"close": "閉じる",
|
||||
"clear": "クリア",
|
||||
"reset": "リセット",
|
||||
"later": "後で",
|
||||
"submit": "送信",
|
||||
"confirm": "確認",
|
||||
"retry": "再試行",
|
||||
"logout": "ログアウト",
|
||||
"unknown": "不明",
|
||||
"refresh": "更新",
|
||||
"yes": "はい",
|
||||
"no": "いいえ",
|
||||
"delete": "削除",
|
||||
"shuffle": "シャッフル",
|
||||
"addTo": "追加...",
|
||||
"createNew": "新規作成",
|
||||
"connect": "接続",
|
||||
"disconnect": "切断",
|
||||
"play": "再生",
|
||||
"pause": "一時停止",
|
||||
"resume": "再開",
|
||||
"error": "エラー",
|
||||
"search": "検索",
|
||||
"home": "ホーム",
|
||||
"back": "戻る",
|
||||
"settings": "設定",
|
||||
"mute": "ミュート",
|
||||
"ok": "OK",
|
||||
"reconnect": "再接続",
|
||||
"exitConfirmTitle": "アプリを終了しますか?",
|
||||
"exitConfirmMessage": "終了してもよろしいですか?",
|
||||
"dontAskAgain": "次回から表示しない",
|
||||
"exit": "終了",
|
||||
"viewAll": "すべて表示",
|
||||
"checkingNetwork": "ネットワークを確認中...",
|
||||
"refreshingServers": "サーバーを更新中...",
|
||||
"loadingServers": "サーバーを読み込み中...",
|
||||
"connectingToServers": "サーバーに接続中...",
|
||||
"startingOfflineMode": "オフラインモードを開始中...",
|
||||
"loading": "読み込み中..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "ライセンス",
|
||||
"switchProfile": "プロフィール切替",
|
||||
"subtitleStyling": "字幕スタイル",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "ログ"
|
||||
},
|
||||
"update": {
|
||||
"available": "アップデート利用可能",
|
||||
"versionAvailable": "バージョン ${version} が利用可能です",
|
||||
"currentVersion": "現在: ${version}",
|
||||
"skipVersion": "このバージョンをスキップ",
|
||||
"viewRelease": "リリースを表示",
|
||||
"latestVersion": "最新バージョンです",
|
||||
"checkFailed": "アップデートの確認に失敗しました"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"supportDeveloper": "Plezy を支援",
|
||||
"supportDeveloperDescription": "Liberapay で寄付して開発を支援",
|
||||
"language": "言語",
|
||||
"theme": "テーマ",
|
||||
"appearance": "外観",
|
||||
"videoPlayback": "動画再生",
|
||||
"videoPlaybackDescription": "再生動作を設定",
|
||||
"advanced": "詳細",
|
||||
"episodePosterMode": "エピソードポスタースタイル",
|
||||
"seriesPoster": "シリーズポスター",
|
||||
"seasonPoster": "シーズンポスター",
|
||||
"episodeThumbnail": "サムネイル",
|
||||
"showHeroSectionDescription": "ホーム画面に注目コンテンツのカルーセルを表示",
|
||||
"secondsLabel": "秒",
|
||||
"minutesLabel": "分",
|
||||
"secondsShort": "秒",
|
||||
"minutesShort": "分",
|
||||
"durationHint": "時間を入力 (${min}-${max})",
|
||||
"systemTheme": "システム",
|
||||
"lightTheme": "ライト",
|
||||
"darkTheme": "ダーク",
|
||||
"oledTheme": "OLED",
|
||||
"libraryDensity": "ライブラリの密度",
|
||||
"compact": "コンパクト",
|
||||
"comfortable": "ゆったり",
|
||||
"viewMode": "表示モード",
|
||||
"gridView": "グリッド",
|
||||
"listView": "リスト",
|
||||
"showHeroSection": "ヒーローセクションを表示",
|
||||
"useGlobalHubs": "Plex Homeレイアウトを使用",
|
||||
"useGlobalHubsDescription": "公式Plexクライアントのようにホームページのハブを表示。オフにすると、ライブラリごとのおすすめを表示。",
|
||||
"showServerNameOnHubs": "ハブにサーバー名を表示",
|
||||
"showServerNameOnHubsDescription": "ハブタイトルに常にサーバー名を表示。オフにすると、重複名のみ表示。",
|
||||
"alwaysKeepSidebarOpen": "サイドバーを常に開いておく",
|
||||
"alwaysKeepSidebarOpenDescription": "サイドバーを展開したまま、コンテンツ領域が調整される",
|
||||
"showUnwatchedCount": "未視聴数を表示",
|
||||
"showUnwatchedCountDescription": "番組とシーズンに未視聴エピソード数を表示",
|
||||
"hideSpoilers": "未視聴エピソードのネタバレを非表示",
|
||||
"hideSpoilersDescription": "まだ視聴していないエピソードのサムネイルをぼかし、説明を非表示",
|
||||
"playerBackend": "プレーヤーバックエンド",
|
||||
"exoPlayer": "ExoPlayer(推奨)",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "ハードウェアデコード",
|
||||
"hardwareDecodingDescription": "利用可能な場合にハードウェアアクセラレーションを使用",
|
||||
"bufferSize": "バッファサイズ",
|
||||
"bufferSizeMB": "${size}MB",
|
||||
"bufferSizeAuto": "自動(推奨)",
|
||||
"bufferSizeWarning": "デバイスのメモリは${heap}MBです。${size}MBのバッファは再生の問題を引き起こす可能性があります。",
|
||||
"subtitleStyling": "字幕スタイル",
|
||||
"subtitleStylingDescription": "字幕の外観をカスタマイズ",
|
||||
"smallSkipDuration": "短いスキップ時間",
|
||||
"largeSkipDuration": "長いスキップ時間",
|
||||
"rewindOnResume": "再開時に巻き戻し",
|
||||
"secondsUnit": "${seconds}秒",
|
||||
"defaultSleepTimer": "デフォルトスリープタイマー",
|
||||
"minutesUnit": "${minutes}分",
|
||||
"rememberTrackSelections": "番組/映画ごとにトラック選択を記憶",
|
||||
"rememberTrackSelectionsDescription": "再生中にトラックを変更すると、音声と字幕の言語設定を自動保存",
|
||||
"clickVideoTogglesPlayback": "動画クリックで再生/一時停止を切替",
|
||||
"clickVideoTogglesPlaybackDescription": "有効にすると、動画プレーヤーをクリックで再生/一時停止。それ以外は再生コントロールの表示/非表示。",
|
||||
"videoPlayerControls": "動画プレーヤーコントロール",
|
||||
"keyboardShortcuts": "キーボードショートカット",
|
||||
"keyboardShortcutsDescription": "キーボードショートカットをカスタマイズ",
|
||||
"videoPlayerNavigation": "動画プレーヤーナビゲーション",
|
||||
"videoPlayerNavigationDescription": "矢印キーで動画プレーヤーコントロールを操作",
|
||||
"watchTogetherRelay": "一緒に視聴リレーサーバー",
|
||||
"watchTogetherRelayDescription": "一緒に視聴用のカスタムリレーサーバーを設定します。すべての参加者が同じサーバーを使用する必要があります。",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"crashReporting": "クラッシュレポート",
|
||||
"crashReportingDescription": "アプリの改善に役立つクラッシュレポートを送信",
|
||||
"debugLogging": "デバッグログ",
|
||||
"debugLoggingDescription": "トラブルシューティング用の詳細なログを有効化",
|
||||
"viewLogs": "ログを表示",
|
||||
"viewLogsDescription": "アプリケーションログを表示",
|
||||
"clearCache": "キャッシュをクリア",
|
||||
"clearCacheDescription": "キャッシュされたすべての画像とデータをクリアします。クリア後、コンテンツの読み込みに時間がかかる場合があります。",
|
||||
"clearCacheSuccess": "キャッシュを正常にクリアしました",
|
||||
"resetSettings": "設定をリセット",
|
||||
"resetSettingsDescription": "すべての設定をデフォルト値にリセットします。この操作は元に戻せません。",
|
||||
"resetSettingsSuccess": "設定を正常にリセットしました",
|
||||
"shortcutsReset": "ショートカットをデフォルトにリセットしました",
|
||||
"about": "アプリについて",
|
||||
"aboutDescription": "アプリ情報とライセンス",
|
||||
"updates": "アップデート",
|
||||
"updateAvailable": "アップデート利用可能",
|
||||
"checkForUpdates": "アップデートを確認",
|
||||
"validationErrorEnterNumber": "有効な数値を入力してください",
|
||||
"validationErrorDuration": "時間は${min}から${max} ${unit}の間である必要があります",
|
||||
"shortcutAlreadyAssigned": "ショートカットは既に${action}に割り当てられています",
|
||||
"shortcutUpdated": "${action}のショートカットを更新しました",
|
||||
"autoSkip": "自動スキップ",
|
||||
"autoSkipIntro": "イントロを自動スキップ",
|
||||
"autoSkipIntroDescription": "数秒後にイントロマーカーを自動的にスキップ",
|
||||
"autoSkipCredits": "クレジットを自動スキップ",
|
||||
"autoSkipCreditsDescription": "クレジットを自動的にスキップして次のエピソードを再生",
|
||||
"autoSkipDelay": "自動スキップの遅延",
|
||||
"autoSkipDelayDescription": "自動スキップまで${seconds}秒待機",
|
||||
"introPattern": "イントロマーカーパターン",
|
||||
"introPatternDescription": "チャプタータイトルのイントロマーカーに一致する正規表現パターン",
|
||||
"creditsPattern": "クレジットマーカーパターン",
|
||||
"creditsPatternDescription": "チャプタータイトルのクレジットマーカーに一致する正規表現パターン",
|
||||
"invalidRegex": "無効な正規表現",
|
||||
"downloads": "ダウンロード",
|
||||
"downloadLocationDescription": "ダウンロードコンテンツの保存場所を選択",
|
||||
"downloadLocationDefault": "デフォルト(アプリストレージ)",
|
||||
"downloadLocationCustom": "カスタムの場所",
|
||||
"selectFolder": "フォルダを選択",
|
||||
"resetToDefault": "デフォルトに戻す",
|
||||
"currentPath": "現在: ${path}",
|
||||
"downloadLocationChanged": "ダウンロード場所を変更しました",
|
||||
"downloadLocationReset": "ダウンロード場所をデフォルトにリセットしました",
|
||||
"downloadLocationInvalid": "選択したフォルダは書き込みできません",
|
||||
"downloadLocationSelectError": "フォルダの選択に失敗しました",
|
||||
"downloadOnWifiOnly": "WiFiのみでダウンロード",
|
||||
"downloadOnWifiOnlyDescription": "モバイルデータ通信時のダウンロードを防止",
|
||||
"autoRemoveWatchedDownloads": "視聴済みダウンロードの自動削除",
|
||||
"autoRemoveWatchedDownloadsDescription": "視聴済みにしたエピソードや映画のダウンロードを自動的に削除",
|
||||
"cellularDownloadBlocked": "モバイルデータ通信ではダウンロードが無効です。WiFiに接続するか設定を変更してください。",
|
||||
"maxVolume": "最大音量",
|
||||
"maxVolumeDescription": "静かなメディアに対して100%以上の音量ブーストを許可",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Discordで視聴中の内容を表示",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "視聴履歴を Trakt と同期",
|
||||
"companionRemoteServer": "コンパニオンリモートサーバー",
|
||||
"companionRemoteServerDescription": "ネットワーク上のモバイルデバイスからこのアプリを操作できるようにする",
|
||||
"autoPip": "自動ピクチャーインピクチャー",
|
||||
"autoPipDescription": "再生中にアプリを離れると自動的にピクチャーインピクチャーに移行",
|
||||
"matchContentFrameRate": "コンテンツのフレームレートに合わせる",
|
||||
"matchContentFrameRateDescription": "動画コンテンツに合わせてディスプレイのリフレッシュレートを調整し、ジャダーを低減しバッテリーを節約",
|
||||
"matchRefreshRate": "リフレッシュレートを合わせる",
|
||||
"matchRefreshRateDescription": "フルスクリーン時にディスプレイのリフレッシュレートを動画コンテンツに合わせて切り替える",
|
||||
"matchDynamicRange": "ダイナミックレンジを合わせる",
|
||||
"matchDynamicRangeDescription": "HDRコンテンツに対して自動的にHDRを有効にし、プレーヤーを閉じるとSDRに戻す",
|
||||
"displaySwitchDelay": "ディスプレイ切り替え遅延",
|
||||
"tunneledPlayback": "トンネル再生",
|
||||
"tunneledPlaybackDescription": "ハードウェアアクセラレーションされたビデオトンネリングを使用。HDRコンテンツで音声のみで画面が黒くなる場合は無効にしてください",
|
||||
"requireProfileSelectionOnOpen": "アプリ起動時にプロフィールを確認",
|
||||
"requireProfileSelectionOnOpenDescription": "アプリを開くたびにプロフィール選択を表示",
|
||||
"confirmExitOnBack": "終了前に確認",
|
||||
"confirmExitOnBackDescription": "戻るボタンでアプリを終了する際に確認ダイアログを表示",
|
||||
"forceTvMode": "TVモードを強制",
|
||||
"forceTvModeDescription": "自動検出に関係なくTVレイアウトを使用します。leanback機能を報告しないAndroid TVデバイスで便利です。変更するとアプリが再起動されます。",
|
||||
"autoHidePerformanceOverlay": "パフォーマンスオーバーレイを自動非表示",
|
||||
"autoHidePerformanceOverlayDescription": "再生コントロールと一緒にパフォーマンスオーバーレイをフェードする",
|
||||
"showNavBarLabels": "ナビゲーションバーラベルを表示",
|
||||
"showNavBarLabelsDescription": "ナビゲーションバーアイコンの下にテキストラベルを表示",
|
||||
"liveTvDefaultFavorites": "お気に入りチャンネルをデフォルトに",
|
||||
"liveTvDefaultFavoritesDescription": "ライブTV を開いたときにお気に入りチャンネルのみ表示",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "映画、番組、音楽を検索...",
|
||||
"tryDifferentTerm": "別の検索語をお試しください",
|
||||
"searchYourMedia": "メディアを検索",
|
||||
"enterTitleActorOrKeyword": "タイトル、俳優、またはキーワードを入力"
|
||||
},
|
||||
"hotkeys": {
|
||||
"setShortcutFor": "${actionName}のショートカットを設定",
|
||||
"clearShortcut": "ショートカットをクリア",
|
||||
"actions": {
|
||||
"playPause": "再生/一時停止",
|
||||
"volumeUp": "音量を上げる",
|
||||
"volumeDown": "音量を下げる",
|
||||
"seekForward": "前方にシーク (${seconds}秒)",
|
||||
"seekBackward": "後方にシーク (${seconds}秒)",
|
||||
"fullscreenToggle": "フルスクリーン切替",
|
||||
"muteToggle": "ミュート切替",
|
||||
"subtitleToggle": "字幕切替",
|
||||
"audioTrackNext": "次の音声トラック",
|
||||
"subtitleTrackNext": "次の字幕トラック",
|
||||
"chapterNext": "次のチャプター",
|
||||
"chapterPrevious": "前のチャプター",
|
||||
"speedIncrease": "速度を上げる",
|
||||
"speedDecrease": "速度を下げる",
|
||||
"speedReset": "速度をリセット",
|
||||
"subSeekNext": "次の字幕にシーク",
|
||||
"subSeekPrev": "前の字幕にシーク",
|
||||
"shaderToggle": "シェーダー切替",
|
||||
"skipMarker": "イントロ/クレジットをスキップ"
|
||||
}
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "ファイル情報",
|
||||
"video": "映像",
|
||||
"audio": "音声",
|
||||
"file": "ファイル",
|
||||
"advanced": "詳細",
|
||||
"codec": "コーデック",
|
||||
"resolution": "解像度",
|
||||
"bitrate": "ビットレート",
|
||||
"frameRate": "フレームレート",
|
||||
"aspectRatio": "アスペクト比",
|
||||
"profile": "プロファイル",
|
||||
"bitDepth": "ビット深度",
|
||||
"colorSpace": "色空間",
|
||||
"colorRange": "色範囲",
|
||||
"colorPrimaries": "色原色",
|
||||
"chromaSubsampling": "クロマサブサンプリング",
|
||||
"channels": "チャンネル",
|
||||
"subtitles": "字幕",
|
||||
"overallBitrate": "全体ビットレート",
|
||||
"path": "パス",
|
||||
"size": "サイズ",
|
||||
"container": "コンテナ",
|
||||
"duration": "長さ",
|
||||
"optimizedForStreaming": "ストリーミング最適化",
|
||||
"has64bitOffsets": "64ビットオフセット"
|
||||
},
|
||||
"mediaMenu": {
|
||||
"markAsWatched": "視聴済みにする",
|
||||
"markAsUnwatched": "未視聴にする",
|
||||
"removeFromContinueWatching": "視聴中から削除",
|
||||
"goToSeries": "シリーズへ移動",
|
||||
"goToSeason": "シーズンへ移動",
|
||||
"shufflePlay": "シャッフル再生",
|
||||
"fileInfo": "ファイル情報",
|
||||
"deleteFromServer": "サーバーから削除",
|
||||
"confirmDelete": "このメディアとそのファイルがサーバーから完全に削除されます。この操作は元に戻せません。",
|
||||
"deleteMultipleWarning": "すべてのエピソードとそのファイルが含まれます。",
|
||||
"mediaDeletedSuccessfully": "メディアアイテムを正常に削除しました",
|
||||
"mediaFailedToDelete": "メディアアイテムの削除に失敗しました",
|
||||
"rate": "評価",
|
||||
"playFromBeginning": "最初から再生",
|
||||
"playVersion": "バージョンを再生..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}、映画",
|
||||
"mediaCardShow": "${title}、テレビ番組",
|
||||
"mediaCardEpisode": "${title}、${episodeInfo}",
|
||||
"mediaCardSeason": "${title}、${seasonInfo}",
|
||||
"mediaCardWatched": "視聴済み",
|
||||
"mediaCardPartiallyWatched": "${percent}パーセント視聴済み",
|
||||
"mediaCardUnwatched": "未視聴",
|
||||
"tapToPlay": "タップして再生"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "シャッフル再生",
|
||||
"playTrailer": "予告編を再生",
|
||||
"markAsWatched": "視聴済みにする",
|
||||
"markAsUnwatched": "未視聴にする"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "音声",
|
||||
"subtitlesLabel": "字幕",
|
||||
"resetToZero": "0msにリセット",
|
||||
"addTime": "+${amount}${unit}",
|
||||
"minusTime": "-${amount}${unit}",
|
||||
"playsLater": "${label}遅く再生",
|
||||
"playsEarlier": "${label}早く再生",
|
||||
"noOffset": "オフセットなし",
|
||||
"letterbox": "レターボックス",
|
||||
"fillScreen": "画面を埋める",
|
||||
"stretch": "引き延ばす",
|
||||
"lockRotation": "回転をロック",
|
||||
"unlockRotation": "回転のロックを解除",
|
||||
"timerActive": "タイマー動作中",
|
||||
"playbackWillPauseIn": "再生は${duration}後に一時停止します",
|
||||
"stillWatching": "まだ視聴中ですか?",
|
||||
"pausingIn": "${seconds}秒後に一時停止",
|
||||
"continueWatching": "続ける",
|
||||
"autoPlayNext": "次を自動再生",
|
||||
"playNext": "次を再生",
|
||||
"playButton": "再生",
|
||||
"pauseButton": "一時停止",
|
||||
"seekBackwardButton": "${seconds}秒戻る",
|
||||
"seekForwardButton": "${seconds}秒進む",
|
||||
"previousButton": "前のエピソード",
|
||||
"nextButton": "次のエピソード",
|
||||
"previousChapterButton": "前のチャプター",
|
||||
"nextChapterButton": "次のチャプター",
|
||||
"muteButton": "ミュート",
|
||||
"unmuteButton": "ミュート解除",
|
||||
"settingsButton": "動画設定",
|
||||
"tracksButton": "音声と字幕",
|
||||
"chaptersButton": "チャプター",
|
||||
"versionsButton": "動画バージョン",
|
||||
"pipButton": "ピクチャーインピクチャーモード",
|
||||
"aspectRatioButton": "アスペクト比",
|
||||
"ambientLighting": "アンビエントライティング",
|
||||
"fullscreenButton": "フルスクリーンに入る",
|
||||
"exitFullscreenButton": "フルスクリーンを終了",
|
||||
"alwaysOnTopButton": "常に前面に表示",
|
||||
"rotationLockButton": "回転ロック",
|
||||
"lockScreen": "画面をロック",
|
||||
"screenLockButton": "画面ロック",
|
||||
"longPressToUnlock": "長押しでロック解除",
|
||||
"timelineSlider": "動画タイムライン",
|
||||
"volumeSlider": "音量レベル",
|
||||
"endsAt": "${time}に終了",
|
||||
"pipActive": "ピクチャーインピクチャーで再生中",
|
||||
"pipFailed": "ピクチャーインピクチャーの開始に失敗しました",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Android 8.0以降が必要です",
|
||||
"iosVersion": "iOS 15.0以降が必要です",
|
||||
"permissionDisabled": "ピクチャーインピクチャーの権限が無効です。設定 > アプリ > Plezy > ピクチャーインピクチャーで有効にしてください",
|
||||
"notSupported": "デバイスはピクチャーインピクチャーモードをサポートしていません",
|
||||
"voSwitchFailed": "ピクチャーインピクチャーの映像出力切替に失敗しました",
|
||||
"failed": "ピクチャーインピクチャーの開始に失敗しました",
|
||||
"unknown": "エラーが発生しました: ${error}"
|
||||
},
|
||||
"chapters": "チャプター",
|
||||
"noChaptersAvailable": "チャプターがありません",
|
||||
"queue": "キュー",
|
||||
"noQueueItems": "キューにアイテムがありません",
|
||||
"searchSubtitles": "字幕を検索",
|
||||
"language": "言語",
|
||||
"noSubtitlesFound": "字幕が見つかりません",
|
||||
"subtitleDownloaded": "字幕をダウンロードしました",
|
||||
"subtitleDownloadFailed": "字幕のダウンロードに失敗しました",
|
||||
"searchLanguages": "言語を検索..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "管理者",
|
||||
"restricted": "制限付き",
|
||||
"protected": "保護済み",
|
||||
"current": "現在"
|
||||
},
|
||||
"messages": {
|
||||
"markedAsWatched": "視聴済みにしました",
|
||||
"markedAsUnwatched": "未視聴にしました",
|
||||
"markedAsWatchedOffline": "視聴済みにしました(オンライン時に同期)",
|
||||
"markedAsUnwatchedOffline": "未視聴にしました(オンライン時に同期)",
|
||||
"autoRemovedWatchedDownload": "自動削除: ${title}",
|
||||
"removedFromContinueWatching": "視聴中から削除しました",
|
||||
"errorLoading": "エラー: ${error}",
|
||||
"fileInfoNotAvailable": "ファイル情報が利用できません",
|
||||
"errorLoadingFileInfo": "ファイル情報の読み込みエラー: ${error}",
|
||||
"errorLoadingSeries": "シリーズの読み込みエラー",
|
||||
"errorLoadingSeason": "シーズンの読み込みエラー",
|
||||
"musicNotSupported": "音楽の再生はまだサポートされていません",
|
||||
"logsCleared": "ログをクリアしました",
|
||||
"logsCopied": "ログをクリップボードにコピーしました",
|
||||
"noLogsAvailable": "ログがありません",
|
||||
"libraryScanning": "\"${title}\"をスキャン中...",
|
||||
"libraryScanStarted": "\"${title}\"のライブラリスキャンを開始しました",
|
||||
"libraryScanFailed": "ライブラリのスキャンに失敗しました: ${error}",
|
||||
"metadataRefreshing": "\"${title}\"のメタデータを更新中...",
|
||||
"metadataRefreshStarted": "\"${title}\"のメタデータ更新を開始しました",
|
||||
"metadataRefreshFailed": "メタデータの更新に失敗しました: ${error}",
|
||||
"logoutConfirm": "ログアウトしてもよろしいですか?",
|
||||
"noSeasonsFound": "シーズンが見つかりません",
|
||||
"noEpisodesFound": "最初のシーズンにエピソードが見つかりません",
|
||||
"noEpisodesFoundGeneral": "エピソードが見つかりません",
|
||||
"noResultsFound": "結果が見つかりません",
|
||||
"sleepTimerSet": "スリープタイマーを${label}に設定しました",
|
||||
"noItemsAvailable": "アイテムがありません",
|
||||
"failedToCreatePlayQueueNoItems": "再生キューの作成に失敗しました - アイテムがありません",
|
||||
"failedPlayback": "${action}に失敗しました: ${error}",
|
||||
"switchingToCompatiblePlayer": "互換プレーヤーに切替中...",
|
||||
"serverLimitTitle": "再生に失敗しました",
|
||||
"serverLimitBody": "サーバーからエラーが返されました (HTTP 500)。通常、これはサーバーの所有者が帯域幅またはトランスコードの制限を設定しており、セッションが拒否されていることを意味します。クライアント側でできることはありません — サーバーの所有者が設定を調整する必要があります。",
|
||||
"logsUploaded": "ログをアップロードしました",
|
||||
"logsUploadFailed": "ログのアップロードに失敗しました",
|
||||
"logId": "ログID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"text": "テキスト",
|
||||
"border": "枠線",
|
||||
"background": "背景",
|
||||
"fontSize": "フォントサイズ",
|
||||
"textColor": "テキストの色",
|
||||
"borderSize": "枠線サイズ",
|
||||
"borderColor": "枠線の色",
|
||||
"backgroundOpacity": "背景の不透明度",
|
||||
"backgroundColor": "背景色",
|
||||
"position": "位置",
|
||||
"assOverride": "ASSオーバーライド",
|
||||
"bold": "太字",
|
||||
"italic": "斜体"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "mpv.conf",
|
||||
"description": "高度な動画プレーヤー設定",
|
||||
"presets": "プリセット",
|
||||
"noPresets": "保存済みプリセットがありません",
|
||||
"saveAsPreset": "プリセットとして保存...",
|
||||
"presetName": "プリセット名",
|
||||
"presetNameHint": "プリセットの名前を入力",
|
||||
"loadPreset": "読み込み",
|
||||
"deletePreset": "削除",
|
||||
"presetSaved": "プリセットを保存しました",
|
||||
"presetLoaded": "プリセットを読み込みました",
|
||||
"presetDeleted": "プリセットを削除しました",
|
||||
"confirmDeletePreset": "このプリセットを削除してもよろしいですか?",
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "操作の確認"
|
||||
},
|
||||
"discover": {
|
||||
"title": "探す",
|
||||
"switchProfile": "プロフィール切替",
|
||||
"noContentAvailable": "コンテンツがありません",
|
||||
"addMediaToLibraries": "ライブラリにメディアを追加してください",
|
||||
"continueWatching": "視聴を続ける",
|
||||
"playEpisode": "S${season}E${episode}",
|
||||
"overview": "あらすじ",
|
||||
"cast": "キャスト",
|
||||
"extras": "予告編とエクストラ",
|
||||
"studio": "スタジオ",
|
||||
"rating": "評価",
|
||||
"movie": "映画",
|
||||
"tvShow": "テレビ番組",
|
||||
"minutesLeft": "残り${minutes}分"
|
||||
},
|
||||
"errors": {
|
||||
"searchFailed": "検索に失敗しました: ${error}",
|
||||
"connectionTimeout": "${context}の読み込み中に接続がタイムアウトしました",
|
||||
"connectionFailed": "Plexサーバーに接続できません",
|
||||
"failedToLoad": "${context}の読み込みに失敗しました: ${error}",
|
||||
"noClientAvailable": "クライアントが利用できません",
|
||||
"authenticationFailed": "認証に失敗しました: ${error}",
|
||||
"couldNotLaunchUrl": "認証URLを開けませんでした",
|
||||
"pleaseEnterToken": "トークンを入力してください",
|
||||
"invalidToken": "無効なトークン",
|
||||
"failedToVerifyToken": "トークンの検証に失敗しました: ${error}",
|
||||
"failedToSwitchProfile": "${displayName}への切替に失敗しました"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "ライブラリ",
|
||||
"scanLibraryFiles": "ライブラリファイルをスキャン",
|
||||
"scanLibrary": "ライブラリをスキャン",
|
||||
"analyze": "解析",
|
||||
"analyzeLibrary": "ライブラリを解析",
|
||||
"refreshMetadata": "メタデータを更新",
|
||||
"emptyTrash": "ゴミ箱を空にする",
|
||||
"emptyingTrash": "\"${title}\"のゴミ箱を空にしています...",
|
||||
"trashEmptied": "\"${title}\"のゴミ箱を空にしました",
|
||||
"failedToEmptyTrash": "ゴミ箱を空にできませんでした: ${error}",
|
||||
"analyzing": "\"${title}\"を解析中...",
|
||||
"analysisStarted": "\"${title}\"の解析を開始しました",
|
||||
"failedToAnalyze": "ライブラリの解析に失敗しました: ${error}",
|
||||
"noLibrariesFound": "ライブラリが見つかりません",
|
||||
"thisLibraryIsEmpty": "このライブラリは空です",
|
||||
"all": "すべて",
|
||||
"clearAll": "すべてクリア",
|
||||
"scanLibraryConfirm": "\"${title}\"をスキャンしてもよろしいですか?",
|
||||
"analyzeLibraryConfirm": "\"${title}\"を解析してもよろしいですか?",
|
||||
"refreshMetadataConfirm": "\"${title}\"のメタデータを更新してもよろしいですか?",
|
||||
"emptyTrashConfirm": "\"${title}\"のゴミ箱を空にしてもよろしいですか?",
|
||||
"manageLibraries": "ライブラリを管理",
|
||||
"sort": "並べ替え",
|
||||
"sortBy": "並べ替え順",
|
||||
"filters": "フィルター",
|
||||
"confirmActionMessage": "この操作を実行してもよろしいですか?",
|
||||
"showLibrary": "ライブラリを表示",
|
||||
"hideLibrary": "ライブラリを非表示",
|
||||
"libraryOptions": "ライブラリオプション",
|
||||
"content": "ライブラリコンテンツ",
|
||||
"selectLibrary": "ライブラリを選択",
|
||||
"filtersWithCount": "フィルター (${count})",
|
||||
"noRecommendations": "おすすめがありません",
|
||||
"noCollections": "このライブラリにコレクションがありません",
|
||||
"noFoldersFound": "フォルダが見つかりません",
|
||||
"folders": "フォルダ",
|
||||
"tabs": {
|
||||
"recommended": "おすすめ",
|
||||
"browse": "ブラウズ",
|
||||
"collections": "コレクション",
|
||||
"playlists": "プレイリスト"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "グループ",
|
||||
"all": "すべて",
|
||||
"movies": "映画",
|
||||
"shows": "テレビ番組",
|
||||
"seasons": "シーズン",
|
||||
"episodes": "エピソード",
|
||||
"folders": "フォルダ"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "アプリについて",
|
||||
"openSourceLicenses": "オープンソースライセンス",
|
||||
"versionLabel": "バージョン ${version}",
|
||||
"appDescription": "Flutter製の美しいPlexクライアント",
|
||||
"viewLicensesDescription": "サードパーティライブラリのライセンスを表示"
|
||||
},
|
||||
"serverSelection": {
|
||||
"allServerConnectionsFailed": "どのサーバーにも接続できませんでした。ネットワークを確認してもう一度お試しください。",
|
||||
"noServersFoundForAccount": "${username} (${email})のサーバーが見つかりません",
|
||||
"failedToLoadServers": "サーバーの読み込みに失敗しました: ${error}"
|
||||
},
|
||||
"hubDetail": {
|
||||
"title": "タイトル",
|
||||
"releaseYear": "公開年",
|
||||
"dateAdded": "追加日",
|
||||
"rating": "評価",
|
||||
"noItemsFound": "アイテムが見つかりません"
|
||||
},
|
||||
"logs": {
|
||||
"clearLogs": "ログをクリア",
|
||||
"copyLogs": "ログをコピー",
|
||||
"uploadLogs": "ログをアップロード"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "関連パッケージ",
|
||||
"license": "ライセンス",
|
||||
"licenseNumber": "ライセンス ${number}",
|
||||
"licensesCount": "${count}件のライセンス"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "ライブラリ",
|
||||
"downloads": "ダウンロード",
|
||||
"liveTv": "ライブTV"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "ライブTV",
|
||||
"guide": "番組表",
|
||||
"noChannels": "チャンネルがありません",
|
||||
"noDvr": "どのサーバーにもDVRが設定されていません",
|
||||
"noPrograms": "番組データがありません",
|
||||
"live": "ライブ",
|
||||
"reloadGuide": "番組表を再読込",
|
||||
"now": "現在",
|
||||
"today": "今日",
|
||||
"midnight": "深夜",
|
||||
"overnight": "深夜",
|
||||
"morning": "朝",
|
||||
"daytime": "昼",
|
||||
"evening": "夕方",
|
||||
"lateNight": "深夜",
|
||||
"whatsOn": "放送中",
|
||||
"watchChannel": "チャンネルを視聴",
|
||||
"favorites": "お気に入り",
|
||||
"reorderFavorites": "お気に入りを並べ替え",
|
||||
"joinSession": "進行中のセッションに参加",
|
||||
"watchFromStart": "最初から視聴(${minutes}分前に開始)",
|
||||
"watchLive": "ライブで視聴",
|
||||
"goToLive": "ライブに移動"
|
||||
},
|
||||
"collections": {
|
||||
"title": "コレクション",
|
||||
"collection": "コレクション",
|
||||
"empty": "コレクションは空です",
|
||||
"unknownLibrarySection": "削除できません:不明なライブラリセクション",
|
||||
"deleteCollection": "コレクションを削除",
|
||||
"deleteConfirm": "\"${title}\"を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"deleted": "コレクションを削除しました",
|
||||
"deleteFailed": "コレクションの削除に失敗しました",
|
||||
"deleteFailedWithError": "コレクションの削除に失敗しました: ${error}",
|
||||
"failedToLoadItems": "コレクションアイテムの読み込みに失敗しました: ${error}",
|
||||
"selectCollection": "コレクションを選択",
|
||||
"collectionName": "コレクション名",
|
||||
"enterCollectionName": "コレクション名を入力",
|
||||
"addedToCollection": "コレクションに追加しました",
|
||||
"errorAddingToCollection": "コレクションへの追加に失敗しました",
|
||||
"created": "コレクションを作成しました",
|
||||
"removeFromCollection": "コレクションから削除",
|
||||
"removeFromCollectionConfirm": "\"${title}\"をこのコレクションから削除しますか?",
|
||||
"removedFromCollection": "コレクションから削除しました",
|
||||
"removeFromCollectionFailed": "コレクションからの削除に失敗しました",
|
||||
"removeFromCollectionError": "コレクションからの削除エラー: ${error}",
|
||||
"searchCollections": "コレクションを検索..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "プレイリスト",
|
||||
"playlist": "プレイリスト",
|
||||
"noPlaylists": "プレイリストが見つかりません",
|
||||
"create": "プレイリストを作成",
|
||||
"playlistName": "プレイリスト名",
|
||||
"enterPlaylistName": "プレイリスト名を入力",
|
||||
"delete": "プレイリストを削除",
|
||||
"removeItem": "プレイリストから削除",
|
||||
"smartPlaylist": "スマートプレイリスト",
|
||||
"itemCount": "${count}アイテム",
|
||||
"oneItem": "1アイテム",
|
||||
"emptyPlaylist": "このプレイリストは空です",
|
||||
"deleteConfirm": "プレイリストを削除しますか?",
|
||||
"deleteMessage": "\"${name}\"を削除してもよろしいですか?",
|
||||
"created": "プレイリストを作成しました",
|
||||
"deleted": "プレイリストを削除しました",
|
||||
"itemAdded": "プレイリストに追加しました",
|
||||
"itemRemoved": "プレイリストから削除しました",
|
||||
"selectPlaylist": "プレイリストを選択",
|
||||
"errorCreating": "プレイリストの作成に失敗しました",
|
||||
"errorDeleting": "プレイリストの削除に失敗しました",
|
||||
"errorLoading": "プレイリストの読み込みに失敗しました",
|
||||
"errorAdding": "プレイリストへの追加に失敗しました",
|
||||
"errorReordering": "プレイリストアイテムの並べ替えに失敗しました",
|
||||
"errorRemoving": "プレイリストからの削除に失敗しました"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "一緒に見る",
|
||||
"description": "友達や家族と同期してコンテンツを視聴",
|
||||
"createSession": "セッションを作成",
|
||||
"creating": "作成中...",
|
||||
"joinSession": "セッションに参加",
|
||||
"joining": "参加中...",
|
||||
"controlMode": "コントロールモード",
|
||||
"controlModeQuestion": "誰が再生を制御できますか?",
|
||||
"hostOnly": "ホストのみ",
|
||||
"anyone": "全員",
|
||||
"hostingSession": "セッションをホスト中",
|
||||
"inSession": "セッション中",
|
||||
"sessionCode": "セッションコード",
|
||||
"hostControlsPlayback": "ホストが再生を制御",
|
||||
"anyoneCanControl": "全員が再生を制御可能",
|
||||
"hostControls": "ホストが制御",
|
||||
"anyoneControls": "全員が制御",
|
||||
"participants": "参加者",
|
||||
"host": "ホスト",
|
||||
"hostBadge": "HOST",
|
||||
"youAreHost": "あなたはホストです",
|
||||
"watchingWithOthers": "他の人と視聴中",
|
||||
"endSession": "セッションを終了",
|
||||
"leaveSession": "セッションを退出",
|
||||
"endSessionQuestion": "セッションを終了しますか?",
|
||||
"leaveSessionQuestion": "セッションを退出しますか?",
|
||||
"endSessionConfirm": "すべての参加者のセッションが終了します。",
|
||||
"leaveSessionConfirm": "セッションから退出されます。",
|
||||
"endSessionConfirmOverlay": "すべての参加者の視聴セッションが終了します。",
|
||||
"leaveSessionConfirmOverlay": "視聴セッションから切断されます。",
|
||||
"end": "終了",
|
||||
"leave": "退出",
|
||||
"syncing": "同期中...",
|
||||
"joinWatchSession": "視聴セッションに参加",
|
||||
"enterCodeHint": "5文字のコードを入力",
|
||||
"pasteFromClipboard": "クリップボードから貼り付け",
|
||||
"pleaseEnterCode": "セッションコードを入力してください",
|
||||
"codeMustBe5Chars": "セッションコードは5文字である必要があります",
|
||||
"joinInstructions": "ホストが共有したセッションコードを入力して視聴セッションに参加してください。",
|
||||
"failedToCreate": "セッションの作成に失敗しました",
|
||||
"failedToJoin": "セッションへの参加に失敗しました",
|
||||
"sessionCodeCopied": "セッションコードをクリップボードにコピーしました",
|
||||
"relayUnreachable": "リレーサーバーに到達できません。ISPが接続をブロックしている可能性があります。試すことはできますが、一緒に見る機能が動作しない場合があります。",
|
||||
"reconnectingToHost": "ホストに再接続中...",
|
||||
"currentPlayback": "現在の再生",
|
||||
"joinCurrentPlayback": "現在の再生に参加",
|
||||
"joinCurrentPlaybackDescription": "ホストが現在視聴中のコンテンツに戻る",
|
||||
"failedToOpenCurrentPlayback": "現在の再生を開けませんでした",
|
||||
"participantJoined": "${name}が参加しました",
|
||||
"participantLeft": "${name}が退出しました",
|
||||
"participantPaused": "${name}が一時停止しました",
|
||||
"participantResumed": "${name}が再開しました",
|
||||
"participantSeeked": "${name}がシークしました",
|
||||
"participantBuffering": "${name}がバッファリング中",
|
||||
"waitingForParticipants": "他の参加者の読み込みを待っています...",
|
||||
"recentRooms": "最近のルーム",
|
||||
"renameRoom": "ルーム名を変更",
|
||||
"removeRoom": "削除"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "ダウンロード",
|
||||
"manage": "管理",
|
||||
"tvShows": "テレビ番組",
|
||||
"movies": "映画",
|
||||
"noDownloads": "ダウンロードなし",
|
||||
"noDownloadsDescription": "ダウンロードしたコンテンツはここに表示され、オフラインで視聴できます",
|
||||
"downloadNow": "ダウンロード",
|
||||
"deleteDownload": "ダウンロードを削除",
|
||||
"retryDownload": "ダウンロードを再試行",
|
||||
"downloadQueued": "ダウンロードをキューに追加しました",
|
||||
"serverErrorBitrate": "サーバーエラー — ファイルがリモートストリーミングのビットレート制限を超えている可能性があります",
|
||||
"episodesQueued": "${count}エピソードをダウンロードキューに追加しました",
|
||||
"downloadDeleted": "ダウンロードを削除しました",
|
||||
"deleteConfirm": "\"${title}\"を削除してもよろしいですか?ダウンロードしたファイルがデバイスから削除されます。",
|
||||
"deletingWithProgress": "${title}を削除中... (${current}/${total})",
|
||||
"noDownloadsTree": "ダウンロードなし",
|
||||
"pauseAll": "すべて一時停止",
|
||||
"resumeAll": "すべて再開",
|
||||
"deleteAll": "すべて削除",
|
||||
"selectVersion": "バージョンを選択",
|
||||
"allEpisodes": "すべてのエピソード",
|
||||
"unwatchedOnly": "未視聴のみ",
|
||||
"nextNUnwatched": "次の${count}件の未視聴",
|
||||
"customAmount": "数を指定...",
|
||||
"howManyEpisodes": "何エピソード?",
|
||||
"itemsQueued": "${count}件をダウンロードキューに追加",
|
||||
"keepSynced": "同期を維持",
|
||||
"downloadOnce": "一度だけダウンロード",
|
||||
"keepNUnwatched": "未視聴を${count}件保持",
|
||||
"editSyncRule": "同期ルールを編集",
|
||||
"removeSyncRule": "同期ルールを削除",
|
||||
"removeSyncRuleConfirm": "「${title}」の同期を停止しますか?ダウンロード済みのエピソードは保持されます。",
|
||||
"syncRuleCreated": "同期ルールを作成しました — 未視聴のエピソードを${count}件保持",
|
||||
"syncRuleUpdated": "同期ルールを更新しました",
|
||||
"syncRuleRemoved": "同期ルールを削除しました",
|
||||
"syncedNewEpisodes": "${title}の新しいエピソードを${count}件同期しました",
|
||||
"activeSyncRules": "同期ルール",
|
||||
"noSyncRules": "同期ルールなし",
|
||||
"manageSyncRule": "同期を管理",
|
||||
"editEpisodeCount": "エピソード数",
|
||||
"editSyncFilter": "同期フィルター",
|
||||
"syncAllItems": "すべてのアイテムを同期中",
|
||||
"syncUnwatchedItems": "未視聴のアイテムを同期中",
|
||||
"syncRuleListCreated": "同期ルールを作成しました"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "シェーダー",
|
||||
"noShaderDescription": "映像補正なし",
|
||||
"nvscalerDescription": "よりシャープな映像のためのNVIDIA画像スケーリング",
|
||||
"qualityFast": "高速",
|
||||
"qualityHQ": "高品質",
|
||||
"mode": "モード",
|
||||
"importShader": "シェーダーをインポート",
|
||||
"customShaderDescription": "カスタムGLSLシェーダー",
|
||||
"shaderImported": "シェーダーをインポートしました",
|
||||
"shaderImportFailed": "シェーダーのインポートに失敗しました",
|
||||
"deleteShader": "シェーダーを削除",
|
||||
"deleteShaderConfirm": "\"${name}\"を削除しますか?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "コンパニオンリモート",
|
||||
"connectedTo": "${name}に接続中",
|
||||
"session": {
|
||||
"startingServer": "リモートサーバーを起動中...",
|
||||
"failedToCreate": "リモートサーバーの起動に失敗しました:",
|
||||
"hostAddress": "ホストアドレス",
|
||||
"connected": "接続済み",
|
||||
"serverRunning": "リモートサーバー稼働中",
|
||||
"serverStopped": "リモートサーバー停止中",
|
||||
"serverRunningDescription": "ネットワーク上のモバイルデバイスがこのアプリを検出して接続できます",
|
||||
"serverStoppedDescription": "モバイルデバイスの接続を許可するにはサーバーを起動してください",
|
||||
"usePhoneToControl": "モバイルデバイスでこのアプリを操作できます",
|
||||
"startServer": "サーバーを起動",
|
||||
"stopServer": "サーバーを停止",
|
||||
"minimize": "最小化"
|
||||
},
|
||||
"pairing": {
|
||||
"discoveryDescription": "同じPlexアカウントでPlezyを実行しているネットワーク上のデバイスが自動的に表示されます",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"connecting": "接続中...",
|
||||
"searchingForDevices": "デバイスを検索中...",
|
||||
"noDevicesFound": "ネットワーク上にデバイスが見つかりません",
|
||||
"noDevicesHint": "デスクトップでPlezyが開いていること、両方のデバイスが同じWiFiネットワーク上にあることを確認してください",
|
||||
"availableDevices": "利用可能なデバイス",
|
||||
"manualConnection": "手動接続",
|
||||
"cryptoInitFailed": "安全な接続を初期化できませんでした。Plexアカウントにサインインしていることを確認してください。",
|
||||
"validationHostRequired": "ホストアドレスを入力してください",
|
||||
"validationHostFormat": "形式はIP:ポートである必要があります(例: 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "接続がタイムアウトしました。両方のデバイスが同じネットワーク上にあることを確認してください。",
|
||||
"sessionNotFound": "デバイスが見つかりません。ホストでPlezyが実行されていることを確認してください。",
|
||||
"authFailed": "認証に失敗しました。両方のデバイスが同じPlexアカウントを使用していることを確認してください。",
|
||||
"failedToConnect": "接続に失敗しました: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
"disconnectConfirm": "リモートセッションから切断しますか?",
|
||||
"reconnecting": "再接続中...",
|
||||
"attemptOf": "試行 ${current}/5",
|
||||
"retryNow": "今すぐ再試行",
|
||||
"tabRemote": "リモート",
|
||||
"tabPlay": "再生",
|
||||
"tabMore": "その他",
|
||||
"menu": "メニュー",
|
||||
"tabNavigation": "タブナビゲーション",
|
||||
"tabDiscover": "探す",
|
||||
"tabLibraries": "ライブラリ",
|
||||
"tabSearch": "検索",
|
||||
"tabDownloads": "ダウンロード",
|
||||
"tabSettings": "設定",
|
||||
"previous": "前へ",
|
||||
"playPause": "再生/一時停止",
|
||||
"next": "次へ",
|
||||
"seekBack": "巻き戻し",
|
||||
"stop": "停止",
|
||||
"seekForward": "早送り",
|
||||
"volume": "音量",
|
||||
"volumeDown": "下げる",
|
||||
"volumeUp": "上げる",
|
||||
"fullscreen": "フルスクリーン",
|
||||
"subtitles": "字幕",
|
||||
"audio": "音声",
|
||||
"searchHint": "デスクトップで検索..."
|
||||
}
|
||||
},
|
||||
"videoSettings": {
|
||||
"playbackSettings": "再生設定",
|
||||
"playbackSpeed": "再生速度",
|
||||
"sleepTimer": "スリープタイマー",
|
||||
"audioSync": "音声同期",
|
||||
"subtitleSync": "字幕同期",
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "音声出力",
|
||||
"performanceOverlay": "パフォーマンスオーバーレイ",
|
||||
"audioPassthrough": "オーディオパススルー",
|
||||
"audioNormalization": "音声正規化"
|
||||
},
|
||||
"externalPlayer": {
|
||||
"title": "外部プレーヤー",
|
||||
"useExternalPlayer": "外部プレーヤーを使用",
|
||||
"useExternalPlayerDescription": "内蔵プレーヤーの代わりに外部アプリで動画を開く",
|
||||
"selectPlayer": "プレーヤーを選択",
|
||||
"customPlayers": "カスタムプレーヤー",
|
||||
"systemDefault": "システムデフォルト",
|
||||
"addCustomPlayer": "カスタムプレーヤーを追加",
|
||||
"playerName": "プレーヤー名",
|
||||
"playerCommand": "コマンド",
|
||||
"playerPackage": "パッケージ名",
|
||||
"playerUrlScheme": "URLスキーム",
|
||||
"off": "オフ",
|
||||
"launchFailed": "外部プレーヤーの起動に失敗しました",
|
||||
"appNotInstalled": "${name}がインストールされていません",
|
||||
"playInExternalPlayer": "外部プレーヤーで再生"
|
||||
},
|
||||
"metadataEdit": {
|
||||
"editMetadata": "編集...",
|
||||
"screenTitle": "メタデータを編集",
|
||||
"basicInfo": "基本情報",
|
||||
"artwork": "アートワーク",
|
||||
"advancedSettings": "詳細設定",
|
||||
"title": "タイトル",
|
||||
"sortTitle": "ソートタイトル",
|
||||
"originalTitle": "原題",
|
||||
"releaseDate": "公開日",
|
||||
"contentRating": "コンテンツレーティング",
|
||||
"studio": "スタジオ",
|
||||
"tagline": "タグライン",
|
||||
"summary": "あらすじ",
|
||||
"poster": "ポスター",
|
||||
"background": "背景",
|
||||
"logo": "ロゴ",
|
||||
"squareArt": "正方形アート",
|
||||
"selectPoster": "ポスターを選択",
|
||||
"selectBackground": "背景を選択",
|
||||
"selectLogo": "ロゴを選択",
|
||||
"selectSquareArt": "正方形アートを選択",
|
||||
"fromUrl": "URLから",
|
||||
"uploadFile": "ファイルをアップロード",
|
||||
"enterImageUrl": "画像URLを入力",
|
||||
"imageUrl": "画像URL",
|
||||
"metadataUpdated": "メタデータを更新しました",
|
||||
"metadataUpdateFailed": "メタデータの更新に失敗しました",
|
||||
"artworkUpdated": "アートワークを更新しました",
|
||||
"artworkUpdateFailed": "アートワークの更新に失敗しました",
|
||||
"noArtworkAvailable": "アートワークがありません",
|
||||
"notSet": "未設定",
|
||||
"libraryDefault": "ライブラリのデフォルト",
|
||||
"accountDefault": "アカウントのデフォルト",
|
||||
"seriesDefault": "シリーズのデフォルト",
|
||||
"episodeSorting": "エピソードの並べ替え",
|
||||
"oldestFirst": "古い順",
|
||||
"newestFirst": "新しい順",
|
||||
"keep": "保持",
|
||||
"allEpisodes": "すべてのエピソード",
|
||||
"latestEpisodes": "最新${count}エピソード",
|
||||
"latestEpisode": "最新エピソード",
|
||||
"episodesAddedPastDays": "過去${count}日間に追加されたエピソード",
|
||||
"deleteAfterPlaying": "再生後にエピソードを削除",
|
||||
"never": "しない",
|
||||
"afterADay": "1日後",
|
||||
"afterAWeek": "1週間後",
|
||||
"afterAMonth": "1ヶ月後",
|
||||
"onNextRefresh": "次回更新時",
|
||||
"seasons": "シーズン",
|
||||
"show": "表示",
|
||||
"hide": "非表示",
|
||||
"episodeOrdering": "エピソードの順序",
|
||||
"tmdbAiring": "The Movie Database(放送順)",
|
||||
"tvdbAiring": "TheTVDB(放送順)",
|
||||
"tvdbAbsolute": "TheTVDB(絶対順)",
|
||||
"metadataLanguage": "メタデータの言語",
|
||||
"useOriginalTitle": "原題を使用",
|
||||
"preferredAudioLanguage": "優先音声言語",
|
||||
"preferredSubtitleLanguage": "優先字幕言語",
|
||||
"subtitleMode": "字幕自動選択モード",
|
||||
"manuallySelected": "手動選択",
|
||||
"shownWithForeignAudio": "外国語音声時に表示",
|
||||
"alwaysEnabled": "常に有効",
|
||||
"tags": "タグ",
|
||||
"addTag": "タグを追加",
|
||||
"genre": "ジャンル",
|
||||
"director": "監督",
|
||||
"writer": "脚本",
|
||||
"producer": "プロデューサー",
|
||||
"country": "国",
|
||||
"collection": "コレクション",
|
||||
"label": "ラベル",
|
||||
"style": "スタイル",
|
||||
"mood": "ムード"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "サーバータスク",
|
||||
"failedToLoad": "タスクの読み込みに失敗しました",
|
||||
"noTasks": "実行中のタスクはありません"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "接続済み",
|
||||
"connectedAs": "@${username} として接続済み",
|
||||
"disconnectConfirm": "Trakt アカウントを切断しますか?",
|
||||
"disconnectConfirmBody": "Plezy から Trakt への再生イベント送信が停止します。いつでも再接続できます。",
|
||||
"scrobble": "リアルタイムのスクロブル",
|
||||
"scrobbleDescription": "再生中に再生・一時停止・停止イベントを Trakt に送信します。",
|
||||
"watchedSync": "視聴済みステータスを同期",
|
||||
"watchedSyncDescription": "Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。",
|
||||
"deviceCodeTitle": "Trakt で Plezy をアクティベート",
|
||||
"deviceCodeBody": "${url} を開き、このコードを入力してください:",
|
||||
"openTraktActivate": "アクティベートするには Trakt を開く",
|
||||
"waitingForAuthorization": "認証を待機中…",
|
||||
"codeCopied": "コードをコピーしました",
|
||||
"connectFailed": "Trakt に接続できませんでした。もう一度お試しください。"
|
||||
}
|
||||
}
|
||||
+204
-121
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "Plex 계정으로 로그인",
|
||||
"showQRCode": "QR 코드",
|
||||
"authenticate": "인증",
|
||||
"debugEnterToken": "디버깅을 위해 Plex 토큰을 입력하세요.",
|
||||
"plexTokenLabel": "Plex 인증 토큰",
|
||||
"plexTokenHint": "Plex.tv 토큰을 입력하세요",
|
||||
"authenticationTimeout": "인증 시간이 초과되었습니다. 다시 시도해 주세요.",
|
||||
"scanQRToSignIn": "QR 코드를 스캔하여 로그인",
|
||||
"waitingForAuth": "인증 대기 중... 브라우저에서 로그인을 완료해 주세요.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "삭제",
|
||||
"shuffle": "무작위 재생",
|
||||
"addTo": "추가하기...",
|
||||
"remove": "삭제",
|
||||
"paste": "붙여넣기",
|
||||
"createNew": "새로 만들기",
|
||||
"connect": "연결",
|
||||
"disconnect": "연결 해제",
|
||||
"play": "재생",
|
||||
@@ -46,7 +42,6 @@
|
||||
"settings": "설정",
|
||||
"mute": "음소거",
|
||||
"ok": "확인",
|
||||
"loading": "로딩 중...",
|
||||
"reconnect": "다시 연결",
|
||||
"exitConfirmTitle": "앱을 종료하시겠습니까?",
|
||||
"exitConfirmMessage": "정말 종료하시겠습니까?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "서버 새로고침 중...",
|
||||
"loadingServers": "서버 로딩 중...",
|
||||
"connectingToServers": "서버 연결 중...",
|
||||
"startingOfflineMode": "오프라인 모드 시작 중..."
|
||||
"startingOfflineMode": "오프라인 모드 시작 중...",
|
||||
"loading": "로딩 중..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "라이선스",
|
||||
"switchProfile": "프로필 전환",
|
||||
"subtitleStyling": "자막 스타일 설정",
|
||||
"mpvConfig": "MPV 설정",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "로그"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "설정",
|
||||
"supportDeveloper": "Plezy 후원하기",
|
||||
"supportDeveloperDescription": "Liberapay로 후원하여 개발 지원",
|
||||
"language": "언어",
|
||||
"theme": "테마",
|
||||
"appearance": "외관",
|
||||
"videoPlayback": "비디오 재생",
|
||||
"videoPlaybackDescription": "재생 동작 구성",
|
||||
"advanced": "고급",
|
||||
"episodePosterMode": "에피소드 포스터 스타일",
|
||||
"seriesPoster": "시리즈 포스터",
|
||||
"seriesPosterDescription": "모든 에피소드에 시리즈 포스터 표시",
|
||||
"seasonPoster": "시즌 포스터",
|
||||
"seasonPosterDescription": "에피소드에 시즌별 포스터 표시",
|
||||
"episodeThumbnail": "에피소드 썸네일",
|
||||
"episodeThumbnailDescription": "16:9 에피소드 스크린샷 썸네일 표시",
|
||||
"episodeThumbnail": "썸네일",
|
||||
"showHeroSectionDescription": "홈 화면에 주요 콘텐츠 캐러셀(슬라이드) 표시",
|
||||
"secondsLabel": "초",
|
||||
"minutesLabel": "분",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "분",
|
||||
"durationHint": "기간 입력 (${min}-${max})",
|
||||
"systemTheme": "시스템 설정",
|
||||
"systemThemeDescription": "시스템 설정에 따름",
|
||||
"lightTheme": "라이트 모드",
|
||||
"darkTheme": "다크 모드",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "OLED 화면을 위한 순수 검정",
|
||||
"libraryDensity": "라이브러리 표시 밀도",
|
||||
"compact": "좁게",
|
||||
"compactDescription": "카드를 작게 표시하여 더 많은 항목을 보여줍니다.",
|
||||
"normal": "보통",
|
||||
"normalDescription": "기본 크기",
|
||||
"comfortable": "넓게",
|
||||
"comfortableDescription": "카드를 크게 표시하여 더 적은 항목을 보여줍니다.",
|
||||
"viewMode": "보기 모드",
|
||||
"gridView": "그리드 보기",
|
||||
"gridViewDescription": "항목을 그리드 레이아웃으로 표시합니다",
|
||||
"listView": "목록 보기",
|
||||
"listViewDescription": "항목을 목록 레이아웃으로 표시합니다",
|
||||
"showHeroSection": "주요 추천 영역 표시",
|
||||
"useGlobalHubs": "Plex 홈 레이아웃 사용",
|
||||
"useGlobalHubsDescription": "공식 Plex 클라이언트처럼 홈 페이지 허브를 표시합니다. 끄면 라이브러리별 추천이 대신 표시됩니다.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "아직 시청하지 않은 에피소드의 썸네일을 흐리게 하고 설명을 숨깁니다",
|
||||
"playerBackend": "플레이어 백엔드",
|
||||
"exoPlayer": "ExoPlayer (권장)",
|
||||
"exoPlayerDescription": "더 나은 하드웨어 지원을 제공하는 Android 네이티브 플레이어",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "더 많은 기능과 ASS 자막을 지원하는 고급 플레이어",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "하드웨어 디코딩",
|
||||
"hardwareDecodingDescription": "가능한 경우 하드웨어 가속을 사용합니다",
|
||||
"bufferSize": "버퍼 크기",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "자막의 외형을 사용자 설정",
|
||||
"smallSkipDuration": "짧은 건너뛰기 시간",
|
||||
"largeSkipDuration": "긴 건너뛰기 시간",
|
||||
"rewindOnResume": "재개 시 되감기",
|
||||
"secondsUnit": "${seconds}초",
|
||||
"defaultSleepTimer": "기본 취침 타이머",
|
||||
"minutesUnit": "${minutes}분",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "사용자 정의 키보드 단축키",
|
||||
"videoPlayerNavigation": "비디오 플레이어 탐색",
|
||||
"videoPlayerNavigationDescription": "방향 키를 사용하여 비디오 플레이어 컨트롤 탐색",
|
||||
"watchTogetherRelay": "함께 보기 릴레이",
|
||||
"watchTogetherRelayDescription": "함께 보기에 사용할 사용자 지정 릴레이 서버를 설정합니다. 모든 참가자가 동일한 서버를 사용해야 합니다.",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"crashReporting": "충돌 보고",
|
||||
"crashReportingDescription": "앱 개선을 위해 충돌 보고서 전송",
|
||||
"debugLogging": "디버그 로깅",
|
||||
"debugLoggingDescription": "문제 해결을 위해 상세 로깅 활성화",
|
||||
"viewLogs": "로그 보기",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "엔딩 크레딧 자동 건너뛰기 후 다음 에피소드 재생",
|
||||
"autoSkipDelay": "자동 건너뛰기 지연",
|
||||
"autoSkipDelayDescription": "자동 건너뛰기 전 ${seconds} 초 대기",
|
||||
"introPattern": "인트로 마커 패턴",
|
||||
"introPatternDescription": "챕터 제목에서 인트로 마커를 인식하는 정규식 패턴",
|
||||
"creditsPattern": "크레딧 마커 패턴",
|
||||
"creditsPatternDescription": "챕터 제목에서 크레딧 마커를 인식하는 정규식 패턴",
|
||||
"invalidRegex": "잘못된 정규식",
|
||||
"downloads": "다운로드",
|
||||
"downloadLocationDescription": "다운로드 콘텐츠 저장 위치 선택",
|
||||
"downloadLocationDefault": "기본값 (앱 저장소)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "폴더 선택 실패",
|
||||
"downloadOnWifiOnly": "WiFi 연결 시에만 다운로드",
|
||||
"downloadOnWifiOnlyDescription": "셀룰러 데이터 사용 시 다운로드 불가",
|
||||
"autoRemoveWatchedDownloads": "시청한 다운로드 자동 삭제",
|
||||
"autoRemoveWatchedDownloadsDescription": "시청 완료로 표시된 에피소드와 영화의 다운로드를 자동으로 삭제",
|
||||
"cellularDownloadBlocked": "셀룰러 데이터에서 다운로드가 차단 되었습니다. WiFi에 연결하거나 설정을 변경하세요.",
|
||||
"maxVolume": "최대 볼륨",
|
||||
"maxVolumeDescription": "조용한 미디어를 위해 100% 이상의 볼륨 허용",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Discord에서 시청 중인 콘텐츠 표시",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Trakt와 시청 기록 동기화",
|
||||
"companionRemoteServer": "컴패니언 리모트 서버",
|
||||
"companionRemoteServerDescription": "네트워크의 모바일 기기가 이 앱을 제어할 수 있도록 허용",
|
||||
"autoPip": "자동 PIP 모드",
|
||||
"autoPipDescription": "재생 중 앱을 나갈 때 자동으로 PIP 모드로 전환",
|
||||
"matchContentFrameRate": "콘텐츠 프레임 레이트 맞춤",
|
||||
"matchContentFrameRateDescription": "비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "주사율 맞춤",
|
||||
"matchRefreshRateDescription": "전체 화면에서 비디오 콘텐츠에 맞게 디스플레이 주사율을 전환합니다",
|
||||
"matchDynamicRange": "다이나믹 레인지 맞춤",
|
||||
"matchDynamicRangeDescription": "HDR 콘텐츠에 대해 자동으로 HDR을 활성화하고 플레이어를 나갈 때 SDR로 복원합니다",
|
||||
"displaySwitchDelay": "디스플레이 전환 지연",
|
||||
"tunneledPlayback": "터널 재생",
|
||||
"tunneledPlaybackDescription": "하드웨어 가속 비디오 터널링을 사용합니다. HDR 콘텐츠에서 소리만 나고 검은 화면이 보이면 비활성화하세요",
|
||||
"requireProfileSelectionOnOpen": "앱 실행 시 프로필 선택",
|
||||
"requireProfileSelectionOnOpenDescription": "앱을 열 때마다 프로필 선택 화면을 표시합니다",
|
||||
"confirmExitOnBack": "종료 전 확인",
|
||||
"confirmExitOnBackDescription": "뒤로 버튼을 눌러 앱을 종료할 때 확인 대화상자를 표시합니다"
|
||||
"confirmExitOnBackDescription": "뒤로 버튼을 눌러 앱을 종료할 때 확인 대화상자를 표시합니다",
|
||||
"forceTvMode": "TV 모드 강제 사용",
|
||||
"forceTvModeDescription": "자동 감지와 관계없이 TV 레이아웃을 사용합니다. leanback 기능을 보고하지 않는 Android TV 기기에 유용합니다. 변경 시 앱이 다시 시작됩니다.",
|
||||
"autoHidePerformanceOverlay": "성능 오버레이 자동 숨기기",
|
||||
"autoHidePerformanceOverlayDescription": "재생 컨트롤과 함께 성능 오버레이를 페이드 처리",
|
||||
"showNavBarLabels": "내비게이션 바 라벨 표시",
|
||||
"showNavBarLabelsDescription": "내비게이션 바 아이콘 아래에 텍스트 라벨을 표시합니다",
|
||||
"liveTvDefaultFavorites": "즐겨찾기 채널 기본 설정",
|
||||
"liveTvDefaultFavoritesDescription": "라이브 TV를 열 때 즐겨찾기 채널만 표시",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "영화, 시리즈, 음악 등을 검색하세요...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "인트로/크레딧 건너뛰기"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "PIN 입력",
|
||||
"showPin": "PIN 표시",
|
||||
"hidePin": "PIN 숨기기"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "파일 정보",
|
||||
"video": "비디오",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "색상 원색",
|
||||
"chromaSubsampling": "채도 서브샘플링",
|
||||
"channels": "채널",
|
||||
"subtitles": "자막",
|
||||
"overallBitrate": "전체 비트레이트",
|
||||
"path": "경로",
|
||||
"size": "크기",
|
||||
"container": "컨테이너",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "모든 에피소드와 파일이 포함됩니다.",
|
||||
"mediaDeletedSuccessfully": "미디어 항목이 성공적으로 삭제되었습니다",
|
||||
"mediaFailedToDelete": "미디어 항목 삭제 실패",
|
||||
"rate": "평가"
|
||||
"rate": "평가",
|
||||
"playFromBeginning": "처음부터 재생",
|
||||
"playVersion": "버전 재생..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, 영화",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "회전 잠금 해제",
|
||||
"timerActive": "타이머 활성화됨",
|
||||
"playbackWillPauseIn": "재생이 ${duration} 후에 일시 중지 됩니다",
|
||||
"sleepTimerCompleted": "수면 타이머 완료됨 - 재생이 일시 중지되었습니다",
|
||||
"stillWatching": "아직 시청 중이신가요?",
|
||||
"pausingIn": "${seconds}초 후 일시 정지",
|
||||
"continueWatching": "계속",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "음소거",
|
||||
"unmuteButton": "음소거 해제",
|
||||
"settingsButton": "동영상 설정",
|
||||
"audioTrackButton": "음원 트랙",
|
||||
"subtitlesButton": "자막",
|
||||
"tracksButton": "오디오 및 자막",
|
||||
"chaptersButton": "챕터",
|
||||
"versionsButton": "동영상 버전",
|
||||
"pipButton": "픽처 인 픽처 모드",
|
||||
"aspectRatioButton": "화면비율",
|
||||
"ambientLighting": "주변 조명",
|
||||
"ambientLightingOn": "주변 조명 켜기",
|
||||
"ambientLightingOff": "주변 조명 끄기",
|
||||
"fullscreenButton": "전체화면",
|
||||
"exitFullscreenButton": "전체화면 종료",
|
||||
"alwaysOnTopButton": "창 최상위 고정",
|
||||
"rotationLockButton": "회전 잠금",
|
||||
"lockScreen": "화면 잠금",
|
||||
"screenLockButton": "화면 잠금",
|
||||
"longPressToUnlock": "길게 눌러 잠금 해제",
|
||||
"timelineSlider": "타임라인",
|
||||
"volumeSlider": "볼륨 조절",
|
||||
"endsAt": "${time}에 종료",
|
||||
"pipActive": "화면 속 화면으로 재생 중",
|
||||
"pipFailed": "화면 속 화면 모드를 시작할 수 없습니다",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Android 8.0 이상이 필요합니다",
|
||||
"iosVersion": "iOS 15.0 이상이 필요합니다",
|
||||
"permissionDisabled": "화면 속 화면 권한이 비활성화되어 있습니다. 설정 > 앱 > Plezy > 화면 속 화면에서 활성화하세요",
|
||||
"notSupported": "이 기기는 화면 속 화면 모드를 지원하지 않습니다",
|
||||
"voSwitchFailed": "화면 속 화면을 위한 비디오 출력 전환에 실패했습니다",
|
||||
"failed": "화면 속 화면 모드를 시작할 수 없습니다",
|
||||
"unknown": "오류가 발생했습니다: ${error}"
|
||||
},
|
||||
"chapters": "챕터",
|
||||
"noChaptersAvailable": "사용 가능한 챕터가 없습니다",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "재생 대기열",
|
||||
"noQueueItems": "대기열에 항목이 없습니다",
|
||||
"searchSubtitles": "자막 검색",
|
||||
"language": "언어",
|
||||
"noSubtitlesFound": "자막을 찾을 수 없습니다",
|
||||
"subtitleDownloaded": "자막이 다운로드되었습니다",
|
||||
"subtitleDownloadFailed": "자막 다운로드에 실패했습니다",
|
||||
"searchLanguages": "언어 검색..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "관리자",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "시청 안 함으로 표시됨",
|
||||
"markedAsWatchedOffline": "시청 완료로 표시됨 (연결 시 동기화됨)",
|
||||
"markedAsUnwatchedOffline": "미시청으로 표시됨 (연결 시 동기화됨)",
|
||||
"autoRemovedWatchedDownload": "자동 삭제됨: ${title}",
|
||||
"removedFromContinueWatching": "계속 시청 목록에서 제거됨",
|
||||
"errorLoading": "오류: ${error}",
|
||||
"fileInfoNotAvailable": "파일 정보가 없습니다",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "재생 대기열 생성 실패 - 항목 없음",
|
||||
"failedPlayback": "${action}을(를) 수행할 수 없습니다: ${error}",
|
||||
"switchingToCompatiblePlayer": "호환되는 플레이어로 전환 중...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "재생 실패",
|
||||
"serverLimitBody": "서버에서 오류를 반환했습니다 (HTTP 500). 일반적으로 이는 서버 소유자가 대역폭 또는 트랜스코딩 제한을 설정하여 세션을 거부하고 있다는 의미입니다. 클라이언트에서 할 수 있는 일은 없습니다 — 서버 소유자가 설정을 조정해야 합니다.",
|
||||
"logsUploaded": "로그 업로드 완료",
|
||||
"logsUploadFailed": "로그 업로드 실패",
|
||||
"logId": "로그 ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "스타일 옵션",
|
||||
"text": "텍스트",
|
||||
"border": "테두리",
|
||||
"background": "배경",
|
||||
"fontSize": "글자 크기",
|
||||
"textColor": "텍스트 색상",
|
||||
"borderSize": "테두리 크기",
|
||||
"borderColor": "테두리 색상",
|
||||
"backgroundOpacity": "배경 불투명도",
|
||||
"backgroundColor": "배경색",
|
||||
"position": "Position"
|
||||
"position": "위치",
|
||||
"assOverride": "ASS 오버라이드",
|
||||
"bold": "굵게",
|
||||
"italic": "기울임꼴"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV 설정",
|
||||
"title": "mpv 설정",
|
||||
"description": "고급 비디오 플레이어 설정",
|
||||
"properties": "속성",
|
||||
"presets": "사전 설정",
|
||||
"noProperties": "설정된 속성이 없습니다",
|
||||
"noPresets": "저장된 사전 설정이 없습니다",
|
||||
"addProperty": "속성 추가",
|
||||
"editProperty": "속성 편집",
|
||||
"deleteProperty": "속성 삭제",
|
||||
"propertyKey": "속성 키",
|
||||
"propertyKeyHint": "예: hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "속성값",
|
||||
"propertyValueHint": "예: auto, 256000000",
|
||||
"saveAsPreset": "프리셋으로 저장...",
|
||||
"presetName": "프리셋 이름",
|
||||
"presetNameHint": "이 프리셋의 이름을 입력하세요",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "프리셋이 로드 되었습니다",
|
||||
"presetDeleted": "프리셋이 삭제 되었습니다",
|
||||
"confirmDeletePreset": "이 프리셋을 삭제 하시겠습니까?",
|
||||
"confirmDeleteProperty": "이 속성을 삭제 하시겠습니까?",
|
||||
"entriesCount": "${count} 항목"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "확인"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "개요",
|
||||
"cast": "출연진",
|
||||
"extras": "예고편 및 부가영상",
|
||||
"seasons": "시즌 수",
|
||||
"studio": "제작사",
|
||||
"rating": "연령 등급",
|
||||
"episodeCount": "${count} 편",
|
||||
"watchedProgress": "${watched}/${total} 편 시청 완료",
|
||||
"movie": "영화",
|
||||
"tvShow": "TV 시리즈",
|
||||
"minutesLeft": "${minutes}분 남음"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "재생 목록"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "그룹",
|
||||
"all": "전체",
|
||||
"movies": "영화",
|
||||
"shows": "TV 프로그램",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "로그 지우기",
|
||||
"copyLogs": "로그 복사",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "오류:",
|
||||
"stackTrace": "스택 추적 (Stack Trace):"
|
||||
"uploadLogs": "로그 업로드"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "관련 소프트웨어 패키지",
|
||||
@@ -565,20 +590,12 @@
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "실시간 TV",
|
||||
"channels": "채널",
|
||||
"guide": "편성표",
|
||||
"noChannels": "사용 가능한 채널이 없습니다",
|
||||
"noDvr": "서버에 DVR이 구성되어 있지 않습니다",
|
||||
"tuneFailed": "채널 튜닝에 실패했습니다",
|
||||
"loading": "채널 로딩 중...",
|
||||
"nowPlaying": "현재 재생 중",
|
||||
"noPrograms": "프로그램 데이터가 없습니다",
|
||||
"channelNumber": "채널 ${number}",
|
||||
"live": "실시간",
|
||||
"hd": "HD",
|
||||
"premiere": "신규",
|
||||
"reloadGuide": "편성표 새로고침",
|
||||
"allChannels": "전체 채널",
|
||||
"now": "지금",
|
||||
"today": "오늘",
|
||||
"midnight": "자정",
|
||||
@@ -588,7 +605,13 @@
|
||||
"evening": "저녁",
|
||||
"lateNight": "심야 방송",
|
||||
"whatsOn": "지금 방송 중",
|
||||
"watchChannel": "채널 시청"
|
||||
"watchChannel": "채널 시청",
|
||||
"favorites": "즐겨찾기",
|
||||
"reorderFavorites": "즐겨찾기 순서 변경",
|
||||
"joinSession": "진행 중인 세션 참여",
|
||||
"watchFromStart": "처음부터 시청 (${minutes}분 전 시작)",
|
||||
"watchLive": "실시간 시청",
|
||||
"goToLive": "실시간으로 이동"
|
||||
},
|
||||
"collections": {
|
||||
"title": "컬렉션",
|
||||
@@ -602,7 +625,6 @@
|
||||
"deleteFailedWithError": "컬렉션 삭제 실패: ${error}",
|
||||
"failedToLoadItems": "컬렉션 항목 로드 실패: ${error}",
|
||||
"selectCollection": "컬렉션 선택",
|
||||
"createNewCollection": "새 컬렉션 생성",
|
||||
"collectionName": "컬렉션 이름",
|
||||
"enterCollectionName": "컬렉션 이름 입력",
|
||||
"addedToCollection": "컬렉션에 추가됨",
|
||||
@@ -612,7 +634,8 @@
|
||||
"removeFromCollectionConfirm": "${title}을/를 이 컬렉션에서 제거 하시겠습니까?",
|
||||
"removedFromCollection": "컬렉션에서 제거됨",
|
||||
"removeFromCollectionFailed": "컬렉션에서 제거 실패",
|
||||
"removeFromCollectionError": "컬렉션에서 제거 중 오류 발생: ${error}"
|
||||
"removeFromCollectionError": "컬렉션에서 제거 중 오류 발생: ${error}",
|
||||
"searchCollections": "컬렉션 검색..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "플레이리스트",
|
||||
@@ -634,7 +657,6 @@
|
||||
"itemAdded": "재생 목록에 추가 되었습니다",
|
||||
"itemRemoved": "재생 목록에서 제거됨",
|
||||
"selectPlaylist": "재생 목록 선택",
|
||||
"createNewPlaylist": "새 재생 목록 생성",
|
||||
"errorCreating": "재생 목록 생성 실패",
|
||||
"errorDeleting": "재생 목록 삭제 실패",
|
||||
"errorLoading": "재생 목록 로드 실패",
|
||||
@@ -677,18 +699,30 @@
|
||||
"leave": "이탈",
|
||||
"syncing": "동기화 중...",
|
||||
"joinWatchSession": "시청 세션에 참여",
|
||||
"enterCodeHint": "8자리 코드 입력",
|
||||
"enterCodeHint": "5자리 코드 입력",
|
||||
"pasteFromClipboard": "클립보드에서 붙여넣기",
|
||||
"pleaseEnterCode": "세션 코드를 입력하세요",
|
||||
"codeMustBe8Chars": "세션 코드는 반드시 8자리여야 합니다",
|
||||
"codeMustBe5Chars": "세션 코드는 반드시 5자리여야 합니다",
|
||||
"joinInstructions": "호스트가 공유한 세션 코드를 입력하여 시청 세션에 참여하세요.",
|
||||
"failedToCreate": "세션 생성 실패",
|
||||
"failedToJoin": "세션 참여 실패",
|
||||
"sessionCodeCopied": "세션 코드가 클립보드에 복사되었습니다",
|
||||
"relayUnreachable": "릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다.",
|
||||
"reconnectingToHost": "호스트에 재연결 중...",
|
||||
"currentPlayback": "현재 재생",
|
||||
"joinCurrentPlayback": "현재 재생 참여",
|
||||
"joinCurrentPlaybackDescription": "호스트가 현재 보고 있는 항목으로 돌아갑니다",
|
||||
"failedToOpenCurrentPlayback": "현재 재생을 열 수 없습니다",
|
||||
"participantJoined": "${name}님이 참여했습니다",
|
||||
"participantLeft": "${name}님이 나갔습니다"
|
||||
"participantLeft": "${name}님이 나갔습니다",
|
||||
"participantPaused": "${name}님이 일시정지했습니다",
|
||||
"participantResumed": "${name}님이 재생했습니다",
|
||||
"participantSeeked": "${name}님이 탐색했습니다",
|
||||
"participantBuffering": "${name}님이 버퍼링 중입니다",
|
||||
"waitingForParticipants": "다른 참가자의 로딩을 기다리는 중...",
|
||||
"recentRooms": "최근 방",
|
||||
"renameRoom": "방 이름 변경",
|
||||
"removeRoom": "제거"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "다운로드",
|
||||
@@ -701,6 +735,7 @@
|
||||
"deleteDownload": "다운로드 삭제",
|
||||
"retryDownload": "다운로드 재시도",
|
||||
"downloadQueued": "다운로드 대기 중",
|
||||
"serverErrorBitrate": "서버 오류 — 파일이 원격 스트리밍 비트레이트 제한을 초과할 수 있습니다",
|
||||
"episodesQueued": "${count} 에피소드가 다운로드 대기열에 추가 되었습니다",
|
||||
"downloadDeleted": "다운로드 삭제됨",
|
||||
"deleteConfirm": "\"${title}\"를 삭제 하시겠습니까? 다운로드한 파일이 기기에서 삭제됩니다.",
|
||||
@@ -708,7 +743,32 @@
|
||||
"noDownloadsTree": "다운로드 없음",
|
||||
"pauseAll": "모두 일시정지",
|
||||
"resumeAll": "모두 재개",
|
||||
"deleteAll": "모두 삭제"
|
||||
"deleteAll": "모두 삭제",
|
||||
"selectVersion": "버전 선택",
|
||||
"allEpisodes": "모든 에피소드",
|
||||
"unwatchedOnly": "시청하지 않은 것만",
|
||||
"nextNUnwatched": "다음 ${count}개 미시청",
|
||||
"customAmount": "직접 입력...",
|
||||
"howManyEpisodes": "몇 개의 에피소드?",
|
||||
"itemsQueued": "${count}개 항목이 다운로드 대기열에 추가됨",
|
||||
"keepSynced": "동기화 유지",
|
||||
"downloadOnce": "한 번만 다운로드",
|
||||
"keepNUnwatched": "미시청 ${count}개 유지",
|
||||
"editSyncRule": "동기화 규칙 편집",
|
||||
"removeSyncRule": "동기화 규칙 제거",
|
||||
"removeSyncRuleConfirm": "\"${title}\" 동기화를 중단하시겠습니까? 다운로드된 에피소드는 유지됩니다.",
|
||||
"syncRuleCreated": "동기화 규칙 생성됨 — 미시청 에피소드 ${count}개 유지",
|
||||
"syncRuleUpdated": "동기화 규칙 업데이트됨",
|
||||
"syncRuleRemoved": "동기화 규칙 제거됨",
|
||||
"syncedNewEpisodes": "${title}의 새 에피소드 ${count}개 동기화됨",
|
||||
"activeSyncRules": "동기화 규칙",
|
||||
"noSyncRules": "동기화 규칙 없음",
|
||||
"manageSyncRule": "동기화 관리",
|
||||
"editEpisodeCount": "에피소드 수",
|
||||
"editSyncFilter": "동기화 필터",
|
||||
"syncAllItems": "모든 항목 동기화 중",
|
||||
"syncUnwatchedItems": "시청하지 않은 항목 동기화 중",
|
||||
"syncRuleListCreated": "동기화 규칙이 생성되었습니다"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "셰이더",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "더 선명한 비디오를 위한 NVIDIA 이미지 스케일링",
|
||||
"qualityFast": "빠름",
|
||||
"qualityHQ": "고품질",
|
||||
"mode": "모드"
|
||||
"mode": "모드",
|
||||
"importShader": "셰이더 가져오기",
|
||||
"customShaderDescription": "사용자 정의 GLSL 셰이더",
|
||||
"shaderImported": "셰이더를 가져왔습니다",
|
||||
"shaderImportFailed": "셰이더 가져오기 실패",
|
||||
"deleteShader": "셰이더 삭제",
|
||||
"deleteShaderConfirm": "\"${name}\"을(를) 삭제하시겠습니까?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "기기에 연결",
|
||||
"hostRemoteSession": "원격 세션 호스트",
|
||||
"controlThisDevice": "휴대폰으로 이 기기를 제어하세요",
|
||||
"remoteControl": "원격 제어",
|
||||
"controlDesktop": "데스크톱 기기 제어",
|
||||
"title": "컴패니언 리모컨",
|
||||
"connectedTo": "${name}에 연결됨",
|
||||
"session": {
|
||||
"creatingSession": "원격 세션 생성 중...",
|
||||
"failedToCreate": "원격 세션 생성 실패:",
|
||||
"noSession": "사용 가능한 세션이 없습니다",
|
||||
"scanQrCode": "QR 코드 스캔",
|
||||
"orEnterManually": "또는 수동으로 입력",
|
||||
"startingServer": "원격 서버 시작 중...",
|
||||
"failedToCreate": "원격 서버를 시작하지 못했습니다:",
|
||||
"hostAddress": "호스트 주소",
|
||||
"sessionId": "세션 ID",
|
||||
"pin": "PIN",
|
||||
"connected": "연결됨",
|
||||
"waitingForConnection": "연결 대기 중...",
|
||||
"serverRunning": "원격 서버 활성",
|
||||
"serverStopped": "원격 서버 중지됨",
|
||||
"serverRunningDescription": "네트워크의 모바일 기기가 이 앱을 검색하고 연결할 수 있습니다",
|
||||
"serverStoppedDescription": "모바일 기기의 연결을 허용하려면 서버를 시작하세요",
|
||||
"usePhoneToControl": "모바일 기기로 이 앱을 제어하세요",
|
||||
"copiedToClipboard": "${label}이(가) 클립보드에 복사되었습니다",
|
||||
"copyToClipboard": "클립보드에 복사",
|
||||
"newSession": "새 세션",
|
||||
"startServer": "서버 시작",
|
||||
"stopServer": "서버 중지",
|
||||
"minimize": "최소화"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "스캔",
|
||||
"manual": "수동",
|
||||
"pairWithDesktop": "데스크톱과 페어링",
|
||||
"enterSessionDetails": "데스크톱 기기에 표시된 세션 정보를 입력하세요",
|
||||
"discoveryDescription": "같은 Plex 계정으로 Plezy를 실행 중인 네트워크의 기기가 자동으로 표시됩니다",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "8자리 세션 ID 입력",
|
||||
"pinHint": "6자리 PIN 입력",
|
||||
"connecting": "연결 중...",
|
||||
"tips": "팁",
|
||||
"tipDesktop": "데스크톱에서 Plezy를 열고 설정 또는 메뉴에서 Companion Remote를 활성화하세요",
|
||||
"tipScan": "스캔 탭을 사용하여 데스크톱의 QR 코드를 스캔하면 빠르게 페어링할 수 있습니다",
|
||||
"tipWifi": "두 기기가 동일한 WiFi 네트워크에 연결되어 있는지 확인하세요",
|
||||
"cameraPermissionRequired": "QR 코드를 스캔하려면 카메라 권한이 필요합니다.\n기기 설정에서 카메라 접근을 허용해 주세요.",
|
||||
"cameraError": "카메라를 시작할 수 없습니다: ${error}",
|
||||
"scanInstruction": "데스크톱에 표시된 QR 코드에 카메라를 향하세요",
|
||||
"invalidQrCode": "유효하지 않은 QR 코드 형식",
|
||||
"searchingForDevices": "기기 검색 중...",
|
||||
"noDevicesFound": "네트워크에서 기기를 찾을 수 없습니다",
|
||||
"noDevicesHint": "데스크톱에서 Plezy가 열려 있고 두 기기가 같은 WiFi 네트워크에 있는지 확인하세요",
|
||||
"availableDevices": "사용 가능한 기기",
|
||||
"manualConnection": "수동 연결",
|
||||
"cryptoInitFailed": "보안 연결을 초기화할 수 없습니다. Plex 계정에 로그인되어 있는지 확인하세요.",
|
||||
"validationHostRequired": "호스트 주소를 입력하세요",
|
||||
"validationHostFormat": "IP:포트 형식이어야 합니다 (예: 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "세션 ID를 입력하세요",
|
||||
"validationSessionIdLength": "세션 ID는 8자리여야 합니다",
|
||||
"validationPinRequired": "PIN을 입력하세요",
|
||||
"validationPinLength": "PIN은 6자리여야 합니다",
|
||||
"connectionTimedOut": "연결 시간이 초과되었습니다. 세션 ID와 PIN을 확인하세요.",
|
||||
"sessionNotFound": "세션을 찾을 수 없습니다. 자격 증명을 확인하세요.",
|
||||
"validationHostFormat": "형식은 IP:포트여야 합니다 (예: 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "연결 시간이 초과되었습니다. 두 기기가 같은 네트워크에 있는지 확인하세요.",
|
||||
"sessionNotFound": "기기를 찾을 수 없습니다. 호스트에서 Plezy가 실행 중인지 확인하세요.",
|
||||
"authFailed": "인증에 실패했습니다. 두 기기가 같은 Plex 계정을 사용하는지 확인하세요.",
|
||||
"failedToConnect": "연결 실패: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "재연결 중...",
|
||||
"attemptOf": "${current}/5 시도 중",
|
||||
"retryNow": "지금 재시도",
|
||||
"connectionError": "연결 오류",
|
||||
"notConnected": "연결되지 않음",
|
||||
"tabRemote": "리모컨",
|
||||
"tabPlay": "재생",
|
||||
"tabMore": "더 보기",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "외부 플레이어 사용",
|
||||
"useExternalPlayerDescription": "내장 플레이어 대신 외부 앱에서 동영상 열기",
|
||||
"selectPlayer": "플레이어 선택",
|
||||
"customPlayers": "사용자 정의 플레이어",
|
||||
"systemDefault": "시스템 기본값",
|
||||
"addCustomPlayer": "사용자 정의 플레이어 추가",
|
||||
"playerName": "플레이어 이름",
|
||||
"playerCommand": "명령어",
|
||||
"playerPackage": "패키지 이름",
|
||||
"playerUrlScheme": "URL 스킴",
|
||||
"customPlayer": "사용자 정의 플레이어",
|
||||
"off": "꺼짐",
|
||||
"launchFailed": "외부 플레이어를 열 수 없습니다",
|
||||
"appNotInstalled": "${name}이(가) 설치되어 있지 않습니다",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "줄거리",
|
||||
"poster": "포스터",
|
||||
"background": "배경",
|
||||
"logo": "로고",
|
||||
"squareArt": "정사각형 아트",
|
||||
"selectPoster": "포스터 선택",
|
||||
"selectBackground": "배경 선택",
|
||||
"selectLogo": "로고 선택",
|
||||
"selectSquareArt": "정사각형 아트 선택",
|
||||
"fromUrl": "URL에서",
|
||||
"uploadFile": "파일 업로드",
|
||||
"enterImageUrl": "이미지 URL 입력",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "자막 자동 선택 모드",
|
||||
"manuallySelected": "수동 선택",
|
||||
"shownWithForeignAudio": "외국어 오디오 시 표시",
|
||||
"alwaysEnabled": "항상 활성화"
|
||||
"alwaysEnabled": "항상 활성화",
|
||||
"tags": "태그",
|
||||
"addTag": "태그 추가",
|
||||
"genre": "장르",
|
||||
"director": "감독",
|
||||
"writer": "작가",
|
||||
"producer": "프로듀서",
|
||||
"country": "국가",
|
||||
"collection": "컬렉션",
|
||||
"label": "라벨",
|
||||
"style": "스타일",
|
||||
"mood": "분위기"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "서버 작업",
|
||||
"failedToLoad": "작업을 불러올 수 없습니다",
|
||||
"noTasks": "실행 중인 작업 없음"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "연결됨",
|
||||
"connectedAs": "@${username}(으)로 연결됨",
|
||||
"disconnectConfirm": "Trakt 계정 연결을 끊으시겠습니까?",
|
||||
"disconnectConfirmBody": "Plezy가 Trakt로 재생 이벤트를 보내지 않습니다. 언제든지 다시 연결할 수 있습니다.",
|
||||
"scrobble": "실시간 스크로블",
|
||||
"scrobbleDescription": "재생 중 재생, 일시정지, 정지 이벤트를 Trakt로 전송합니다.",
|
||||
"watchedSync": "시청 상태 동기화",
|
||||
"watchedSyncDescription": "Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다.",
|
||||
"deviceCodeTitle": "Trakt에서 Plezy 활성화",
|
||||
"deviceCodeBody": "${url}을(를) 방문하여 이 코드를 입력하세요:",
|
||||
"openTraktActivate": "활성화하려면 Trakt 열기",
|
||||
"waitingForAuthorization": "인증 대기 중…",
|
||||
"codeCopied": "코드가 복사됨",
|
||||
"connectFailed": "Trakt에 연결할 수 없습니다. 다시 시도하세요."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,978 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Plezy"
|
||||
},
|
||||
"auth": {
|
||||
"signInWithPlex": "Logg inn med Plex",
|
||||
"showQRCode": "Vis QR-kode",
|
||||
"authenticate": "Autentiser",
|
||||
"authenticationTimeout": "Autentiseringen ble tidsavbrutt. Prøv igjen.",
|
||||
"scanQRToSignIn": "Skann denne QR-koden for å logge inn",
|
||||
"waitingForAuth": "Venter på autentisering...\nFullfør innloggingen i nettleseren din.",
|
||||
"useBrowser": "Bruk nettleser"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Avbryt",
|
||||
"save": "Lagre",
|
||||
"close": "Lukk",
|
||||
"clear": "Tøm",
|
||||
"reset": "Tilbakestill",
|
||||
"later": "Senere",
|
||||
"submit": "Send inn",
|
||||
"confirm": "Bekreft",
|
||||
"retry": "Prøv igjen",
|
||||
"logout": "Logg ut",
|
||||
"unknown": "Ukjent",
|
||||
"refresh": "Oppdater",
|
||||
"yes": "Ja",
|
||||
"no": "Nei",
|
||||
"delete": "Slett",
|
||||
"shuffle": "Tilfeldig",
|
||||
"addTo": "Legg til i...",
|
||||
"createNew": "Opprett ny",
|
||||
"connect": "Koble til",
|
||||
"disconnect": "Koble fra",
|
||||
"play": "Spill av",
|
||||
"pause": "Pause",
|
||||
"resume": "Gjenoppta",
|
||||
"error": "Feil",
|
||||
"search": "Søk",
|
||||
"home": "Hjem",
|
||||
"back": "Tilbake",
|
||||
"settings": "Innstillinger",
|
||||
"mute": "Demp",
|
||||
"ok": "OK",
|
||||
"reconnect": "Koble til på nytt",
|
||||
"exitConfirmTitle": "Avslutte appen?",
|
||||
"exitConfirmMessage": "Er du sikker på at du vil avslutte?",
|
||||
"dontAskAgain": "Ikke spør igjen",
|
||||
"exit": "Avslutt",
|
||||
"viewAll": "Vis alle",
|
||||
"checkingNetwork": "Sjekker nettverk...",
|
||||
"refreshingServers": "Oppdaterer servere...",
|
||||
"loadingServers": "Laster servere...",
|
||||
"connectingToServers": "Kobler til servere...",
|
||||
"startingOfflineMode": "Starter frakoblet modus...",
|
||||
"loading": "Laster..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Lisenser",
|
||||
"switchProfile": "Bytt profil",
|
||||
"subtitleStyling": "Undertekststil",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logger"
|
||||
},
|
||||
"update": {
|
||||
"available": "Oppdatering tilgjengelig",
|
||||
"versionAvailable": "Versjon ${version} er tilgjengelig",
|
||||
"currentVersion": "Gjeldende: ${version}",
|
||||
"skipVersion": "Hopp over denne versjonen",
|
||||
"viewRelease": "Vis utgivelse",
|
||||
"latestVersion": "Du har den nyeste versjonen",
|
||||
"checkFailed": "Kunne ikke se etter oppdateringer"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Innstillinger",
|
||||
"supportDeveloper": "Støtt Plezy",
|
||||
"supportDeveloperDescription": "Doner via Liberapay for å finansiere utviklingen",
|
||||
"language": "Språk",
|
||||
"theme": "Tema",
|
||||
"appearance": "Utseende",
|
||||
"videoPlayback": "Videoavspilling",
|
||||
"videoPlaybackDescription": "Konfigurer avspillingsatferd",
|
||||
"advanced": "Avansert",
|
||||
"episodePosterMode": "Episodeplakatstil",
|
||||
"seriesPoster": "Serieplakat",
|
||||
"seasonPoster": "Sesongplakat",
|
||||
"episodeThumbnail": "Miniatyrbilde",
|
||||
"showHeroSectionDescription": "Vis fremhevet innholdskarusell på hjemmeskjermen",
|
||||
"secondsLabel": "Sekunder",
|
||||
"minutesLabel": "Minutter",
|
||||
"secondsShort": "s",
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Angi varighet (${min}-${max})",
|
||||
"systemTheme": "System",
|
||||
"lightTheme": "Lyst",
|
||||
"darkTheme": "Mørkt",
|
||||
"oledTheme": "OLED",
|
||||
"libraryDensity": "Bibliotekets tetthet",
|
||||
"compact": "Kompakt",
|
||||
"comfortable": "Komfortabel",
|
||||
"viewMode": "Visningsmodus",
|
||||
"gridView": "Rutenett",
|
||||
"listView": "Liste",
|
||||
"showHeroSection": "Vis fremhevet seksjon",
|
||||
"useGlobalHubs": "Bruk Plex Home-layout",
|
||||
"useGlobalHubsDescription": "Vis hjemmeside-huber som den offisielle Plex-klienten. Når av, vises per-bibliotek-anbefalinger i stedet.",
|
||||
"showServerNameOnHubs": "Vis servernavn på huber",
|
||||
"showServerNameOnHubsDescription": "Vis alltid servernavnet i hubtitler. Når av, vises kun for dupliserte hubnavn.",
|
||||
"alwaysKeepSidebarOpen": "Hold sidefeltet alltid åpent",
|
||||
"alwaysKeepSidebarOpenDescription": "Sidefeltet forblir utvidet og innholdsområdet tilpasser seg",
|
||||
"showUnwatchedCount": "Vis antall usette",
|
||||
"showUnwatchedCountDescription": "Vis antall usette episoder på serier og sesonger",
|
||||
"hideSpoilers": "Skjul spoilere for usette episoder",
|
||||
"hideSpoilersDescription": "Slør miniatyrbilder og skjul beskrivelser for episoder du ikke har sett ennå",
|
||||
"playerBackend": "Spillermotor",
|
||||
"exoPlayer": "ExoPlayer (Anbefalt)",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Maskinvaredekoding",
|
||||
"hardwareDecodingDescription": "Bruk maskinvareakselerasjon når tilgjengelig",
|
||||
"bufferSize": "Bufferstørrelse",
|
||||
"bufferSizeMB": "${size}MB",
|
||||
"bufferSizeAuto": "Auto (Anbefalt)",
|
||||
"bufferSizeWarning": "Enheten din har ${heap}MB minne. En ${size}MB buffer kan forårsake avspillingsproblemer.",
|
||||
"subtitleStyling": "Undertekststil",
|
||||
"subtitleStylingDescription": "Tilpass utseendet på undertekster",
|
||||
"smallSkipDuration": "Kort hoppvarighet",
|
||||
"largeSkipDuration": "Lang hoppvarighet",
|
||||
"rewindOnResume": "Spol tilbake ved gjenopptakelse",
|
||||
"secondsUnit": "${seconds} sekunder",
|
||||
"defaultSleepTimer": "Standard søvntimer",
|
||||
"minutesUnit": "${minutes} minutter",
|
||||
"rememberTrackSelections": "Husk sporvalg per serie/film",
|
||||
"rememberTrackSelectionsDescription": "Lagre automatisk lyd- og undertekstspråkpreferanser når du bytter spor under avspilling",
|
||||
"clickVideoTogglesPlayback": "Klikk på video for å veksle avspilling",
|
||||
"clickVideoTogglesPlaybackDescription": "Hvis aktivert, vil klikk på videospilleren spille av/pause videoen. Ellers vil klikk vise/skjule avspillingskontrollene.",
|
||||
"videoPlayerControls": "Videospillerkontroller",
|
||||
"keyboardShortcuts": "Tastatursnarveier",
|
||||
"keyboardShortcutsDescription": "Tilpass tastatursnarveier",
|
||||
"videoPlayerNavigation": "Videospillernavigering",
|
||||
"videoPlayerNavigationDescription": "Bruk piltaster for å navigere videospillerkontroller",
|
||||
"watchTogetherRelay": "Se Sammen-relay",
|
||||
"watchTogetherRelayDescription": "Angi en egendefinert relay-server for Se Sammen. Alle deltakere må bruke den samme serveren.",
|
||||
"watchTogetherRelayHint": "https://min-relay.eksempel.no",
|
||||
"crashReporting": "Krasjrapportering",
|
||||
"crashReportingDescription": "Send krasjrapporter for å hjelpe med å forbedre appen",
|
||||
"debugLogging": "Feilsøkingslogging",
|
||||
"debugLoggingDescription": "Aktiver detaljert logging for feilsøking",
|
||||
"viewLogs": "Vis logger",
|
||||
"viewLogsDescription": "Vis applikasjonslogger",
|
||||
"clearCache": "Tøm hurtigbuffer",
|
||||
"clearCacheDescription": "Dette vil tømme alle hurtigbufrede bilder og data. Appen kan bruke lengre tid på å laste innhold etter tømming.",
|
||||
"clearCacheSuccess": "Hurtigbuffer tømt",
|
||||
"resetSettings": "Tilbakestill innstillinger",
|
||||
"resetSettingsDescription": "Dette vil tilbakestille alle innstillinger til standardverdier. Denne handlingen kan ikke angres.",
|
||||
"resetSettingsSuccess": "Innstillinger tilbakestilt",
|
||||
"shortcutsReset": "Snarveier tilbakestilt til standard",
|
||||
"about": "Om",
|
||||
"aboutDescription": "Appinformasjon og lisenser",
|
||||
"updates": "Oppdateringer",
|
||||
"updateAvailable": "Oppdatering tilgjengelig",
|
||||
"checkForUpdates": "Se etter oppdateringer",
|
||||
"validationErrorEnterNumber": "Vennligst skriv inn et gyldig tall",
|
||||
"validationErrorDuration": "Varigheten må være mellom ${min} og ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Snarvei allerede tilordnet til ${action}",
|
||||
"shortcutUpdated": "Snarvei oppdatert for ${action}",
|
||||
"autoSkip": "Automatisk hopp",
|
||||
"autoSkipIntro": "Hopp over intro automatisk",
|
||||
"autoSkipIntroDescription": "Hopp automatisk over intromarkører etter noen sekunder",
|
||||
"autoSkipCredits": "Hopp over rulletekst automatisk",
|
||||
"autoSkipCreditsDescription": "Hopp automatisk over rulletekst og spill neste episode",
|
||||
"autoSkipDelay": "Forsinkelse for automatisk hopp",
|
||||
"autoSkipDelayDescription": "Vent ${seconds} sekunder før automatisk hopping",
|
||||
"introPattern": "Intromarkørmønster",
|
||||
"introPatternDescription": "Regulært uttrykk for å gjenkjenne intromarkører i kapitteltitler",
|
||||
"creditsPattern": "Rulletekstmarkørmønster",
|
||||
"creditsPatternDescription": "Regulært uttrykk for å gjenkjenne rulletekstmarkører i kapitteltitler",
|
||||
"invalidRegex": "Ugyldig regulært uttrykk",
|
||||
"downloads": "Nedlastinger",
|
||||
"downloadLocationDescription": "Velg hvor nedlastet innhold skal lagres",
|
||||
"downloadLocationDefault": "Standard (App-lagring)",
|
||||
"downloadLocationCustom": "Egendefinert plassering",
|
||||
"selectFolder": "Velg mappe",
|
||||
"resetToDefault": "Tilbakestill til standard",
|
||||
"currentPath": "Gjeldende: ${path}",
|
||||
"downloadLocationChanged": "Nedlastingsplassering endret",
|
||||
"downloadLocationReset": "Nedlastingsplassering tilbakestilt til standard",
|
||||
"downloadLocationInvalid": "Valgt mappe er ikke skrivbar",
|
||||
"downloadLocationSelectError": "Kunne ikke velge mappe",
|
||||
"downloadOnWifiOnly": "Last ned kun på WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Forhindre nedlastinger på mobildata",
|
||||
"autoRemoveWatchedDownloads": "Fjern sette nedlastinger automatisk",
|
||||
"autoRemoveWatchedDownloadsDescription": "Slett nedlastede episoder og filmer automatisk når de er markert som sett",
|
||||
"cellularDownloadBlocked": "Nedlastinger er deaktivert på mobildata. Koble til WiFi eller endre innstillingen.",
|
||||
"maxVolume": "Maks volum",
|
||||
"maxVolumeDescription": "Tillat volumforsterkning over 100% for stille media",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Vis hva du ser på Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Synkroniser visningshistorikk med Trakt",
|
||||
"companionRemoteServer": "Companion Remote-server",
|
||||
"companionRemoteServerDescription": "Tillat mobilenheter på nettverket ditt å styre denne appen",
|
||||
"autoPip": "Auto bilde-i-bilde",
|
||||
"autoPipDescription": "Gå automatisk til bilde-i-bilde når du forlater appen under avspilling",
|
||||
"matchContentFrameRate": "Tilpass innholdets bildefrekvens",
|
||||
"matchContentFrameRateDescription": "Juster skjermens oppdateringsfrekvens for å matche videoinnhold, reduserer hakking og sparer batteri",
|
||||
"matchRefreshRate": "Tilpass oppdateringsfrekvens",
|
||||
"matchRefreshRateDescription": "Bytt skjermens oppdateringsfrekvens for å matche videoinnhold i fullskjerm",
|
||||
"matchDynamicRange": "Tilpass dynamisk område",
|
||||
"matchDynamicRangeDescription": "Aktiver HDR automatisk for HDR-innhold og gå tilbake til SDR når spilleren lukkes",
|
||||
"displaySwitchDelay": "Forsinkelse ved skjermbytte",
|
||||
"tunneledPlayback": "Tunnelert avspilling",
|
||||
"tunneledPlaybackDescription": "Bruk maskinvareakselerert videotunnelering. Deaktiver hvis du ser svart skjerm med lyd på HDR-innhold",
|
||||
"requireProfileSelectionOnOpen": "Spør om profil ved appåpning",
|
||||
"requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åpnes",
|
||||
"confirmExitOnBack": "Bekreft før avslutning",
|
||||
"confirmExitOnBackDescription": "Vis en bekreftelsesdialog når du trykker tilbake for å avslutte appen",
|
||||
"forceTvMode": "Tving TV-modus",
|
||||
"forceTvModeDescription": "Bruk TV-oppsettet uavhengig av automatisk gjenkjenning. Nyttig på Android TV-enheter som ikke rapporterer leanback-funksjonen. Starter appen på nytt ved endring.",
|
||||
"autoHidePerformanceOverlay": "Skjul ytelsesoverlegg automatisk",
|
||||
"autoHidePerformanceOverlayDescription": "Fade ytelsesoverlegget med avspillingskontrollene",
|
||||
"showNavBarLabels": "Vis navigasjonsfeltlabeler",
|
||||
"showNavBarLabelsDescription": "Vis tekstlabeler under navigasjonsfeltikoner",
|
||||
"liveTvDefaultFavorites": "Standard til favorittkanaler",
|
||||
"liveTvDefaultFavoritesDescription": "Vis kun favorittkanaler når du åpner Live TV",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Søk i filmer, serier, musikk...",
|
||||
"tryDifferentTerm": "Prøv et annet søkeord",
|
||||
"searchYourMedia": "Søk i mediene dine",
|
||||
"enterTitleActorOrKeyword": "Skriv inn tittel, skuespiller eller nøkkelord"
|
||||
},
|
||||
"hotkeys": {
|
||||
"setShortcutFor": "Angi snarvei for ${actionName}",
|
||||
"clearShortcut": "Fjern snarvei",
|
||||
"actions": {
|
||||
"playPause": "Spill av/Pause",
|
||||
"volumeUp": "Volum opp",
|
||||
"volumeDown": "Volum ned",
|
||||
"seekForward": "Spol fremover (${seconds}s)",
|
||||
"seekBackward": "Spol bakover (${seconds}s)",
|
||||
"fullscreenToggle": "Veksle fullskjerm",
|
||||
"muteToggle": "Veksle demping",
|
||||
"subtitleToggle": "Veksle undertekster",
|
||||
"audioTrackNext": "Neste lydspor",
|
||||
"subtitleTrackNext": "Neste undertekstspor",
|
||||
"chapterNext": "Neste kapittel",
|
||||
"chapterPrevious": "Forrige kapittel",
|
||||
"speedIncrease": "Øk hastighet",
|
||||
"speedDecrease": "Reduser hastighet",
|
||||
"speedReset": "Tilbakestill hastighet",
|
||||
"subSeekNext": "Spol til neste undertekst",
|
||||
"subSeekPrev": "Spol til forrige undertekst",
|
||||
"shaderToggle": "Veksle shadere",
|
||||
"skipMarker": "Hopp over intro/rulletekst"
|
||||
}
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Filinformasjon",
|
||||
"video": "Video",
|
||||
"audio": "Lyd",
|
||||
"file": "Fil",
|
||||
"advanced": "Avansert",
|
||||
"codec": "Kodek",
|
||||
"resolution": "Oppløsning",
|
||||
"bitrate": "Bitrate",
|
||||
"frameRate": "Bildefrekvens",
|
||||
"aspectRatio": "Sideforhold",
|
||||
"profile": "Profil",
|
||||
"bitDepth": "Bitdybde",
|
||||
"colorSpace": "Fargerom",
|
||||
"colorRange": "Fargeområde",
|
||||
"colorPrimaries": "Fargeprimærer",
|
||||
"chromaSubsampling": "Krominansnedsampling",
|
||||
"channels": "Kanaler",
|
||||
"subtitles": "Undertekster",
|
||||
"overallBitrate": "Total bitrate",
|
||||
"path": "Sti",
|
||||
"size": "Størrelse",
|
||||
"container": "Beholder",
|
||||
"duration": "Varighet",
|
||||
"optimizedForStreaming": "Optimalisert for strømming",
|
||||
"has64bitOffsets": "64-biters forskyvninger"
|
||||
},
|
||||
"mediaMenu": {
|
||||
"markAsWatched": "Merk som sett",
|
||||
"markAsUnwatched": "Merk som usett",
|
||||
"removeFromContinueWatching": "Fjern fra Fortsett å se",
|
||||
"goToSeries": "Gå til serie",
|
||||
"goToSeason": "Gå til sesong",
|
||||
"shufflePlay": "Tilfeldig avspilling",
|
||||
"fileInfo": "Filinformasjon",
|
||||
"deleteFromServer": "Slett fra server",
|
||||
"confirmDelete": "Dette vil permanent slette dette mediet og filene fra serveren din. Dette kan ikke angres.",
|
||||
"deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.",
|
||||
"mediaDeletedSuccessfully": "Medieelement slettet",
|
||||
"mediaFailedToDelete": "Kunne ikke slette medieelement",
|
||||
"rate": "Vurder",
|
||||
"playFromBeginning": "Spill fra begynnelsen",
|
||||
"playVersion": "Spill av versjon..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, film",
|
||||
"mediaCardShow": "${title}, TV-serie",
|
||||
"mediaCardEpisode": "${title}, ${episodeInfo}",
|
||||
"mediaCardSeason": "${title}, ${seasonInfo}",
|
||||
"mediaCardWatched": "sett",
|
||||
"mediaCardPartiallyWatched": "${percent} prosent sett",
|
||||
"mediaCardUnwatched": "usett",
|
||||
"tapToPlay": "Trykk for å spille"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Tilfeldig avspilling",
|
||||
"playTrailer": "Spill trailer",
|
||||
"markAsWatched": "Merk som sett",
|
||||
"markAsUnwatched": "Merk som usett"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Lyd",
|
||||
"subtitlesLabel": "Undertekster",
|
||||
"resetToZero": "Tilbakestill til 0ms",
|
||||
"addTime": "+${amount}${unit}",
|
||||
"minusTime": "-${amount}${unit}",
|
||||
"playsLater": "${label} spilles senere",
|
||||
"playsEarlier": "${label} spilles tidligere",
|
||||
"noOffset": "Ingen forskyvning",
|
||||
"letterbox": "Letterbox",
|
||||
"fillScreen": "Fyll skjerm",
|
||||
"stretch": "Strekk",
|
||||
"lockRotation": "Lås rotasjon",
|
||||
"unlockRotation": "Lås opp rotasjon",
|
||||
"timerActive": "Timer aktiv",
|
||||
"playbackWillPauseIn": "Avspilling vil pause om ${duration}",
|
||||
"stillWatching": "Ser du fortsatt?",
|
||||
"pausingIn": "Pauser om ${seconds}s",
|
||||
"continueWatching": "Fortsett",
|
||||
"autoPlayNext": "Spill neste automatisk",
|
||||
"playNext": "Spill neste",
|
||||
"playButton": "Spill av",
|
||||
"pauseButton": "Pause",
|
||||
"seekBackwardButton": "Spol tilbake ${seconds} sekunder",
|
||||
"seekForwardButton": "Spol fremover ${seconds} sekunder",
|
||||
"previousButton": "Forrige episode",
|
||||
"nextButton": "Neste episode",
|
||||
"previousChapterButton": "Forrige kapittel",
|
||||
"nextChapterButton": "Neste kapittel",
|
||||
"muteButton": "Demp",
|
||||
"unmuteButton": "Opphev demping",
|
||||
"settingsButton": "Videoinnstillinger",
|
||||
"tracksButton": "Lyd og undertekster",
|
||||
"chaptersButton": "Kapitler",
|
||||
"versionsButton": "Videoversjoner",
|
||||
"pipButton": "Bilde-i-bilde-modus",
|
||||
"aspectRatioButton": "Sideforhold",
|
||||
"ambientLighting": "Omgivelseslys",
|
||||
"fullscreenButton": "Gå til fullskjerm",
|
||||
"exitFullscreenButton": "Avslutt fullskjerm",
|
||||
"alwaysOnTopButton": "Alltid øverst",
|
||||
"rotationLockButton": "Rotasjonslås",
|
||||
"lockScreen": "Lås skjerm",
|
||||
"screenLockButton": "Skjermlås",
|
||||
"longPressToUnlock": "Langt trykk for å låse opp",
|
||||
"timelineSlider": "Videotidslinje",
|
||||
"volumeSlider": "Volumnivå",
|
||||
"endsAt": "Slutter kl. ${time}",
|
||||
"pipActive": "Spiller i bilde-i-bilde",
|
||||
"pipFailed": "Bilde-i-bilde kunne ikke starte",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Krever Android 8.0 eller nyere",
|
||||
"iosVersion": "Krever iOS 15.0 eller nyere",
|
||||
"permissionDisabled": "Bilde-i-bilde-tillatelse er deaktivert. Aktiver den i Innstillinger > Apper > Plezy > Bilde-i-bilde",
|
||||
"notSupported": "Enheten støtter ikke bilde-i-bilde-modus",
|
||||
"voSwitchFailed": "Kunne ikke bytte videoutgang for bilde-i-bilde",
|
||||
"failed": "Bilde-i-bilde kunne ikke starte",
|
||||
"unknown": "En feil oppstod: ${error}"
|
||||
},
|
||||
"chapters": "Kapitler",
|
||||
"noChaptersAvailable": "Ingen kapitler tilgjengelig",
|
||||
"queue": "Kø",
|
||||
"noQueueItems": "Ingen elementer i kø",
|
||||
"searchSubtitles": "Søk etter undertekster",
|
||||
"language": "Språk",
|
||||
"noSubtitlesFound": "Ingen undertekster funnet",
|
||||
"subtitleDownloaded": "Undertekst lastet ned",
|
||||
"subtitleDownloadFailed": "Kunne ikke laste ned undertekst",
|
||||
"searchLanguages": "Søk etter språk..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrator",
|
||||
"restricted": "Begrenset",
|
||||
"protected": "Beskyttet",
|
||||
"current": "GJELDENDE"
|
||||
},
|
||||
"messages": {
|
||||
"markedAsWatched": "Merket som sett",
|
||||
"markedAsUnwatched": "Merket som usett",
|
||||
"markedAsWatchedOffline": "Merket som sett (synkroniseres når tilkoblet)",
|
||||
"markedAsUnwatchedOffline": "Merket som usett (synkroniseres når tilkoblet)",
|
||||
"autoRemovedWatchedDownload": "Automatisk fjernet: ${title}",
|
||||
"removedFromContinueWatching": "Fjernet fra Fortsett å se",
|
||||
"errorLoading": "Feil: ${error}",
|
||||
"fileInfoNotAvailable": "Filinformasjon ikke tilgjengelig",
|
||||
"errorLoadingFileInfo": "Feil ved lasting av filinformasjon: ${error}",
|
||||
"errorLoadingSeries": "Feil ved lasting av serie",
|
||||
"errorLoadingSeason": "Feil ved lasting av sesong",
|
||||
"musicNotSupported": "Musikkavspilling støttes ikke ennå",
|
||||
"logsCleared": "Logger tømt",
|
||||
"logsCopied": "Logger kopiert til utklippstavle",
|
||||
"noLogsAvailable": "Ingen logger tilgjengelig",
|
||||
"libraryScanning": "Skanner \"${title}\"...",
|
||||
"libraryScanStarted": "Bibliotekkanning startet for \"${title}\"",
|
||||
"libraryScanFailed": "Kunne ikke skanne bibliotek: ${error}",
|
||||
"metadataRefreshing": "Oppdaterer metadata for \"${title}\"...",
|
||||
"metadataRefreshStarted": "Metadataoppdatering startet for \"${title}\"",
|
||||
"metadataRefreshFailed": "Kunne ikke oppdatere metadata: ${error}",
|
||||
"logoutConfirm": "Er du sikker på at du vil logge ut?",
|
||||
"noSeasonsFound": "Ingen sesonger funnet",
|
||||
"noEpisodesFound": "Ingen episoder funnet i første sesong",
|
||||
"noEpisodesFoundGeneral": "Ingen episoder funnet",
|
||||
"noResultsFound": "Ingen resultater funnet",
|
||||
"sleepTimerSet": "Søvntimer satt til ${label}",
|
||||
"noItemsAvailable": "Ingen elementer tilgjengelig",
|
||||
"failedToCreatePlayQueueNoItems": "Kunne ikke opprette avspillingskø – ingen elementer",
|
||||
"failedPlayback": "Kunne ikke ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Bytter til kompatibel spiller...",
|
||||
"serverLimitTitle": "Avspilling mislyktes",
|
||||
"serverLimitBody": "Serveren returnerte en feil (HTTP 500). Det betyr vanligvis at servereieren har satt en båndbredde- eller transkodingsgrense som avviser økten din. Det er ingenting du kan gjøre fra klienten — servereieren må justere innstillingene sine.",
|
||||
"logsUploaded": "Logger lastet opp",
|
||||
"logsUploadFailed": "Kunne ikke laste opp logger",
|
||||
"logId": "Logg-ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"text": "Tekst",
|
||||
"border": "Kantlinje",
|
||||
"background": "Bakgrunn",
|
||||
"fontSize": "Skriftstørrelse",
|
||||
"textColor": "Tekstfarge",
|
||||
"borderSize": "Kantstørrelse",
|
||||
"borderColor": "Kantfarge",
|
||||
"backgroundOpacity": "Bakgrunnsopasitet",
|
||||
"backgroundColor": "Bakgrunnsfarge",
|
||||
"position": "Posisjon",
|
||||
"assOverride": "ASS-overstyring",
|
||||
"bold": "Fet",
|
||||
"italic": "Kursiv"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "mpv.conf",
|
||||
"description": "Avanserte videospillerinnstillinger",
|
||||
"presets": "Forhåndsinnstillinger",
|
||||
"noPresets": "Ingen lagrede forhåndsinnstillinger",
|
||||
"saveAsPreset": "Lagre som forhåndsinnstilling...",
|
||||
"presetName": "Forhåndsinnstillingsnavn",
|
||||
"presetNameHint": "Skriv inn et navn for denne forhåndsinnstillingen",
|
||||
"loadPreset": "Last inn",
|
||||
"deletePreset": "Slett",
|
||||
"presetSaved": "Forhåndsinnstilling lagret",
|
||||
"presetLoaded": "Forhåndsinnstilling lastet inn",
|
||||
"presetDeleted": "Forhåndsinnstilling slettet",
|
||||
"confirmDeletePreset": "Er du sikker på at du vil slette denne forhåndsinnstillingen?",
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# kommentar"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Bekreft handling"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Oppdag",
|
||||
"switchProfile": "Bytt profil",
|
||||
"noContentAvailable": "Ingen innhold tilgjengelig",
|
||||
"addMediaToLibraries": "Legg til medier i bibliotekene dine",
|
||||
"continueWatching": "Fortsett å se",
|
||||
"playEpisode": "S${season}E${episode}",
|
||||
"overview": "Oversikt",
|
||||
"cast": "Skuespillere",
|
||||
"extras": "Trailere og ekstra",
|
||||
"studio": "Studio",
|
||||
"rating": "Vurdering",
|
||||
"movie": "Film",
|
||||
"tvShow": "TV-serie",
|
||||
"minutesLeft": "${minutes} min igjen"
|
||||
},
|
||||
"errors": {
|
||||
"searchFailed": "Søk mislyktes: ${error}",
|
||||
"connectionTimeout": "Tidsavbrudd ved lasting av ${context}",
|
||||
"connectionFailed": "Kunne ikke koble til Plex-server",
|
||||
"failedToLoad": "Kunne ikke laste ${context}: ${error}",
|
||||
"noClientAvailable": "Ingen klient tilgjengelig",
|
||||
"authenticationFailed": "Autentisering mislyktes: ${error}",
|
||||
"couldNotLaunchUrl": "Kunne ikke åpne autentiserings-URL",
|
||||
"pleaseEnterToken": "Vennligst skriv inn et token",
|
||||
"invalidToken": "Ugyldig token",
|
||||
"failedToVerifyToken": "Kunne ikke verifisere token: ${error}",
|
||||
"failedToSwitchProfile": "Kunne ikke bytte til ${displayName}"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Biblioteker",
|
||||
"scanLibraryFiles": "Skann bibliotekfiler",
|
||||
"scanLibrary": "Skann bibliotek",
|
||||
"analyze": "Analyser",
|
||||
"analyzeLibrary": "Analyser bibliotek",
|
||||
"refreshMetadata": "Oppdater metadata",
|
||||
"emptyTrash": "Tøm papirkurv",
|
||||
"emptyingTrash": "Tømmer papirkurv for \"${title}\"...",
|
||||
"trashEmptied": "Papirkurv tømt for \"${title}\"",
|
||||
"failedToEmptyTrash": "Kunne ikke tømme papirkurv: ${error}",
|
||||
"analyzing": "Analyserer \"${title}\"...",
|
||||
"analysisStarted": "Analyse startet for \"${title}\"",
|
||||
"failedToAnalyze": "Kunne ikke analysere bibliotek: ${error}",
|
||||
"noLibrariesFound": "Ingen biblioteker funnet",
|
||||
"thisLibraryIsEmpty": "Dette biblioteket er tomt",
|
||||
"all": "Alle",
|
||||
"clearAll": "Tøm alle",
|
||||
"scanLibraryConfirm": "Er du sikker på at du vil skanne \"${title}\"?",
|
||||
"analyzeLibraryConfirm": "Er du sikker på at du vil analysere \"${title}\"?",
|
||||
"refreshMetadataConfirm": "Er du sikker på at du vil oppdatere metadata for \"${title}\"?",
|
||||
"emptyTrashConfirm": "Er du sikker på at du vil tømme papirkurven for \"${title}\"?",
|
||||
"manageLibraries": "Administrer biblioteker",
|
||||
"sort": "Sorter",
|
||||
"sortBy": "Sorter etter",
|
||||
"filters": "Filtre",
|
||||
"confirmActionMessage": "Er du sikker på at du vil utføre denne handlingen?",
|
||||
"showLibrary": "Vis bibliotek",
|
||||
"hideLibrary": "Skjul bibliotek",
|
||||
"libraryOptions": "Bibliotekalternativer",
|
||||
"content": "bibliotekinnhold",
|
||||
"selectLibrary": "Velg bibliotek",
|
||||
"filtersWithCount": "Filtre (${count})",
|
||||
"noRecommendations": "Ingen anbefalinger tilgjengelig",
|
||||
"noCollections": "Ingen samlinger i dette biblioteket",
|
||||
"noFoldersFound": "Ingen mapper funnet",
|
||||
"folders": "mapper",
|
||||
"tabs": {
|
||||
"recommended": "Anbefalt",
|
||||
"browse": "Bla gjennom",
|
||||
"collections": "Samlinger",
|
||||
"playlists": "Spillelister"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Gruppering",
|
||||
"all": "Alle",
|
||||
"movies": "Filmer",
|
||||
"shows": "TV-serier",
|
||||
"seasons": "Sesonger",
|
||||
"episodes": "Episoder",
|
||||
"folders": "Mapper"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "Om",
|
||||
"openSourceLicenses": "Åpen kildekode-lisenser",
|
||||
"versionLabel": "Versjon ${version}",
|
||||
"appDescription": "En vakker Plex-klient for Flutter",
|
||||
"viewLicensesDescription": "Vis lisenser for tredjepartsbiblioteker"
|
||||
},
|
||||
"serverSelection": {
|
||||
"allServerConnectionsFailed": "Kunne ikke koble til noen servere. Sjekk nettverket ditt og prøv igjen.",
|
||||
"noServersFoundForAccount": "Ingen servere funnet for ${username} (${email})",
|
||||
"failedToLoadServers": "Kunne ikke laste servere: ${error}"
|
||||
},
|
||||
"hubDetail": {
|
||||
"title": "Tittel",
|
||||
"releaseYear": "Utgivelsesår",
|
||||
"dateAdded": "Dato lagt til",
|
||||
"rating": "Vurdering",
|
||||
"noItemsFound": "Ingen elementer funnet"
|
||||
},
|
||||
"logs": {
|
||||
"clearLogs": "Tøm logger",
|
||||
"copyLogs": "Kopier logger",
|
||||
"uploadLogs": "Last opp logger"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Relaterte pakker",
|
||||
"license": "Lisens",
|
||||
"licenseNumber": "Lisens ${number}",
|
||||
"licensesCount": "${count} lisenser"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Biblioteker",
|
||||
"downloads": "Nedlastinger",
|
||||
"liveTv": "Direkte-TV"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "Direkte-TV",
|
||||
"guide": "Programguide",
|
||||
"noChannels": "Ingen kanaler tilgjengelig",
|
||||
"noDvr": "Ingen DVR konfigurert på noen server",
|
||||
"noPrograms": "Ingen programdata tilgjengelig",
|
||||
"live": "DIREKTE",
|
||||
"reloadGuide": "Last inn programguide på nytt",
|
||||
"now": "Nå",
|
||||
"today": "I dag",
|
||||
"midnight": "Midnatt",
|
||||
"overnight": "Natt",
|
||||
"morning": "Morgen",
|
||||
"daytime": "Dagtid",
|
||||
"evening": "Kveld",
|
||||
"lateNight": "Sen kveld",
|
||||
"whatsOn": "Hva går nå",
|
||||
"watchChannel": "Se kanal",
|
||||
"favorites": "Favoritter",
|
||||
"reorderFavorites": "Endre rekkefølge på favoritter",
|
||||
"joinSession": "Bli med i pågående økt",
|
||||
"watchFromStart": "Se fra starten (${minutes} min siden)",
|
||||
"watchLive": "Se direkte",
|
||||
"goToLive": "Gå til direkte"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Samlinger",
|
||||
"collection": "Samling",
|
||||
"empty": "Samlingen er tom",
|
||||
"unknownLibrarySection": "Kan ikke slette: Ukjent bibliotekseksjon",
|
||||
"deleteCollection": "Slett samling",
|
||||
"deleteConfirm": "Er du sikker på at du vil slette \"${title}\"? Denne handlingen kan ikke angres.",
|
||||
"deleted": "Samling slettet",
|
||||
"deleteFailed": "Kunne ikke slette samling",
|
||||
"deleteFailedWithError": "Kunne ikke slette samling: ${error}",
|
||||
"failedToLoadItems": "Kunne ikke laste samlingselementer: ${error}",
|
||||
"selectCollection": "Velg samling",
|
||||
"collectionName": "Samlingsnavn",
|
||||
"enterCollectionName": "Skriv inn samlingsnavn",
|
||||
"addedToCollection": "Lagt til i samling",
|
||||
"errorAddingToCollection": "Kunne ikke legge til i samling",
|
||||
"created": "Samling opprettet",
|
||||
"removeFromCollection": "Fjern fra samling",
|
||||
"removeFromCollectionConfirm": "Fjerne \"${title}\" fra denne samlingen?",
|
||||
"removedFromCollection": "Fjernet fra samling",
|
||||
"removeFromCollectionFailed": "Kunne ikke fjerne fra samling",
|
||||
"removeFromCollectionError": "Feil ved fjerning fra samling: ${error}",
|
||||
"searchCollections": "Søk i samlinger..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Spillelister",
|
||||
"playlist": "Spilleliste",
|
||||
"noPlaylists": "Ingen spillelister funnet",
|
||||
"create": "Opprett spilleliste",
|
||||
"playlistName": "Spillelistenavn",
|
||||
"enterPlaylistName": "Skriv inn spillelistenavn",
|
||||
"delete": "Slett spilleliste",
|
||||
"removeItem": "Fjern fra spilleliste",
|
||||
"smartPlaylist": "Smart spilleliste",
|
||||
"itemCount": "${count} elementer",
|
||||
"oneItem": "1 element",
|
||||
"emptyPlaylist": "Denne spillelisten er tom",
|
||||
"deleteConfirm": "Slett spilleliste?",
|
||||
"deleteMessage": "Er du sikker på at du vil slette \"${name}\"?",
|
||||
"created": "Spilleliste opprettet",
|
||||
"deleted": "Spilleliste slettet",
|
||||
"itemAdded": "Lagt til i spilleliste",
|
||||
"itemRemoved": "Fjernet fra spilleliste",
|
||||
"selectPlaylist": "Velg spilleliste",
|
||||
"errorCreating": "Kunne ikke opprette spilleliste",
|
||||
"errorDeleting": "Kunne ikke slette spilleliste",
|
||||
"errorLoading": "Kunne ikke laste spillelister",
|
||||
"errorAdding": "Kunne ikke legge til i spilleliste",
|
||||
"errorReordering": "Kunne ikke omorganisere spillelisteelement",
|
||||
"errorRemoving": "Kunne ikke fjerne fra spilleliste"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Se sammen",
|
||||
"description": "Se innhold synkronisert med venner og familie",
|
||||
"createSession": "Opprett økt",
|
||||
"creating": "Oppretter...",
|
||||
"joinSession": "Bli med i økt",
|
||||
"joining": "Blir med...",
|
||||
"controlMode": "Kontrollmodus",
|
||||
"controlModeQuestion": "Hvem kan kontrollere avspilling?",
|
||||
"hostOnly": "Kun vert",
|
||||
"anyone": "Alle",
|
||||
"hostingSession": "Er vert for økt",
|
||||
"inSession": "I økt",
|
||||
"sessionCode": "Øktkode",
|
||||
"hostControlsPlayback": "Verten kontrollerer avspilling",
|
||||
"anyoneCanControl": "Alle kan kontrollere avspilling",
|
||||
"hostControls": "Vertskontroll",
|
||||
"anyoneControls": "Alle kontrollerer",
|
||||
"participants": "Deltakere",
|
||||
"host": "Vert",
|
||||
"hostBadge": "VERT",
|
||||
"youAreHost": "Du er verten",
|
||||
"watchingWithOthers": "Ser med andre",
|
||||
"endSession": "Avslutt økt",
|
||||
"leaveSession": "Forlat økt",
|
||||
"endSessionQuestion": "Avslutte økt?",
|
||||
"leaveSessionQuestion": "Forlate økt?",
|
||||
"endSessionConfirm": "Dette vil avslutte økten for alle deltakere.",
|
||||
"leaveSessionConfirm": "Du vil bli fjernet fra økten.",
|
||||
"endSessionConfirmOverlay": "Dette vil avslutte se sammen-økten for alle deltakere.",
|
||||
"leaveSessionConfirmOverlay": "Du vil bli frakoblet fra se sammen-økten.",
|
||||
"end": "Avslutt",
|
||||
"leave": "Forlat",
|
||||
"syncing": "Synkroniserer...",
|
||||
"joinWatchSession": "Bli med i se sammen-økt",
|
||||
"enterCodeHint": "Skriv inn 5-tegns kode",
|
||||
"pasteFromClipboard": "Lim inn fra utklippstavle",
|
||||
"pleaseEnterCode": "Vennligst skriv inn en øktkode",
|
||||
"codeMustBe5Chars": "Øktkoden må være 5 tegn",
|
||||
"joinInstructions": "Skriv inn øktkoden delt av verten for å bli med i se sammen-økten.",
|
||||
"failedToCreate": "Kunne ikke opprette økt",
|
||||
"failedToJoin": "Kunne ikke bli med i økt",
|
||||
"sessionCodeCopied": "Øktkode kopiert til utklippstavle",
|
||||
"relayUnreachable": "Reléserveren er utilgjengelig. Dette kan skyldes at internettleverandøren din blokkerer tilkoblingen. Du kan fortsatt prøve, men Se sammen fungerer kanskje ikke.",
|
||||
"reconnectingToHost": "Kobler til verten på nytt...",
|
||||
"currentPlayback": "Gjeldende avspilling",
|
||||
"joinCurrentPlayback": "Bli med i gjeldende avspilling",
|
||||
"joinCurrentPlaybackDescription": "Hopp tilbake til det verten ser på nå",
|
||||
"failedToOpenCurrentPlayback": "Kunne ikke åpne gjeldende avspilling",
|
||||
"participantJoined": "${name} ble med",
|
||||
"participantLeft": "${name} forlot",
|
||||
"participantPaused": "${name} pauset",
|
||||
"participantResumed": "${name} gjenopptok",
|
||||
"participantSeeked": "${name} spolet",
|
||||
"participantBuffering": "${name} buffrer",
|
||||
"waitingForParticipants": "Venter på at andre laster inn...",
|
||||
"recentRooms": "Nylige rom",
|
||||
"renameRoom": "Gi nytt navn til rom",
|
||||
"removeRoom": "Fjern"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Nedlastinger",
|
||||
"manage": "Administrer",
|
||||
"tvShows": "TV-serier",
|
||||
"movies": "Filmer",
|
||||
"noDownloads": "Ingen nedlastinger ennå",
|
||||
"noDownloadsDescription": "Nedlastet innhold vil vises her for frakoblet visning",
|
||||
"downloadNow": "Last ned",
|
||||
"deleteDownload": "Slett nedlasting",
|
||||
"retryDownload": "Prøv nedlasting på nytt",
|
||||
"downloadQueued": "Nedlasting i kø",
|
||||
"serverErrorBitrate": "Serverfeil — filen overskrider muligens grensen for fjernstrømming-bitrate",
|
||||
"episodesQueued": "${count} episoder i nedlastingskø",
|
||||
"downloadDeleted": "Nedlasting slettet",
|
||||
"deleteConfirm": "Er du sikker på at du vil slette \"${title}\"? Dette vil fjerne den nedlastede filen fra enheten din.",
|
||||
"deletingWithProgress": "Sletter ${title}... (${current} av ${total})",
|
||||
"noDownloadsTree": "Ingen nedlastinger",
|
||||
"pauseAll": "Pause alle",
|
||||
"resumeAll": "Gjenoppta alle",
|
||||
"deleteAll": "Slett alle",
|
||||
"selectVersion": "Velg versjon",
|
||||
"allEpisodes": "Alle episoder",
|
||||
"unwatchedOnly": "Kun usette",
|
||||
"nextNUnwatched": "Neste ${count} usette",
|
||||
"customAmount": "Egendefinert antall...",
|
||||
"howManyEpisodes": "Hvor mange episoder?",
|
||||
"itemsQueued": "${count} elementer i nedlastingskø",
|
||||
"keepSynced": "Hold synkronisert",
|
||||
"downloadOnce": "Last ned én gang",
|
||||
"keepNUnwatched": "Behold ${count} usette",
|
||||
"editSyncRule": "Rediger synkroniseringsregel",
|
||||
"removeSyncRule": "Fjern synkroniseringsregel",
|
||||
"removeSyncRuleConfirm": "Slutte å synkronisere \"${title}\"? Nedlastede episoder beholdes.",
|
||||
"syncRuleCreated": "Synkroniseringsregel opprettet — beholder ${count} usette episoder",
|
||||
"syncRuleUpdated": "Synkroniseringsregel oppdatert",
|
||||
"syncRuleRemoved": "Synkroniseringsregel fjernet",
|
||||
"syncedNewEpisodes": "Synkroniserte ${count} nye episoder for ${title}",
|
||||
"activeSyncRules": "Synkroniseringsregler",
|
||||
"noSyncRules": "Ingen synkroniseringsregler",
|
||||
"manageSyncRule": "Administrer synkronisering",
|
||||
"editEpisodeCount": "Antall episoder",
|
||||
"editSyncFilter": "Synkroniseringsfilter",
|
||||
"syncAllItems": "Synkroniserer alle elementer",
|
||||
"syncUnwatchedItems": "Synkroniserer usette elementer",
|
||||
"syncRuleListCreated": "Synkroniseringsregel opprettet"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shadere",
|
||||
"noShaderDescription": "Ingen videoforbedring",
|
||||
"nvscalerDescription": "NVIDIA bildeskalering for skarpere video",
|
||||
"qualityFast": "Rask",
|
||||
"qualityHQ": "Høy kvalitet",
|
||||
"mode": "Modus",
|
||||
"importShader": "Importer shader",
|
||||
"customShaderDescription": "Egendefinert GLSL-shader",
|
||||
"shaderImported": "Shader importert",
|
||||
"shaderImportFailed": "Kunne ikke importere shader",
|
||||
"deleteShader": "Slett shader",
|
||||
"deleteShaderConfirm": "Slette \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Følgesvenn-fjernkontroll",
|
||||
"connectedTo": "Tilkoblet ${name}",
|
||||
"session": {
|
||||
"startingServer": "Starter fjernserver...",
|
||||
"failedToCreate": "Kunne ikke starte fjernserver:",
|
||||
"hostAddress": "Vertsadresse",
|
||||
"connected": "Tilkoblet",
|
||||
"serverRunning": "Fjernserver aktiv",
|
||||
"serverStopped": "Fjernserver stoppet",
|
||||
"serverRunningDescription": "Mobilenheter på nettverket ditt kan oppdage og koble til denne appen",
|
||||
"serverStoppedDescription": "Start serveren for å la mobilenheter koble til",
|
||||
"usePhoneToControl": "Bruk mobilenheten din til å styre denne appen",
|
||||
"startServer": "Start server",
|
||||
"stopServer": "Stopp server",
|
||||
"minimize": "Minimer"
|
||||
},
|
||||
"pairing": {
|
||||
"discoveryDescription": "Enheter på nettverket ditt som kjører Plezy med samme Plex-konto vil vises automatisk",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"connecting": "Kobler til...",
|
||||
"searchingForDevices": "Søker etter enheter...",
|
||||
"noDevicesFound": "Ingen enheter funnet på nettverket ditt",
|
||||
"noDevicesHint": "Sørg for at Plezy er åpen på datamaskinen din og at begge enhetene er på samme WiFi-nettverk",
|
||||
"availableDevices": "Tilgjengelige enheter",
|
||||
"manualConnection": "Manuell tilkobling",
|
||||
"cryptoInitFailed": "Kunne ikke initialisere sikker tilkobling. Sørg for at du er logget inn på en Plex-konto.",
|
||||
"validationHostRequired": "Vennligst oppgi vertsadresse",
|
||||
"validationHostFormat": "Format må være IP:port (f.eks. 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "Tilkoblingen ble tidsavbrutt. Sørg for at begge enhetene er på samme nettverk.",
|
||||
"sessionNotFound": "Kunne ikke finne enheten. Sørg for at Plezy kjører på verten.",
|
||||
"authFailed": "Autentisering mislyktes. Sørg for at begge enhetene bruker samme Plex-konto.",
|
||||
"failedToConnect": "Kunne ikke koble til: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
"disconnectConfirm": "Vil du koble fra fjernøkten?",
|
||||
"reconnecting": "Kobler til på nytt...",
|
||||
"attemptOf": "Forsøk ${current} av 5",
|
||||
"retryNow": "Prøv nå",
|
||||
"tabRemote": "Fjernkontroll",
|
||||
"tabPlay": "Spill av",
|
||||
"tabMore": "Mer",
|
||||
"menu": "Meny",
|
||||
"tabNavigation": "Fanenavigering",
|
||||
"tabDiscover": "Oppdag",
|
||||
"tabLibraries": "Biblioteker",
|
||||
"tabSearch": "Søk",
|
||||
"tabDownloads": "Nedlastinger",
|
||||
"tabSettings": "Innstillinger",
|
||||
"previous": "Forrige",
|
||||
"playPause": "Spill av/Pause",
|
||||
"next": "Neste",
|
||||
"seekBack": "Spol tilbake",
|
||||
"stop": "Stopp",
|
||||
"seekForward": "Spol fremover",
|
||||
"volume": "Volum",
|
||||
"volumeDown": "Ned",
|
||||
"volumeUp": "Opp",
|
||||
"fullscreen": "Fullskjerm",
|
||||
"subtitles": "Undertekster",
|
||||
"audio": "Lyd",
|
||||
"searchHint": "Søk på stasjonær..."
|
||||
}
|
||||
},
|
||||
"videoSettings": {
|
||||
"playbackSettings": "Avspillingsinnstillinger",
|
||||
"playbackSpeed": "Avspillingshastighet",
|
||||
"sleepTimer": "Søvntimer",
|
||||
"audioSync": "Lydsynkronisering",
|
||||
"subtitleSync": "Undertekstsynkronisering",
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Lydutgang",
|
||||
"performanceOverlay": "Ytelsesoverlegg",
|
||||
"audioPassthrough": "Lydgjennomgang",
|
||||
"audioNormalization": "Lydnormalisering"
|
||||
},
|
||||
"externalPlayer": {
|
||||
"title": "Ekstern spiller",
|
||||
"useExternalPlayer": "Bruk ekstern spiller",
|
||||
"useExternalPlayerDescription": "Åpne videoer i en ekstern app i stedet for den innebygde spilleren",
|
||||
"selectPlayer": "Velg spiller",
|
||||
"customPlayers": "Egendefinerte spillere",
|
||||
"systemDefault": "Systemstandard",
|
||||
"addCustomPlayer": "Legg til egendefinert spiller",
|
||||
"playerName": "Spillernavn",
|
||||
"playerCommand": "Kommando",
|
||||
"playerPackage": "Pakkenavn",
|
||||
"playerUrlScheme": "URL-skjema",
|
||||
"off": "Av",
|
||||
"launchFailed": "Kunne ikke åpne ekstern spiller",
|
||||
"appNotInstalled": "${name} er ikke installert",
|
||||
"playInExternalPlayer": "Spill av i ekstern spiller"
|
||||
},
|
||||
"metadataEdit": {
|
||||
"editMetadata": "Rediger...",
|
||||
"screenTitle": "Rediger metadata",
|
||||
"basicInfo": "Grunnleggende info",
|
||||
"artwork": "Kunstverk",
|
||||
"advancedSettings": "Avanserte innstillinger",
|
||||
"title": "Tittel",
|
||||
"sortTitle": "Sorteringsstittel",
|
||||
"originalTitle": "Originaltittel",
|
||||
"releaseDate": "Utgivelsesdato",
|
||||
"contentRating": "Innholdsvurdering",
|
||||
"studio": "Studio",
|
||||
"tagline": "Slagord",
|
||||
"summary": "Sammendrag",
|
||||
"poster": "Plakat",
|
||||
"background": "Bakgrunn",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Kvadratisk bilde",
|
||||
"selectPoster": "Velg plakat",
|
||||
"selectBackground": "Velg bakgrunn",
|
||||
"selectLogo": "Velg logo",
|
||||
"selectSquareArt": "Velg kvadratisk bilde",
|
||||
"fromUrl": "Fra URL",
|
||||
"uploadFile": "Last opp fil",
|
||||
"enterImageUrl": "Skriv inn bilde-URL",
|
||||
"imageUrl": "Bilde-URL",
|
||||
"metadataUpdated": "Metadata oppdatert",
|
||||
"metadataUpdateFailed": "Kunne ikke oppdatere metadata",
|
||||
"artworkUpdated": "Kunstverk oppdatert",
|
||||
"artworkUpdateFailed": "Kunne ikke oppdatere kunstverk",
|
||||
"noArtworkAvailable": "Ingen kunstverk tilgjengelig",
|
||||
"notSet": "Ikke angitt",
|
||||
"libraryDefault": "Bibliotekstandard",
|
||||
"accountDefault": "Kontostandard",
|
||||
"seriesDefault": "Seriestandard",
|
||||
"episodeSorting": "Episodesortering",
|
||||
"oldestFirst": "Eldste først",
|
||||
"newestFirst": "Nyeste først",
|
||||
"keep": "Behold",
|
||||
"allEpisodes": "Alle episoder",
|
||||
"latestEpisodes": "${count} nyeste episoder",
|
||||
"latestEpisode": "Nyeste episode",
|
||||
"episodesAddedPastDays": "Episoder lagt til de siste ${count} dagene",
|
||||
"deleteAfterPlaying": "Slett episoder etter avspilling",
|
||||
"never": "Aldri",
|
||||
"afterADay": "Etter en dag",
|
||||
"afterAWeek": "Etter en uke",
|
||||
"afterAMonth": "Etter en måned",
|
||||
"onNextRefresh": "Ved neste oppdatering",
|
||||
"seasons": "Sesonger",
|
||||
"show": "Vis",
|
||||
"hide": "Skjul",
|
||||
"episodeOrdering": "Episoderekkefølge",
|
||||
"tmdbAiring": "The Movie Database (Sendt)",
|
||||
"tvdbAiring": "TheTVDB (Sendt)",
|
||||
"tvdbAbsolute": "TheTVDB (Absolutt)",
|
||||
"metadataLanguage": "Metadataspråk",
|
||||
"useOriginalTitle": "Bruk originaltittel",
|
||||
"preferredAudioLanguage": "Foretrukket lydspråk",
|
||||
"preferredSubtitleLanguage": "Foretrukket undertekstspråk",
|
||||
"subtitleMode": "Automatisk valg av undertekstmodus",
|
||||
"manuallySelected": "Manuelt valgt",
|
||||
"shownWithForeignAudio": "Vist med fremmedspråklig lyd",
|
||||
"alwaysEnabled": "Alltid aktivert",
|
||||
"tags": "Tagger",
|
||||
"addTag": "Legg til tagg",
|
||||
"genre": "Sjanger",
|
||||
"director": "Regissør",
|
||||
"writer": "Forfatter",
|
||||
"producer": "Produsent",
|
||||
"country": "Land",
|
||||
"collection": "Samling",
|
||||
"label": "Etikett",
|
||||
"style": "Stil",
|
||||
"mood": "Stemning"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Serveroppgaver",
|
||||
"failedToLoad": "Kunne ikke laste oppgaver",
|
||||
"noTasks": "Ingen oppgaver kjører"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Tilkoblet",
|
||||
"connectedAs": "Tilkoblet som @${username}",
|
||||
"disconnectConfirm": "Koble fra Trakt-konto?",
|
||||
"disconnectConfirmBody": "Plezy slutter å sende avspillingshendelser til Trakt. Du kan koble til igjen når som helst.",
|
||||
"scrobble": "Sanntids-scrobbling",
|
||||
"scrobbleDescription": "Send avspillings-, pause- og stopphendelser til Trakt under avspilling.",
|
||||
"watchedSync": "Synkroniser sett-status",
|
||||
"watchedSyncDescription": "Når du markerer noe som sett i Plezy, markeres det også på Trakt.",
|
||||
"deviceCodeTitle": "Aktiver Plezy på Trakt",
|
||||
"deviceCodeBody": "Besøk ${url} og skriv inn denne koden:",
|
||||
"openTraktActivate": "Åpne Trakt for å aktivere",
|
||||
"waitingForAuthorization": "Venter på autorisering…",
|
||||
"codeCopied": "Kode kopiert",
|
||||
"connectFailed": "Kunne ikke koble til Trakt. Prøv igjen."
|
||||
}
|
||||
}
|
||||
+249
-166
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "Inloggen met Plex",
|
||||
"showQRCode": "Toon QR-code",
|
||||
"authenticate": "Authenticeren",
|
||||
"debugEnterToken": "Debug: Voer Plex Token in",
|
||||
"plexTokenLabel": "Plex Authenticatietoken",
|
||||
"plexTokenHint": "Voer je Plex.tv token in",
|
||||
"authenticationTimeout": "Authenticatie verlopen. Probeer opnieuw.",
|
||||
"scanQRToSignIn": "Scan deze QR-code om in te loggen",
|
||||
"waitingForAuth": "Wachten op authenticatie...\nVoltooi het inloggen in je browser.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "Verwijderen",
|
||||
"shuffle": "Willekeurig",
|
||||
"addTo": "Toevoegen aan...",
|
||||
"remove": "Verwijderen",
|
||||
"paste": "Plakken",
|
||||
"createNew": "Nieuw aanmaken",
|
||||
"connect": "Verbinden",
|
||||
"disconnect": "Verbinding verbreken",
|
||||
"play": "Afspelen",
|
||||
@@ -43,10 +39,9 @@
|
||||
"search": "Zoeken",
|
||||
"home": "Home",
|
||||
"back": "Terug",
|
||||
"settings": "Instellingen",
|
||||
"settings": "Opties",
|
||||
"mute": "Dempen",
|
||||
"ok": "OK",
|
||||
"loading": "Laden...",
|
||||
"reconnect": "Opnieuw verbinden",
|
||||
"exitConfirmTitle": "App afsluiten?",
|
||||
"exitConfirmMessage": "Weet je zeker dat je wilt afsluiten?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "Servers vernieuwen...",
|
||||
"loadingServers": "Servers laden...",
|
||||
"connectingToServers": "Verbinden met servers...",
|
||||
"startingOfflineMode": "Offlinemodus starten..."
|
||||
"startingOfflineMode": "Offlinemodus starten...",
|
||||
"loading": "Laden..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licenties",
|
||||
"switchProfile": "Wissel van profiel",
|
||||
"subtitleStyling": "Ondertitel opmaak",
|
||||
"mpvConfig": "MPV-configuratie",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logbestanden"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"supportDeveloper": "Steun Plezy",
|
||||
"supportDeveloperDescription": "Doneer via Liberapay om de ontwikkeling te steunen",
|
||||
"language": "Taal",
|
||||
"theme": "Thema",
|
||||
"appearance": "Uiterlijk",
|
||||
"videoPlayback": "Video afspelen",
|
||||
"videoPlaybackDescription": "Afspeelgedrag configureren",
|
||||
"advanced": "Geavanceerd",
|
||||
"episodePosterMode": "Aflevering poster stijl",
|
||||
"seriesPoster": "Serie poster",
|
||||
"seriesPosterDescription": "Toon de serie poster voor alle afleveringen",
|
||||
"seasonPoster": "Seizoen poster",
|
||||
"seasonPosterDescription": "Toon de seizoensspecifieke poster voor afleveringen",
|
||||
"episodeThumbnail": "Aflevering miniatuur",
|
||||
"episodeThumbnailDescription": "Toon 16:9 aflevering miniaturen",
|
||||
"episodeThumbnail": "Miniatuur",
|
||||
"showHeroSectionDescription": "Toon uitgelichte inhoud carrousel op startscherm",
|
||||
"secondsLabel": "Seconden",
|
||||
"minutesLabel": "Minuten",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Voer duur in (${min}-${max})",
|
||||
"systemTheme": "Systeem",
|
||||
"systemThemeDescription": "Volg systeeminstellingen",
|
||||
"lightTheme": "Licht",
|
||||
"darkTheme": "Donker",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "Puur zwart voor OLED-schermen",
|
||||
"libraryDensity": "Bibliotheek dichtheid",
|
||||
"compact": "Compact",
|
||||
"compactDescription": "Kleinere kaarten, meer items zichtbaar",
|
||||
"normal": "Normaal",
|
||||
"normalDescription": "Standaard grootte",
|
||||
"comfortable": "Comfortabel",
|
||||
"comfortableDescription": "Grotere kaarten, minder items zichtbaar",
|
||||
"viewMode": "Weergavemodus",
|
||||
"gridView": "Raster",
|
||||
"gridViewDescription": "Items weergeven in een rasterindeling",
|
||||
"listView": "Lijst",
|
||||
"listViewDescription": "Items weergeven in een lijstindeling",
|
||||
"showHeroSection": "Toon hoofdsectie",
|
||||
"useGlobalHubs": "Plex Home-indeling gebruiken",
|
||||
"useGlobalHubsDescription": "Toon startpagina-hubs zoals de officiële Plex-client. Indien uitgeschakeld, worden in plaats daarvan aanbevelingen per bibliotheek getoond.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "Miniaturen vervagen en beschrijvingen verbergen voor afleveringen die je nog niet hebt gezien",
|
||||
"playerBackend": "Speler backend",
|
||||
"exoPlayer": "ExoPlayer (Aanbevolen)",
|
||||
"exoPlayerDescription": "Android-native speler met betere hardware-ondersteuning",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "Geavanceerde speler met meer functies en ASS-ondertitelondersteuning",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Hardware decodering",
|
||||
"hardwareDecodingDescription": "Gebruik hardware versnelling indien beschikbaar",
|
||||
"bufferSize": "Buffer grootte",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "Pas ondertitel uiterlijk aan",
|
||||
"smallSkipDuration": "Korte skip duur",
|
||||
"largeSkipDuration": "Lange skip duur",
|
||||
"rewindOnResume": "Terugspoelen bij hervatten",
|
||||
"secondsUnit": "${seconds} seconden",
|
||||
"defaultSleepTimer": "Standaard slaap timer",
|
||||
"minutesUnit": "bij ${minutes} minuten",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "Pas toetsenbord sneltoetsen aan",
|
||||
"videoPlayerNavigation": "Videospeler navigatie",
|
||||
"videoPlayerNavigationDescription": "Gebruik pijltjestoetsen om door de videospeler bediening te navigeren",
|
||||
"watchTogetherRelay": "Samen Kijken Relay",
|
||||
"watchTogetherRelayDescription": "Stel een aangepaste relay-server in voor Samen Kijken. Alle deelnemers moeten dezelfde server gebruiken.",
|
||||
"watchTogetherRelayHint": "https://mijn-relay.voorbeeld.nl",
|
||||
"crashReporting": "Crashrapportage",
|
||||
"crashReportingDescription": "Crashrapporten verzenden om de app te verbeteren",
|
||||
"debugLogging": "Debug logging",
|
||||
"debugLoggingDescription": "Schakel gedetailleerde logging in voor probleemoplossing",
|
||||
"viewLogs": "Bekijk logs",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "Credits automatisch overslaan en volgende aflevering afspelen",
|
||||
"autoSkipDelay": "Vertraging Automatisch Overslaan",
|
||||
"autoSkipDelayDescription": "${seconds} seconden wachten voor automatisch overslaan",
|
||||
"introPattern": "Intromarkeringspatroon",
|
||||
"introPatternDescription": "Reguliere expressie om intromarkeringen in hoofdstuktitels te herkennen",
|
||||
"creditsPattern": "Aftitelingmarkeringspatroon",
|
||||
"creditsPatternDescription": "Reguliere expressie om aftitelingmarkeringen in hoofdstuktitels te herkennen",
|
||||
"invalidRegex": "Ongeldige reguliere expressie",
|
||||
"downloads": "Downloads",
|
||||
"downloadLocationDescription": "Kies waar gedownloade content wordt opgeslagen",
|
||||
"downloadLocationDefault": "Standaard (App-opslag)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "Kan map niet selecteren",
|
||||
"downloadOnWifiOnly": "Alleen via WiFi downloaden",
|
||||
"downloadOnWifiOnlyDescription": "Voorkom downloads bij gebruik van mobiele data",
|
||||
"autoRemoveWatchedDownloads": "Bekeken downloads automatisch verwijderen",
|
||||
"autoRemoveWatchedDownloadsDescription": "Gedownloade afleveringen en films automatisch verwijderen wanneer ze als bekeken zijn gemarkeerd",
|
||||
"cellularDownloadBlocked": "Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling.",
|
||||
"maxVolume": "Maximaal volume",
|
||||
"maxVolumeDescription": "Volume boven 100% toestaan voor stille media",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Toon op Discord wat je aan het kijken bent",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Kijkgeschiedenis synchroniseren met Trakt",
|
||||
"companionRemoteServer": "Companion Remote-server",
|
||||
"companionRemoteServerDescription": "Sta mobiele apparaten op je netwerk toe om deze app te bedienen",
|
||||
"autoPip": "Automatische beeld-in-beeld",
|
||||
"autoPipDescription": "Automatisch beeld-in-beeld activeren bij het verlaten van de app tijdens afspelen",
|
||||
"matchContentFrameRate": "Inhoudsframesnelheid afstemmen",
|
||||
"matchContentFrameRateDescription": "Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "Verversingssnelheid afstemmen",
|
||||
"matchRefreshRateDescription": "Schermverversingssnelheid aanpassen aan de video-inhoud op volledig scherm",
|
||||
"matchDynamicRange": "Dynamisch bereik afstemmen",
|
||||
"matchDynamicRangeDescription": "HDR automatisch inschakelen voor HDR-inhoud en terugkeren naar SDR bij het verlaten van de speler",
|
||||
"displaySwitchDelay": "Vertraging bij schermwisseling",
|
||||
"tunneledPlayback": "Getunnelde weergave",
|
||||
"tunneledPlaybackDescription": "Gebruik hardwareversnelde videotunneling. Schakel uit als je een zwart scherm met geluid ziet bij HDR-content",
|
||||
"requireProfileSelectionOnOpen": "Vraag om profiel bij openen",
|
||||
"requireProfileSelectionOnOpenDescription": "Toon profielselectie telkens wanneer de app wordt geopend",
|
||||
"confirmExitOnBack": "Bevestigen voor afsluiten",
|
||||
"confirmExitOnBackDescription": "Toon een bevestigingsvenster bij het drukken op terug om de app af te sluiten"
|
||||
"confirmExitOnBackDescription": "Toon een bevestigingsvenster bij het drukken op terug om de app af te sluiten",
|
||||
"forceTvMode": "TV-modus forceren",
|
||||
"forceTvModeDescription": "Gebruik de TV-indeling ongeacht automatische detectie. Handig op Android TV-apparaten die de leanback-functie niet melden. Start de app opnieuw bij wijziging.",
|
||||
"autoHidePerformanceOverlay": "Prestatie-overlay automatisch verbergen",
|
||||
"autoHidePerformanceOverlayDescription": "Laat de prestatie-overlay meevervagen met de afspeelknoppen",
|
||||
"showNavBarLabels": "Navigatiebalk labels tonen",
|
||||
"showNavBarLabelsDescription": "Tekstlabels onder de pictogrammen van de navigatiebalk weergeven",
|
||||
"liveTvDefaultFavorites": "Standaard favoriete zenders",
|
||||
"liveTvDefaultFavoritesDescription": "Toon alleen favoriete zenders bij het openen van Live TV",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Zoek films, series, muziek...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "Intro/aftiteling overslaan"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "Voer PIN in",
|
||||
"showPin": "Toon PIN",
|
||||
"hidePin": "Verberg PIN"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Bestand info",
|
||||
"video": "Video",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "Kleurprimaires",
|
||||
"chromaSubsampling": "Chroma subsampling",
|
||||
"channels": "Kanalen",
|
||||
"subtitles": "Ondertitels",
|
||||
"overallBitrate": "Totale bitrate",
|
||||
"path": "Pad",
|
||||
"size": "Grootte",
|
||||
"container": "Container",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "Dit omvat alle afleveringen en hun bestanden.",
|
||||
"mediaDeletedSuccessfully": "Media-item succesvol verwijderd",
|
||||
"mediaFailedToDelete": "Verwijderen van media-item mislukt",
|
||||
"rate": "Beoordelen"
|
||||
"rate": "Beoordelen",
|
||||
"playFromBeginning": "Afspelen vanaf het begin",
|
||||
"playVersion": "Versie afspelen..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, film",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "Ontgrendel rotatie",
|
||||
"timerActive": "Timer actief",
|
||||
"playbackWillPauseIn": "Afspelen wordt gepauzeerd over ${duration}",
|
||||
"sleepTimerCompleted": "Slaaptimer voltooid - afspelen gepauzeerd",
|
||||
"stillWatching": "Kijk je nog?",
|
||||
"pausingIn": "Pauze over ${seconds}s",
|
||||
"continueWatching": "Doorgaan",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "Dempen",
|
||||
"unmuteButton": "Dempen opheffen",
|
||||
"settingsButton": "Video-instellingen",
|
||||
"audioTrackButton": "Audiosporen",
|
||||
"subtitlesButton": "Ondertitels",
|
||||
"tracksButton": "Audio en ondertitels",
|
||||
"chaptersButton": "Hoofdstukken",
|
||||
"versionsButton": "Videoversies",
|
||||
"pipButton": "Beeld-in-beeld modus",
|
||||
"aspectRatioButton": "Beeldverhouding",
|
||||
"ambientLighting": "Omgevingsverlichting",
|
||||
"ambientLightingOn": "Omgevingsverlichting inschakelen",
|
||||
"ambientLightingOff": "Omgevingsverlichting uitschakelen",
|
||||
"fullscreenButton": "Volledig scherm activeren",
|
||||
"exitFullscreenButton": "Volledig scherm verlaten",
|
||||
"alwaysOnTopButton": "Altijd bovenop",
|
||||
"rotationLockButton": "Rotatievergrendeling",
|
||||
"lockScreen": "Vergrendel scherm",
|
||||
"screenLockButton": "Schermvergrendeling",
|
||||
"longPressToUnlock": "Lang indrukken om te ontgrendelen",
|
||||
"timelineSlider": "Videotijdlijn",
|
||||
"volumeSlider": "Volumeniveau",
|
||||
"endsAt": "Eindigt om ${time}",
|
||||
"pipActive": "Afspelen in beeld-in-beeld",
|
||||
"pipFailed": "Beeld-in-beeld kon niet worden gestart",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Vereist Android 8.0 of nieuwer",
|
||||
"iosVersion": "Vereist iOS 15.0 of nieuwer",
|
||||
"permissionDisabled": "Beeld-in-beeld toestemming is uitgeschakeld. Schakel deze in via Instellingen > Apps > Plezy > Beeld-in-beeld",
|
||||
"notSupported": "Dit apparaat ondersteunt geen beeld-in-beeld modus",
|
||||
"voSwitchFailed": "Kan video-uitvoer niet wisselen voor beeld-in-beeld",
|
||||
"failed": "Beeld-in-beeld kon niet worden gestart",
|
||||
"unknown": "Er is een fout opgetreden: ${error}"
|
||||
},
|
||||
"chapters": "Hoofdstukken",
|
||||
"noChaptersAvailable": "Geen hoofdstukken beschikbaar",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "Wachtrij",
|
||||
"noQueueItems": "Geen items in de wachtrij",
|
||||
"searchSubtitles": "Ondertitels zoeken",
|
||||
"language": "Taal",
|
||||
"noSubtitlesFound": "Geen ondertitels gevonden",
|
||||
"subtitleDownloaded": "Ondertitel gedownload",
|
||||
"subtitleDownloadFailed": "Ondertitel downloaden mislukt",
|
||||
"searchLanguages": "Talen zoeken..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Beheerder",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "Gemarkeerd als ongekeken",
|
||||
"markedAsWatchedOffline": "Gemarkeerd als gekeken (sync wanneer online)",
|
||||
"markedAsUnwatchedOffline": "Gemarkeerd als ongekeken (sync wanneer online)",
|
||||
"autoRemovedWatchedDownload": "Automatisch verwijderd: ${title}",
|
||||
"removedFromContinueWatching": "Verwijderd uit Doorgaan met kijken",
|
||||
"errorLoading": "Fout: ${error}",
|
||||
"fileInfoNotAvailable": "Bestand informatie niet beschikbaar",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "Kan afspeelwachtrij niet maken - geen items",
|
||||
"failedPlayback": "Afspelen van ${action} mislukt: ${error}",
|
||||
"switchingToCompatiblePlayer": "Overschakelen naar compatibele speler...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "Afspelen mislukt",
|
||||
"serverLimitBody": "De server gaf een fout terug (HTTP 500). Dit betekent meestal dat de servereigenaar een bandbreedte- of transcoderingslimiet heeft ingesteld die jouw sessie weigert. Er valt vanaf de client niets te doen — de servereigenaar moet de instellingen aanpassen.",
|
||||
"logsUploaded": "Logs geüpload",
|
||||
"logsUploadFailed": "Uploaden van logs mislukt",
|
||||
"logId": "Log-ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "Opmaak opties",
|
||||
"text": "Tekst",
|
||||
"border": "Rand",
|
||||
"background": "Achtergrond",
|
||||
"fontSize": "Lettergrootte",
|
||||
"textColor": "Tekstkleur",
|
||||
"borderSize": "Rand grootte",
|
||||
"borderColor": "Randkleur",
|
||||
"backgroundOpacity": "Achtergrond transparantie",
|
||||
"backgroundColor": "Achtergrondkleur",
|
||||
"position": "Position"
|
||||
"position": "Positie",
|
||||
"assOverride": "ASS-overschrijving",
|
||||
"bold": "Vet",
|
||||
"italic": "Cursief"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV-configuratie",
|
||||
"title": "mpv-configuratie",
|
||||
"description": "Geavanceerde videospeler-instellingen",
|
||||
"properties": "Eigenschappen",
|
||||
"presets": "Voorinstellingen",
|
||||
"noProperties": "Geen eigenschappen geconfigureerd",
|
||||
"noPresets": "Geen opgeslagen voorinstellingen",
|
||||
"addProperty": "Eigenschap toevoegen",
|
||||
"editProperty": "Eigenschap bewerken",
|
||||
"deleteProperty": "Eigenschap verwijderen",
|
||||
"propertyKey": "Eigenschapssleutel",
|
||||
"propertyKeyHint": "bijv. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Eigenschapswaarde",
|
||||
"propertyValueHint": "bijv. auto, 256000000",
|
||||
"saveAsPreset": "Opslaan als voorinstelling...",
|
||||
"presetName": "Naam voorinstelling",
|
||||
"presetNameHint": "Voer een naam in voor deze voorinstelling",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "Voorinstelling geladen",
|
||||
"presetDeleted": "Voorinstelling verwijderd",
|
||||
"confirmDeletePreset": "Weet je zeker dat je deze voorinstelling wilt verwijderen?",
|
||||
"confirmDeleteProperty": "Weet je zeker dat je deze eigenschap wilt verwijderen?",
|
||||
"entriesCount": "${count} items"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Bevestig actie"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "Overzicht",
|
||||
"cast": "Acteurs",
|
||||
"extras": "Trailers & Extra's",
|
||||
"seasons": "Seizoenen",
|
||||
"studio": "Studio",
|
||||
"rating": "Leeftijd",
|
||||
"episodeCount": "${count} afleveringen",
|
||||
"watchedProgress": "${watched}/${total} gekeken",
|
||||
"movie": "Film",
|
||||
"tvShow": "TV Serie",
|
||||
"minutesLeft": "${minutes} min over"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "Afspeellijsten"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Groepering",
|
||||
"all": "Alles",
|
||||
"movies": "Films",
|
||||
"shows": "Series",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "Wis logs",
|
||||
"copyLogs": "Kopieer logs",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "Fout:",
|
||||
"stackTrace": "Stacktracering:"
|
||||
"uploadLogs": "Logs uploaden"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Gerelateerde pakketten",
|
||||
@@ -559,26 +584,18 @@
|
||||
"licensesCount": "${count} licenties"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Bibliotheken",
|
||||
"libraries": "Media",
|
||||
"downloads": "Downloads",
|
||||
"liveTv": "Live TV"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "Live TV",
|
||||
"channels": "Zenders",
|
||||
"guide": "Gids",
|
||||
"noChannels": "Geen zenders beschikbaar",
|
||||
"noDvr": "Geen DVR geconfigureerd op een server",
|
||||
"tuneFailed": "Kan zender niet afstemmen",
|
||||
"loading": "Zenders laden...",
|
||||
"nowPlaying": "Nu aan het afspelen",
|
||||
"noPrograms": "Geen programmagegevens beschikbaar",
|
||||
"channelNumber": "Kanaal ${number}",
|
||||
"live": "LIVE",
|
||||
"hd": "HD",
|
||||
"premiere": "NIEUW",
|
||||
"reloadGuide": "Gids herladen",
|
||||
"allChannels": "Alle zenders",
|
||||
"now": "Nu",
|
||||
"today": "Vandaag",
|
||||
"midnight": "Middernacht",
|
||||
@@ -588,30 +605,41 @@
|
||||
"evening": "Avond",
|
||||
"lateNight": "Late avond",
|
||||
"whatsOn": "Nu op TV",
|
||||
"watchChannel": "Kanaal bekijken"
|
||||
"watchChannel": "Kanaal bekijken",
|
||||
"favorites": "Favorieten",
|
||||
"reorderFavorites": "Favorieten herordenen",
|
||||
"joinSession": "Deelnemen aan lopende sessie",
|
||||
"watchFromStart": "Kijk vanaf het begin (${minutes} min geleden)",
|
||||
"watchLive": "Live kijken",
|
||||
"goToLive": "Ga naar live"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"manage": "Beheren",
|
||||
"tvShows": "Series",
|
||||
"movies": "Films",
|
||||
"noDownloads": "Nog geen downloads",
|
||||
"noDownloadsDescription": "Gedownloade content verschijnt hier voor offline weergave",
|
||||
"downloadNow": "Download",
|
||||
"deleteDownload": "Download verwijderen",
|
||||
"retryDownload": "Download opnieuw proberen",
|
||||
"downloadQueued": "Download in wachtrij",
|
||||
"episodesQueued": "${count} afleveringen in wachtrij voor download",
|
||||
"downloadDeleted": "Download verwijderd",
|
||||
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Het gedownloade bestand wordt van je apparaat verwijderd.",
|
||||
"deletingWithProgress": "Verwijderen van ${title}... (${current} van ${total})",
|
||||
"noDownloadsTree": "Geen downloads",
|
||||
"pauseAll": "Alles pauzeren",
|
||||
"resumeAll": "Alles hervatten",
|
||||
"deleteAll": "Alles verwijderen"
|
||||
"collections": {
|
||||
"title": "Collecties",
|
||||
"collection": "Collectie",
|
||||
"empty": "Collectie is leeg",
|
||||
"unknownLibrarySection": "Kan niet verwijderen: onbekende bibliotheeksectie",
|
||||
"deleteCollection": "Collectie verwijderen",
|
||||
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"deleted": "Collectie verwijderd",
|
||||
"deleteFailed": "Collectie verwijderen mislukt",
|
||||
"deleteFailedWithError": "Collectie verwijderen mislukt: ${error}",
|
||||
"failedToLoadItems": "Collectie-items laden mislukt: ${error}",
|
||||
"selectCollection": "Selecteer collectie",
|
||||
"collectionName": "Collectienaam",
|
||||
"enterCollectionName": "Voer collectienaam in",
|
||||
"addedToCollection": "Toegevoegd aan collectie",
|
||||
"errorAddingToCollection": "Fout bij toevoegen aan collectie",
|
||||
"created": "Collectie gemaakt",
|
||||
"removeFromCollection": "Verwijderen uit collectie",
|
||||
"removeFromCollectionConfirm": "\"${title}\" uit deze collectie verwijderen?",
|
||||
"removedFromCollection": "Uit collectie verwijderd",
|
||||
"removeFromCollectionFailed": "Verwijderen uit collectie mislukt",
|
||||
"removeFromCollectionError": "Fout bij verwijderen uit collectie: ${error}",
|
||||
"searchCollections": "Collecties zoeken..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Afspeellijsten",
|
||||
"playlist": "Afspeellijst",
|
||||
"noPlaylists": "Geen afspeellijsten gevonden",
|
||||
"create": "Afspeellijst maken",
|
||||
"playlistName": "Naam afspeellijst",
|
||||
@@ -629,38 +657,12 @@
|
||||
"itemAdded": "Toegevoegd aan afspeellijst",
|
||||
"itemRemoved": "Verwijderd uit afspeellijst",
|
||||
"selectPlaylist": "Selecteer afspeellijst",
|
||||
"createNewPlaylist": "Nieuwe afspeellijst maken",
|
||||
"errorCreating": "Fout bij maken afspeellijst",
|
||||
"errorDeleting": "Fout bij verwijderen afspeellijst",
|
||||
"errorLoading": "Fout bij laden afspeellijsten",
|
||||
"errorAdding": "Fout bij toevoegen aan afspeellijst",
|
||||
"errorReordering": "Fout bij herschikken van afspeellijstitem",
|
||||
"errorRemoving": "Fout bij verwijderen uit afspeellijst",
|
||||
"playlist": "Afspeellijst"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collecties",
|
||||
"collection": "Collectie",
|
||||
"empty": "Collectie is leeg",
|
||||
"unknownLibrarySection": "Kan niet verwijderen: onbekende bibliotheeksectie",
|
||||
"deleteCollection": "Collectie verwijderen",
|
||||
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"deleted": "Collectie verwijderd",
|
||||
"deleteFailed": "Collectie verwijderen mislukt",
|
||||
"deleteFailedWithError": "Collectie verwijderen mislukt: ${error}",
|
||||
"failedToLoadItems": "Collectie-items laden mislukt: ${error}",
|
||||
"selectCollection": "Selecteer collectie",
|
||||
"createNewCollection": "Nieuwe collectie maken",
|
||||
"collectionName": "Collectienaam",
|
||||
"enterCollectionName": "Voer collectienaam in",
|
||||
"addedToCollection": "Toegevoegd aan collectie",
|
||||
"errorAddingToCollection": "Fout bij toevoegen aan collectie",
|
||||
"created": "Collectie gemaakt",
|
||||
"removeFromCollection": "Verwijderen uit collectie",
|
||||
"removeFromCollectionConfirm": "\"${title}\" uit deze collectie verwijderen?",
|
||||
"removedFromCollection": "Uit collectie verwijderd",
|
||||
"removeFromCollectionFailed": "Verwijderen uit collectie mislukt",
|
||||
"removeFromCollectionError": "Fout bij verwijderen uit collectie: ${error}"
|
||||
"errorRemoving": "Fout bij verwijderen uit afspeellijst"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Samen Kijken",
|
||||
@@ -697,18 +699,76 @@
|
||||
"leave": "Verlaten",
|
||||
"syncing": "Synchroniseren...",
|
||||
"joinWatchSession": "Kijksessie Deelnemen",
|
||||
"enterCodeHint": "Voer 8-teken code in",
|
||||
"enterCodeHint": "Voer 5-teken code in",
|
||||
"pasteFromClipboard": "Plakken van klembord",
|
||||
"pleaseEnterCode": "Voer een sessiecode in",
|
||||
"codeMustBe8Chars": "Sessiecode moet 8 tekens zijn",
|
||||
"codeMustBe5Chars": "Sessiecode moet 5 tekens zijn",
|
||||
"joinInstructions": "Voer de sessiecode in die door de host is gedeeld om deel te nemen aan hun kijksessie.",
|
||||
"failedToCreate": "Sessie maken mislukt",
|
||||
"failedToJoin": "Sessie deelnemen mislukt",
|
||||
"sessionCodeCopied": "Sessiecode gekopieerd naar klembord",
|
||||
"relayUnreachable": "De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet.",
|
||||
"reconnectingToHost": "Opnieuw verbinden met host...",
|
||||
"currentPlayback": "Huidige weergave",
|
||||
"joinCurrentPlayback": "Deelnemen aan huidige weergave",
|
||||
"joinCurrentPlaybackDescription": "Ga terug naar wat de host nu kijkt",
|
||||
"failedToOpenCurrentPlayback": "Huidige weergave kon niet worden geopend",
|
||||
"participantJoined": "${name} is toegetreden",
|
||||
"participantLeft": "${name} heeft de sessie verlaten"
|
||||
"participantLeft": "${name} heeft de sessie verlaten",
|
||||
"participantPaused": "${name} heeft gepauzeerd",
|
||||
"participantResumed": "${name} heeft hervat",
|
||||
"participantSeeked": "${name} heeft gespoeld",
|
||||
"participantBuffering": "${name} is aan het bufferen",
|
||||
"waitingForParticipants": "Wachten tot anderen geladen zijn...",
|
||||
"recentRooms": "Recente kamers",
|
||||
"renameRoom": "Kamer hernoemen",
|
||||
"removeRoom": "Verwijderen"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"manage": "Beheren",
|
||||
"tvShows": "Series",
|
||||
"movies": "Films",
|
||||
"noDownloads": "Nog geen downloads",
|
||||
"noDownloadsDescription": "Gedownloade content verschijnt hier voor offline weergave",
|
||||
"downloadNow": "Download",
|
||||
"deleteDownload": "Download verwijderen",
|
||||
"retryDownload": "Download opnieuw proberen",
|
||||
"downloadQueued": "Download in wachtrij",
|
||||
"serverErrorBitrate": "Serverfout — het bestand overschrijdt mogelijk de bitrate-limiet voor remote streaming",
|
||||
"episodesQueued": "${count} afleveringen in wachtrij voor download",
|
||||
"downloadDeleted": "Download verwijderd",
|
||||
"deleteConfirm": "Weet je zeker dat je \"${title}\" wilt verwijderen? Het gedownloade bestand wordt van je apparaat verwijderd.",
|
||||
"deletingWithProgress": "Verwijderen van ${title}... (${current} van ${total})",
|
||||
"noDownloadsTree": "Geen downloads",
|
||||
"pauseAll": "Alles pauzeren",
|
||||
"resumeAll": "Alles hervatten",
|
||||
"deleteAll": "Alles verwijderen",
|
||||
"selectVersion": "Versie selecteren",
|
||||
"allEpisodes": "Alle afleveringen",
|
||||
"unwatchedOnly": "Alleen onbekeken",
|
||||
"nextNUnwatched": "Volgende ${count} onbekeken",
|
||||
"customAmount": "Aangepast aantal...",
|
||||
"howManyEpisodes": "Hoeveel afleveringen?",
|
||||
"itemsQueued": "${count} items in downloadwachtrij",
|
||||
"keepSynced": "Gesynchroniseerd houden",
|
||||
"downloadOnce": "Eenmalig downloaden",
|
||||
"keepNUnwatched": "${count} onbekeken behouden",
|
||||
"editSyncRule": "Synchronisatieregel bewerken",
|
||||
"removeSyncRule": "Synchronisatieregel verwijderen",
|
||||
"removeSyncRuleConfirm": "Synchronisatie van \"${title}\" stoppen? Gedownloade afleveringen worden behouden.",
|
||||
"syncRuleCreated": "Synchronisatieregel aangemaakt — ${count} onbekeken afleveringen behouden",
|
||||
"syncRuleUpdated": "Synchronisatieregel bijgewerkt",
|
||||
"syncRuleRemoved": "Synchronisatieregel verwijderd",
|
||||
"syncedNewEpisodes": "${count} nieuwe afleveringen gesynchroniseerd voor ${title}",
|
||||
"activeSyncRules": "Synchronisatieregels",
|
||||
"noSyncRules": "Geen synchronisatieregels",
|
||||
"manageSyncRule": "Synchronisatie beheren",
|
||||
"editEpisodeCount": "Aantal afleveringen",
|
||||
"editSyncFilter": "Synchronisatiefilter",
|
||||
"syncAllItems": "Alle items synchroniseren",
|
||||
"syncUnwatchedItems": "Ongekeken items synchroniseren",
|
||||
"syncRuleListCreated": "Synchronisatieregel aangemaakt"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shaders",
|
||||
@@ -716,67 +776,53 @@
|
||||
"nvscalerDescription": "NVIDIA-beeldschaling voor scherpere video",
|
||||
"qualityFast": "Snel",
|
||||
"qualityHQ": "Hoge kwaliteit",
|
||||
"mode": "Modus"
|
||||
"mode": "Modus",
|
||||
"importShader": "Shader importeren",
|
||||
"customShaderDescription": "Aangepaste GLSL-shader",
|
||||
"shaderImported": "Shader geïmporteerd",
|
||||
"shaderImportFailed": "Shader importeren mislukt",
|
||||
"deleteShader": "Shader verwijderen",
|
||||
"deleteShaderConfirm": "\"${name}\" verwijderen?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "Verbinden met apparaat",
|
||||
"hostRemoteSession": "Externe sessie hosten",
|
||||
"controlThisDevice": "Bedien dit apparaat met je telefoon",
|
||||
"remoteControl": "Afstandsbediening",
|
||||
"controlDesktop": "Bedien een desktop-apparaat",
|
||||
"title": "Afstandsbediening",
|
||||
"connectedTo": "Verbonden met ${name}",
|
||||
"session": {
|
||||
"creatingSession": "Externe sessie aanmaken...",
|
||||
"failedToCreate": "Kan externe sessie niet aanmaken:",
|
||||
"noSession": "Geen sessie beschikbaar",
|
||||
"scanQrCode": "Scan QR-code",
|
||||
"orEnterManually": "Of voer handmatig in",
|
||||
"startingServer": "Externe server starten...",
|
||||
"failedToCreate": "Kan externe server niet starten:",
|
||||
"hostAddress": "Hostadres",
|
||||
"sessionId": "Sessie-ID",
|
||||
"pin": "PIN",
|
||||
"connected": "Verbonden",
|
||||
"waitingForConnection": "Wachten op verbinding...",
|
||||
"serverRunning": "Externe server actief",
|
||||
"serverStopped": "Externe server gestopt",
|
||||
"serverRunningDescription": "Mobiele apparaten op je netwerk kunnen deze app ontdekken en ermee verbinden",
|
||||
"serverStoppedDescription": "Start de server om mobiele apparaten te laten verbinden",
|
||||
"usePhoneToControl": "Gebruik je mobiele apparaat om deze app te bedienen",
|
||||
"copiedToClipboard": "${label} gekopieerd naar klembord",
|
||||
"copyToClipboard": "Kopieer naar klembord",
|
||||
"newSession": "Nieuwe sessie",
|
||||
"startServer": "Server starten",
|
||||
"stopServer": "Server stoppen",
|
||||
"minimize": "Minimaliseren"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "Scannen",
|
||||
"manual": "Handmatig",
|
||||
"pairWithDesktop": "Koppelen met desktop",
|
||||
"enterSessionDetails": "Voer de sessiegegevens in die op je desktop-apparaat worden getoond",
|
||||
"discoveryDescription": "Apparaten op je netwerk die Plezy gebruiken met hetzelfde Plex-account verschijnen automatisch",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "Voer 8-tekens sessie-ID in",
|
||||
"pinHint": "Voer 6-cijferige PIN in",
|
||||
"connecting": "Verbinden...",
|
||||
"tips": "Tips",
|
||||
"tipDesktop": "Open Plezy op je desktop en schakel Companion Remote in via instellingen of menu",
|
||||
"tipScan": "Gebruik het tabblad Scannen om snel te koppelen door de QR-code op je desktop te scannen",
|
||||
"tipWifi": "Zorg ervoor dat beide apparaten op hetzelfde WiFi-netwerk zitten",
|
||||
"cameraPermissionRequired": "Cameratoestemming is vereist om QR-codes te scannen.\nGeef cameratoegang in je apparaatinstellingen.",
|
||||
"cameraError": "Kan camera niet starten: ${error}",
|
||||
"scanInstruction": "Richt je camera op de QR-code die op je desktop wordt getoond",
|
||||
"invalidQrCode": "Ongeldig QR-codeformaat",
|
||||
"validationHostRequired": "Voer een hostadres in",
|
||||
"searchingForDevices": "Apparaten zoeken...",
|
||||
"noDevicesFound": "Geen apparaten gevonden op je netwerk",
|
||||
"noDevicesHint": "Zorg ervoor dat Plezy geopend is op je desktop en dat beide apparaten op hetzelfde WiFi-netwerk zitten",
|
||||
"availableDevices": "Beschikbare apparaten",
|
||||
"manualConnection": "Handmatige verbinding",
|
||||
"cryptoInitFailed": "Kan beveiligde verbinding niet initialiseren. Zorg ervoor dat je bent ingelogd bij een Plex-account.",
|
||||
"validationHostRequired": "Voer het hostadres in",
|
||||
"validationHostFormat": "Formaat moet IP:poort zijn (bijv. 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "Voer een sessie-ID in",
|
||||
"validationSessionIdLength": "Sessie-ID moet 8 tekens zijn",
|
||||
"validationPinRequired": "Voer een PIN in",
|
||||
"validationPinLength": "PIN moet 6 cijfers zijn",
|
||||
"connectionTimedOut": "Verbinding verlopen. Controleer de sessie-ID en PIN.",
|
||||
"sessionNotFound": "Kan de sessie niet vinden. Controleer je gegevens.",
|
||||
"failedToConnect": "Verbinden mislukt: ${error}"
|
||||
"connectionTimedOut": "Verbinding verlopen. Zorg ervoor dat beide apparaten op hetzelfde netwerk zitten.",
|
||||
"sessionNotFound": "Apparaat niet gevonden. Zorg ervoor dat Plezy draait op de host.",
|
||||
"authFailed": "Authenticatie mislukt. Zorg ervoor dat beide apparaten hetzelfde Plex-account gebruiken.",
|
||||
"failedToConnect": "Kan niet verbinden: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
"disconnectConfirm": "Wil je de verbinding met de externe sessie verbreken?",
|
||||
"reconnecting": "Opnieuw verbinden...",
|
||||
"attemptOf": "Poging ${current} van 5",
|
||||
"retryNow": "Nu opnieuw proberen",
|
||||
"connectionError": "Verbindingsfout",
|
||||
"notConnected": "Niet verbonden",
|
||||
"tabRemote": "Afstandsbediening",
|
||||
"tabPlay": "Afspelen",
|
||||
"tabMore": "Meer",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "Externe speler gebruiken",
|
||||
"useExternalPlayerDescription": "Open video's in een externe app in plaats van de ingebouwde speler",
|
||||
"selectPlayer": "Speler selecteren",
|
||||
"customPlayers": "Aangepaste spelers",
|
||||
"systemDefault": "Systeemstandaard",
|
||||
"addCustomPlayer": "Aangepaste speler toevoegen",
|
||||
"playerName": "Spelernaam",
|
||||
"playerCommand": "Commando",
|
||||
"playerPackage": "Pakketnaam",
|
||||
"playerUrlScheme": "URL-schema",
|
||||
"customPlayer": "Aangepaste speler",
|
||||
"off": "Uit",
|
||||
"launchFailed": "Kan externe speler niet openen",
|
||||
"appNotInstalled": "${name} is niet geïnstalleerd",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "Samenvatting",
|
||||
"poster": "Poster",
|
||||
"background": "Achtergrond",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Vierkante afbeelding",
|
||||
"selectPoster": "Poster selecteren",
|
||||
"selectBackground": "Achtergrond selecteren",
|
||||
"selectLogo": "Logo selecteren",
|
||||
"selectSquareArt": "Vierkante afbeelding selecteren",
|
||||
"fromUrl": "Vanaf URL",
|
||||
"uploadFile": "Bestand uploaden",
|
||||
"enterImageUrl": "Voer afbeeldings-URL in",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "Automatische ondertitelselectie",
|
||||
"manuallySelected": "Handmatig geselecteerd",
|
||||
"shownWithForeignAudio": "Weergeven bij anderstalig geluid",
|
||||
"alwaysEnabled": "Altijd ingeschakeld"
|
||||
"alwaysEnabled": "Altijd ingeschakeld",
|
||||
"tags": "Tags",
|
||||
"addTag": "Tag toevoegen",
|
||||
"genre": "Genre",
|
||||
"director": "Regisseur",
|
||||
"writer": "Schrijver",
|
||||
"producer": "Producent",
|
||||
"country": "Land",
|
||||
"collection": "Collectie",
|
||||
"label": "Label",
|
||||
"style": "Stijl",
|
||||
"mood": "Stemming"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Servertaken",
|
||||
"failedToLoad": "Taken konden niet worden geladen",
|
||||
"noTasks": "Geen actieve taken"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Verbonden",
|
||||
"connectedAs": "Verbonden als @${username}",
|
||||
"disconnectConfirm": "Trakt-account loskoppelen?",
|
||||
"disconnectConfirmBody": "Plezy stopt met het versturen van afspeelgebeurtenissen naar Trakt. Je kunt op elk moment opnieuw verbinden.",
|
||||
"scrobble": "Realtime scrobbling",
|
||||
"scrobbleDescription": "Verstuur play-, pauze- en stopgebeurtenissen tijdens afspelen naar Trakt.",
|
||||
"watchedSync": "Bekeken-status synchroniseren",
|
||||
"watchedSyncDescription": "Wanneer je items als bekeken markeert in Plezy, worden ze ook op Trakt gemarkeerd.",
|
||||
"deviceCodeTitle": "Activeer Plezy op Trakt",
|
||||
"deviceCodeBody": "Ga naar ${url} en voer deze code in:",
|
||||
"openTraktActivate": "Open Trakt om te activeren",
|
||||
"waitingForAuthorization": "Wacht op autorisatie…",
|
||||
"codeCopied": "Code gekopieerd",
|
||||
"connectFailed": "Kan niet verbinden met Trakt. Probeer opnieuw."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,978 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Plezy"
|
||||
},
|
||||
"auth": {
|
||||
"signInWithPlex": "Zaloguj się przez Plex",
|
||||
"showQRCode": "Pokaż kod QR",
|
||||
"authenticate": "Uwierzytelnienie",
|
||||
"authenticationTimeout": "Upłynął czas uwierzytelniania. Spróbuj ponownie.",
|
||||
"scanQRToSignIn": "Zeskanuj ten kod QR, aby się zalogować",
|
||||
"waitingForAuth": "Oczekiwanie na uwierzytelnienie...\nDokończ logowanie w przeglądarce.",
|
||||
"useBrowser": "Użyj przeglądarki"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Anuluj",
|
||||
"save": "Zapisz",
|
||||
"close": "Zamknij",
|
||||
"clear": "Wyczyść",
|
||||
"reset": "Resetuj",
|
||||
"later": "Później",
|
||||
"submit": "Wyślij",
|
||||
"confirm": "Potwierdź",
|
||||
"retry": "Ponów",
|
||||
"logout": "Wyloguj",
|
||||
"unknown": "Nieznane",
|
||||
"refresh": "Odśwież",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
"delete": "Usuń",
|
||||
"shuffle": "Losowo",
|
||||
"addTo": "Dodaj do...",
|
||||
"createNew": "Utwórz nowy",
|
||||
"connect": "Połącz",
|
||||
"disconnect": "Rozłącz",
|
||||
"play": "Odtwórz",
|
||||
"pause": "Pauza",
|
||||
"resume": "Wznów",
|
||||
"error": "Błąd",
|
||||
"search": "Szukaj",
|
||||
"home": "Strona główna",
|
||||
"back": "Wstecz",
|
||||
"settings": "Ustawienia",
|
||||
"mute": "Wycisz",
|
||||
"ok": "OK",
|
||||
"reconnect": "Połącz ponownie",
|
||||
"exitConfirmTitle": "Zamknąć aplikację?",
|
||||
"exitConfirmMessage": "Czy na pewno chcesz wyjść?",
|
||||
"dontAskAgain": "Nie pytaj ponownie",
|
||||
"exit": "Wyjdź",
|
||||
"viewAll": "Pokaż wszystko",
|
||||
"checkingNetwork": "Sprawdzanie sieci...",
|
||||
"refreshingServers": "Odświeżanie serwerów...",
|
||||
"loadingServers": "Ładowanie serwerów...",
|
||||
"connectingToServers": "Łączenie z serwerami...",
|
||||
"startingOfflineMode": "Uruchamianie trybu offline...",
|
||||
"loading": "Ładowanie..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licencje",
|
||||
"switchProfile": "Zmień profil",
|
||||
"subtitleStyling": "Styl napisów",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logi"
|
||||
},
|
||||
"update": {
|
||||
"available": "Dostępna aktualizacja",
|
||||
"versionAvailable": "Dostępna wersja ${version}",
|
||||
"currentVersion": "Bieżąca: ${version}",
|
||||
"skipVersion": "Pomiń tę wersję",
|
||||
"viewRelease": "Zobacz wydanie",
|
||||
"latestVersion": "Masz najnowszą wersję",
|
||||
"checkFailed": "Nie udało się sprawdzić aktualizacji"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ustawienia",
|
||||
"supportDeveloper": "Wesprzyj Plezy",
|
||||
"supportDeveloperDescription": "Wspomóż rozwój darowizną na Liberapay",
|
||||
"language": "Język",
|
||||
"theme": "Motyw",
|
||||
"appearance": "Wygląd",
|
||||
"videoPlayback": "Odtwarzanie wideo",
|
||||
"videoPlaybackDescription": "Skonfiguruj zachowanie odtwarzania",
|
||||
"advanced": "Zaawansowane",
|
||||
"episodePosterMode": "Styl plakatu odcinka",
|
||||
"seriesPoster": "Plakat serialu",
|
||||
"seasonPoster": "Plakat sezonu",
|
||||
"episodeThumbnail": "Miniatura",
|
||||
"showHeroSectionDescription": "Wyświetl karuzelę wyróżnionych treści na ekranie głównym",
|
||||
"secondsLabel": "Sekundy",
|
||||
"minutesLabel": "Minuty",
|
||||
"secondsShort": "s",
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Wprowadź czas (${min}-${max})",
|
||||
"systemTheme": "Systemowy",
|
||||
"lightTheme": "Jasny",
|
||||
"darkTheme": "Ciemny",
|
||||
"oledTheme": "OLED",
|
||||
"libraryDensity": "Gęstość biblioteki",
|
||||
"compact": "Kompaktowy",
|
||||
"comfortable": "Wygodny",
|
||||
"viewMode": "Tryb widoku",
|
||||
"gridView": "Siatka",
|
||||
"listView": "Lista",
|
||||
"showHeroSection": "Pokaż sekcję wyróżnioną",
|
||||
"useGlobalHubs": "Użyj układu Plex Home",
|
||||
"useGlobalHubsDescription": "Pokaż huby strony głównej jak w oficjalnym kliencie Plex. Gdy wyłączone, pokazuje rekomendacje per biblioteka.",
|
||||
"showServerNameOnHubs": "Pokaż nazwę serwera w hubach",
|
||||
"showServerNameOnHubsDescription": "Zawsze wyświetlaj nazwę serwera w tytułach hubów. Gdy wyłączone, pokazuje tylko dla zduplikowanych nazw.",
|
||||
"alwaysKeepSidebarOpen": "Zawsze utrzymuj panel boczny otwarty",
|
||||
"alwaysKeepSidebarOpenDescription": "Panel boczny jest rozwinięty, a obszar treści dostosowuje się",
|
||||
"showUnwatchedCount": "Pokaż liczbę nieobejrzanych",
|
||||
"showUnwatchedCountDescription": "Wyświetl liczbę nieobejrzanych odcinków w serialach i sezonach",
|
||||
"hideSpoilers": "Ukryj spoilery nieobejrzanych odcinków",
|
||||
"hideSpoilersDescription": "Rozmyj miniatury i ukryj opisy odcinków, których jeszcze nie obejrzałeś",
|
||||
"playerBackend": "Backend odtwarzacza",
|
||||
"exoPlayer": "ExoPlayer (Zalecany)",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Dekodowanie sprzętowe",
|
||||
"hardwareDecodingDescription": "Użyj akceleracji sprzętowej, gdy dostępna",
|
||||
"bufferSize": "Rozmiar bufora",
|
||||
"bufferSizeMB": "${size}MB",
|
||||
"bufferSizeAuto": "Automatyczny (Zalecany)",
|
||||
"bufferSizeWarning": "Twoje urządzenie ma ${heap}MB pamięci. Bufor ${size}MB może powodować problemy z odtwarzaniem.",
|
||||
"subtitleStyling": "Styl napisów",
|
||||
"subtitleStylingDescription": "Dostosuj wygląd napisów",
|
||||
"smallSkipDuration": "Krótki skok",
|
||||
"largeSkipDuration": "Długi skok",
|
||||
"rewindOnResume": "Przewiń przy wznowieniu",
|
||||
"secondsUnit": "${seconds} sekund",
|
||||
"defaultSleepTimer": "Domyślny wyłącznik czasowy",
|
||||
"minutesUnit": "${minutes} minut",
|
||||
"rememberTrackSelections": "Zapamiętaj wybór ścieżek per serial/film",
|
||||
"rememberTrackSelectionsDescription": "Automatycznie zapisuj preferencje języka audio i napisów przy zmianie ścieżek podczas odtwarzania",
|
||||
"clickVideoTogglesPlayback": "Kliknięcie wideo przełącza odtwarzanie/pauzę",
|
||||
"clickVideoTogglesPlaybackDescription": "Gdy włączone, kliknięcie na odtwarzacz wideo odtwarza/wstrzymuje. W przeciwnym razie pokazuje/ukrywa kontrolki.",
|
||||
"videoPlayerControls": "Kontrolki odtwarzacza wideo",
|
||||
"keyboardShortcuts": "Skróty klawiszowe",
|
||||
"keyboardShortcutsDescription": "Dostosuj skróty klawiszowe",
|
||||
"videoPlayerNavigation": "Nawigacja odtwarzacza wideo",
|
||||
"videoPlayerNavigationDescription": "Użyj klawiszy strzałek do nawigacji kontrolkami odtwarzacza",
|
||||
"watchTogetherRelay": "Relay Oglądaj Razem",
|
||||
"watchTogetherRelayDescription": "Ustaw niestandardowy serwer relay dla Oglądaj Razem. Wszyscy uczestnicy muszą korzystać z tego samego serwera.",
|
||||
"watchTogetherRelayHint": "https://moj-relay.przyklad.pl",
|
||||
"crashReporting": "Raportowanie błędów",
|
||||
"crashReportingDescription": "Wysyłaj raporty o błędach, aby pomóc ulepszyć aplikację",
|
||||
"debugLogging": "Logowanie debugowania",
|
||||
"debugLoggingDescription": "Włącz szczegółowe logowanie do rozwiązywania problemów",
|
||||
"viewLogs": "Pokaż logi",
|
||||
"viewLogsDescription": "Pokaż logi aplikacji",
|
||||
"clearCache": "Wyczyść pamięć podręczną",
|
||||
"clearCacheDescription": "Spowoduje to wyczyszczenie wszystkich zapisanych obrazów i danych. Po wyczyszczeniu aplikacja może ładować treści wolniej.",
|
||||
"clearCacheSuccess": "Pamięć podręczna wyczyszczona",
|
||||
"resetSettings": "Zresetuj ustawienia",
|
||||
"resetSettingsDescription": "Wszystkie ustawienia zostaną przywrócone do wartości domyślnych. Tej operacji nie można cofnąć.",
|
||||
"resetSettingsSuccess": "Ustawienia zresetowane pomyślnie",
|
||||
"shortcutsReset": "Skróty przywrócone do domyślnych",
|
||||
"about": "O aplikacji",
|
||||
"aboutDescription": "Informacje o aplikacji i licencje",
|
||||
"updates": "Aktualizacje",
|
||||
"updateAvailable": "Dostępna aktualizacja",
|
||||
"checkForUpdates": "Sprawdź aktualizacje",
|
||||
"validationErrorEnterNumber": "Wprowadź prawidłową liczbę",
|
||||
"validationErrorDuration": "Czas musi być między ${min} a ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Skrót jest już przypisany do ${action}",
|
||||
"shortcutUpdated": "Skrót zaktualizowany dla ${action}",
|
||||
"autoSkip": "Automatyczne pomijanie",
|
||||
"autoSkipIntro": "Automatyczne pomijanie intro",
|
||||
"autoSkipIntroDescription": "Automatycznie pomijaj znaczniki intro po kilku sekundach",
|
||||
"autoSkipCredits": "Automatyczne pomijanie napisów końcowych",
|
||||
"autoSkipCreditsDescription": "Automatycznie pomijaj napisy końcowe i odtwórz następny odcinek",
|
||||
"autoSkipDelay": "Opóźnienie automatycznego pomijania",
|
||||
"autoSkipDelayDescription": "Czekaj ${seconds} sekund przed automatycznym pominięciem",
|
||||
"introPattern": "Wzorzec znacznika intro",
|
||||
"introPatternDescription": "Wyrażenie regularne do rozpoznawania znaczników intro w tytułach rozdziałów",
|
||||
"creditsPattern": "Wzorzec znacznika napisów końcowych",
|
||||
"creditsPatternDescription": "Wyrażenie regularne do rozpoznawania znaczników napisów końcowych w tytułach rozdziałów",
|
||||
"invalidRegex": "Nieprawidłowe wyrażenie regularne",
|
||||
"downloads": "Pobrania",
|
||||
"downloadLocationDescription": "Wybierz miejsce przechowywania pobranych treści",
|
||||
"downloadLocationDefault": "Domyślne (Pamięć aplikacji)",
|
||||
"downloadLocationCustom": "Niestandardowa lokalizacja",
|
||||
"selectFolder": "Wybierz folder",
|
||||
"resetToDefault": "Przywróć domyślne",
|
||||
"currentPath": "Bieżąca: ${path}",
|
||||
"downloadLocationChanged": "Lokalizacja pobierania zmieniona",
|
||||
"downloadLocationReset": "Lokalizacja pobierania przywrócona do domyślnej",
|
||||
"downloadLocationInvalid": "Wybrany folder nie jest zapisywalny",
|
||||
"downloadLocationSelectError": "Nie udało się wybrać folderu",
|
||||
"downloadOnWifiOnly": "Pobieraj tylko przez WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Blokuj pobieranie na danych komórkowych",
|
||||
"autoRemoveWatchedDownloads": "Automatycznie usuwaj obejrzane pobrania",
|
||||
"autoRemoveWatchedDownloadsDescription": "Automatycznie usuwaj pobrane odcinki i filmy po oznaczeniu jako obejrzane",
|
||||
"cellularDownloadBlocked": "Pobieranie na danych komórkowych jest wyłączone. Połącz się z WiFi lub zmień ustawienie.",
|
||||
"maxVolume": "Maksymalna głośność",
|
||||
"maxVolumeDescription": "Pozwól na wzmocnienie głośności powyżej 100% dla cichych multimediów",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Pokaż, co oglądasz na Discordzie",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Synchronizuj historię oglądania z Trakt",
|
||||
"companionRemoteServer": "Serwer zdalnego sterowania",
|
||||
"companionRemoteServerDescription": "Pozwól urządzeniom mobilnym w sieci sterować tą aplikacją",
|
||||
"autoPip": "Automatyczny obraz w obrazie",
|
||||
"autoPipDescription": "Automatycznie przejdź do trybu obraz w obrazie przy wyjściu z aplikacji podczas odtwarzania",
|
||||
"matchContentFrameRate": "Dopasuj częstotliwość klatek do treści",
|
||||
"matchContentFrameRateDescription": "Dostosuj częstotliwość odświeżania ekranu do treści wideo, zmniejszając drgania i oszczędzając baterię",
|
||||
"matchRefreshRate": "Dopasuj częstotliwość odświeżania",
|
||||
"matchRefreshRateDescription": "Przełącz częstotliwość odświeżania ekranu, aby dopasować ją do treści wideo na pełnym ekranie",
|
||||
"matchDynamicRange": "Dopasuj zakres dynamiki",
|
||||
"matchDynamicRangeDescription": "Automatycznie włącz HDR dla treści HDR i przywróć SDR po wyjściu z odtwarzacza",
|
||||
"displaySwitchDelay": "Opóźnienie przełączania ekranu",
|
||||
"tunneledPlayback": "Tunelowane odtwarzanie",
|
||||
"tunneledPlaybackDescription": "Użyj sprzętowo przyspieszonego tunelowania wideo. Wyłącz, jeśli widzisz czarny ekran z dźwiękiem przy treściach HDR",
|
||||
"requireProfileSelectionOnOpen": "Pytaj o profil przy otwarciu aplikacji",
|
||||
"requireProfileSelectionOnOpenDescription": "Pokaż wybór profilu za każdym razem, gdy aplikacja jest otwierana",
|
||||
"confirmExitOnBack": "Potwierdź przed wyjściem",
|
||||
"confirmExitOnBackDescription": "Pokaż dialog potwierdzenia przy naciśnięciu wstecz, aby wyjść z aplikacji",
|
||||
"forceTvMode": "Wymuś tryb TV",
|
||||
"forceTvModeDescription": "Użyj układu TV niezależnie od automatycznego wykrywania. Przydatne na urządzeniach Android TV, które nie zgłaszają funkcji leanback. Po zmianie aplikacja zostanie zrestartowana.",
|
||||
"autoHidePerformanceOverlay": "Automatycznie ukrywaj nakładkę wydajności",
|
||||
"autoHidePerformanceOverlayDescription": "Wygaszaj nakładkę wydajności wraz z kontrolkami odtwarzania",
|
||||
"showNavBarLabels": "Pokaż etykiety paska nawigacji",
|
||||
"showNavBarLabelsDescription": "Wyświetl tekstowe etykiety pod ikonami paska nawigacji",
|
||||
"liveTvDefaultFavorites": "Domyślnie ulubione kanały",
|
||||
"liveTvDefaultFavoritesDescription": "Pokaż tylko ulubione kanały po otwarciu telewizji na żywo",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Szukaj filmów, seriali, muzyki...",
|
||||
"tryDifferentTerm": "Spróbuj innego wyszukiwania",
|
||||
"searchYourMedia": "Przeszukaj swoje media",
|
||||
"enterTitleActorOrKeyword": "Wprowadź tytuł, aktora lub słowo kluczowe"
|
||||
},
|
||||
"hotkeys": {
|
||||
"setShortcutFor": "Ustaw skrót dla ${actionName}",
|
||||
"clearShortcut": "Wyczyść skrót",
|
||||
"actions": {
|
||||
"playPause": "Odtwórz/Pauza",
|
||||
"volumeUp": "Głośniej",
|
||||
"volumeDown": "Ciszej",
|
||||
"seekForward": "Przewiń do przodu (${seconds}s)",
|
||||
"seekBackward": "Przewiń do tyłu (${seconds}s)",
|
||||
"fullscreenToggle": "Pełny ekran",
|
||||
"muteToggle": "Wyciszenie",
|
||||
"subtitleToggle": "Napisy",
|
||||
"audioTrackNext": "Następna ścieżka audio",
|
||||
"subtitleTrackNext": "Następna ścieżka napisów",
|
||||
"chapterNext": "Następny rozdział",
|
||||
"chapterPrevious": "Poprzedni rozdział",
|
||||
"speedIncrease": "Zwiększ prędkość",
|
||||
"speedDecrease": "Zmniejsz prędkość",
|
||||
"speedReset": "Zresetuj prędkość",
|
||||
"subSeekNext": "Przewiń do następnego napisu",
|
||||
"subSeekPrev": "Przewiń do poprzedniego napisu",
|
||||
"shaderToggle": "Przełącz shadery",
|
||||
"skipMarker": "Pomiń intro/napisy końcowe"
|
||||
}
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Informacje o pliku",
|
||||
"video": "Wideo",
|
||||
"audio": "Audio",
|
||||
"file": "Plik",
|
||||
"advanced": "Zaawansowane",
|
||||
"codec": "Kodek",
|
||||
"resolution": "Rozdzielczość",
|
||||
"bitrate": "Bitrate",
|
||||
"frameRate": "Klatki na sekundę",
|
||||
"aspectRatio": "Proporcje",
|
||||
"profile": "Profil",
|
||||
"bitDepth": "Głębia bitowa",
|
||||
"colorSpace": "Przestrzeń kolorów",
|
||||
"colorRange": "Zakres kolorów",
|
||||
"colorPrimaries": "Kolory podstawowe",
|
||||
"chromaSubsampling": "Subsampling chrominancji",
|
||||
"channels": "Kanały",
|
||||
"subtitles": "Napisy",
|
||||
"overallBitrate": "Całkowity bitrate",
|
||||
"path": "Ścieżka",
|
||||
"size": "Rozmiar",
|
||||
"container": "Kontener",
|
||||
"duration": "Czas trwania",
|
||||
"optimizedForStreaming": "Zoptymalizowane do strumieniowania",
|
||||
"has64bitOffsets": "64-bitowe offsety"
|
||||
},
|
||||
"mediaMenu": {
|
||||
"markAsWatched": "Oznacz jako obejrzane",
|
||||
"markAsUnwatched": "Oznacz jako nieobejrzane",
|
||||
"removeFromContinueWatching": "Usuń z kontynuowania oglądania",
|
||||
"goToSeries": "Przejdź do serialu",
|
||||
"goToSeason": "Przejdź do sezonu",
|
||||
"shufflePlay": "Odtwarzanie losowe",
|
||||
"fileInfo": "Informacje o pliku",
|
||||
"deleteFromServer": "Usuń z serwera",
|
||||
"confirmDelete": "To trwale usunie te multimedia i ich pliki z twojego serwera. Tej operacji nie można cofnąć.",
|
||||
"deleteMultipleWarning": "Obejmuje to wszystkie odcinki i ich pliki.",
|
||||
"mediaDeletedSuccessfully": "Element multimedialny usunięty pomyślnie",
|
||||
"mediaFailedToDelete": "Nie udało się usunąć elementu multimedialnego",
|
||||
"rate": "Oceń",
|
||||
"playFromBeginning": "Odtwórz od początku",
|
||||
"playVersion": "Odtwórz wersję..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, film",
|
||||
"mediaCardShow": "${title}, serial TV",
|
||||
"mediaCardEpisode": "${title}, ${episodeInfo}",
|
||||
"mediaCardSeason": "${title}, ${seasonInfo}",
|
||||
"mediaCardWatched": "obejrzane",
|
||||
"mediaCardPartiallyWatched": "${percent} procent obejrzane",
|
||||
"mediaCardUnwatched": "nieobejrzane",
|
||||
"tapToPlay": "Dotknij, aby odtworzyć"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Odtwarzanie losowe",
|
||||
"playTrailer": "Odtwórz zwiastun",
|
||||
"markAsWatched": "Oznacz jako obejrzane",
|
||||
"markAsUnwatched": "Oznacz jako nieobejrzane"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Audio",
|
||||
"subtitlesLabel": "Napisy",
|
||||
"resetToZero": "Zresetuj do 0ms",
|
||||
"addTime": "+${amount}${unit}",
|
||||
"minusTime": "-${amount}${unit}",
|
||||
"playsLater": "${label} odtwarza później",
|
||||
"playsEarlier": "${label} odtwarza wcześniej",
|
||||
"noOffset": "Bez przesunięcia",
|
||||
"letterbox": "Letterbox",
|
||||
"fillScreen": "Wypełnij ekran",
|
||||
"stretch": "Rozciągnij",
|
||||
"lockRotation": "Zablokuj obrót",
|
||||
"unlockRotation": "Odblokuj obrót",
|
||||
"timerActive": "Wyłącznik aktywny",
|
||||
"playbackWillPauseIn": "Odtwarzanie zatrzyma się za ${duration}",
|
||||
"stillWatching": "Nadal oglądasz?",
|
||||
"pausingIn": "Pauza za ${seconds}s",
|
||||
"continueWatching": "Kontynuuj",
|
||||
"autoPlayNext": "Automatycznie odtwórz następny",
|
||||
"playNext": "Odtwórz następny",
|
||||
"playButton": "Odtwórz",
|
||||
"pauseButton": "Pauza",
|
||||
"seekBackwardButton": "Przewiń do tyłu o ${seconds} sekund",
|
||||
"seekForwardButton": "Przewiń do przodu o ${seconds} sekund",
|
||||
"previousButton": "Poprzedni odcinek",
|
||||
"nextButton": "Następny odcinek",
|
||||
"previousChapterButton": "Poprzedni rozdział",
|
||||
"nextChapterButton": "Następny rozdział",
|
||||
"muteButton": "Wycisz",
|
||||
"unmuteButton": "Wyłącz wyciszenie",
|
||||
"settingsButton": "Ustawienia wideo",
|
||||
"tracksButton": "Audio i napisy",
|
||||
"chaptersButton": "Rozdziały",
|
||||
"versionsButton": "Wersje wideo",
|
||||
"pipButton": "Tryb obraz w obrazie",
|
||||
"aspectRatioButton": "Proporcje",
|
||||
"ambientLighting": "Oświetlenie otoczenia",
|
||||
"fullscreenButton": "Wejdź w pełny ekran",
|
||||
"exitFullscreenButton": "Wyjdź z pełnego ekranu",
|
||||
"alwaysOnTopButton": "Zawsze na wierzchu",
|
||||
"rotationLockButton": "Blokada obrotu",
|
||||
"lockScreen": "Zablokuj ekran",
|
||||
"screenLockButton": "Blokada ekranu",
|
||||
"longPressToUnlock": "Przytrzymaj, aby odblokować",
|
||||
"timelineSlider": "Oś czasu wideo",
|
||||
"volumeSlider": "Poziom głośności",
|
||||
"endsAt": "Kończy się o ${time}",
|
||||
"pipActive": "Odtwarzanie w trybie obraz w obrazie",
|
||||
"pipFailed": "Nie udało się uruchomić trybu obraz w obrazie",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Wymaga Androida 8.0 lub nowszego",
|
||||
"iosVersion": "Wymaga iOS 15.0 lub nowszego",
|
||||
"permissionDisabled": "Uprawnienie obraz w obrazie jest wyłączone. Włącz w Ustawienia > Aplikacje > Plezy > Obraz w obrazie",
|
||||
"notSupported": "Urządzenie nie obsługuje trybu obraz w obrazie",
|
||||
"voSwitchFailed": "Nie udało się przełączyć wyjścia wideo dla trybu obraz w obrazie",
|
||||
"failed": "Nie udało się uruchomić trybu obraz w obrazie",
|
||||
"unknown": "Wystąpił błąd: ${error}"
|
||||
},
|
||||
"chapters": "Rozdziały",
|
||||
"noChaptersAvailable": "Brak dostępnych rozdziałów",
|
||||
"queue": "Kolejka",
|
||||
"noQueueItems": "Brak elementów w kolejce",
|
||||
"searchSubtitles": "Szukaj napisów",
|
||||
"language": "Język",
|
||||
"noSubtitlesFound": "Nie znaleziono napisów",
|
||||
"subtitleDownloaded": "Napisy pobrane",
|
||||
"subtitleDownloadFailed": "Nie udało się pobrać napisów",
|
||||
"searchLanguages": "Szukaj języków..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrator",
|
||||
"restricted": "Ograniczony",
|
||||
"protected": "Chroniony",
|
||||
"current": "BIEŻĄCY"
|
||||
},
|
||||
"messages": {
|
||||
"markedAsWatched": "Oznaczono jako obejrzane",
|
||||
"markedAsUnwatched": "Oznaczono jako nieobejrzane",
|
||||
"markedAsWatchedOffline": "Oznaczono jako obejrzane (zsynchronizuje się po połączeniu)",
|
||||
"markedAsUnwatchedOffline": "Oznaczono jako nieobejrzane (zsynchronizuje się po połączeniu)",
|
||||
"autoRemovedWatchedDownload": "Automatycznie usunięto: ${title}",
|
||||
"removedFromContinueWatching": "Usunięto z kontynuowania oglądania",
|
||||
"errorLoading": "Błąd: ${error}",
|
||||
"fileInfoNotAvailable": "Informacje o pliku niedostępne",
|
||||
"errorLoadingFileInfo": "Błąd ładowania informacji o pliku: ${error}",
|
||||
"errorLoadingSeries": "Błąd ładowania serialu",
|
||||
"errorLoadingSeason": "Błąd ładowania sezonu",
|
||||
"musicNotSupported": "Odtwarzanie muzyki nie jest jeszcze obsługiwane",
|
||||
"logsCleared": "Logi wyczyszczone",
|
||||
"logsCopied": "Logi skopiowane do schowka",
|
||||
"noLogsAvailable": "Brak dostępnych logów",
|
||||
"libraryScanning": "Skanowanie \"${title}\"...",
|
||||
"libraryScanStarted": "Rozpoczęto skanowanie biblioteki \"${title}\"",
|
||||
"libraryScanFailed": "Nie udało się zeskanować biblioteki: ${error}",
|
||||
"metadataRefreshing": "Odświeżanie metadanych \"${title}\"...",
|
||||
"metadataRefreshStarted": "Rozpoczęto odświeżanie metadanych \"${title}\"",
|
||||
"metadataRefreshFailed": "Nie udało się odświeżyć metadanych: ${error}",
|
||||
"logoutConfirm": "Czy na pewno chcesz się wylogować?",
|
||||
"noSeasonsFound": "Nie znaleziono sezonów",
|
||||
"noEpisodesFound": "Nie znaleziono odcinków w pierwszym sezonie",
|
||||
"noEpisodesFoundGeneral": "Nie znaleziono odcinków",
|
||||
"noResultsFound": "Nie znaleziono wyników",
|
||||
"sleepTimerSet": "Wyłącznik czasowy ustawiony na ${label}",
|
||||
"noItemsAvailable": "Brak dostępnych elementów",
|
||||
"failedToCreatePlayQueueNoItems": "Nie udało się utworzyć kolejki odtwarzania — brak elementów",
|
||||
"failedPlayback": "Nie udało się ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Przełączanie na kompatybilny odtwarzacz...",
|
||||
"serverLimitTitle": "Odtwarzanie nie powiodło się",
|
||||
"serverLimitBody": "Serwer zwrócił błąd (HTTP 500). Zwykle oznacza to, że właściciel serwera ustawił limit przepustowości lub transkodowania, który odrzuca Twoją sesję. Z poziomu klienta nie da się nic zrobić — właściciel serwera musi dostosować swoje ustawienia.",
|
||||
"logsUploaded": "Logi przesłane",
|
||||
"logsUploadFailed": "Nie udało się przesłać logów",
|
||||
"logId": "ID logu"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"text": "Tekst",
|
||||
"border": "Obramowanie",
|
||||
"background": "Tło",
|
||||
"fontSize": "Rozmiar czcionki",
|
||||
"textColor": "Kolor tekstu",
|
||||
"borderSize": "Rozmiar obramowania",
|
||||
"borderColor": "Kolor obramowania",
|
||||
"backgroundOpacity": "Przezroczystość tła",
|
||||
"backgroundColor": "Kolor tła",
|
||||
"position": "Pozycja",
|
||||
"assOverride": "Nadpisywanie ASS",
|
||||
"bold": "Pogrubienie",
|
||||
"italic": "Kursywa"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "mpv.conf",
|
||||
"description": "Zaawansowane ustawienia odtwarzacza wideo",
|
||||
"presets": "Presety",
|
||||
"noPresets": "Brak zapisanych presetów",
|
||||
"saveAsPreset": "Zapisz jako preset...",
|
||||
"presetName": "Nazwa presetu",
|
||||
"presetNameHint": "Wprowadź nazwę dla tego presetu",
|
||||
"loadPreset": "Załaduj",
|
||||
"deletePreset": "Usuń",
|
||||
"presetSaved": "Preset zapisany",
|
||||
"presetLoaded": "Preset załadowany",
|
||||
"presetDeleted": "Preset usunięty",
|
||||
"confirmDeletePreset": "Czy na pewno chcesz usunąć ten preset?",
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Potwierdź działanie"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Odkryj",
|
||||
"switchProfile": "Zmień profil",
|
||||
"noContentAvailable": "Brak dostępnych treści",
|
||||
"addMediaToLibraries": "Dodaj multimedia do swoich bibliotek",
|
||||
"continueWatching": "Kontynuuj oglądanie",
|
||||
"playEpisode": "S${season}E${episode}",
|
||||
"overview": "Opis",
|
||||
"cast": "Obsada",
|
||||
"extras": "Zwiastuny i dodatki",
|
||||
"studio": "Studio",
|
||||
"rating": "Ocena",
|
||||
"movie": "Film",
|
||||
"tvShow": "Serial TV",
|
||||
"minutesLeft": "${minutes} min pozostało"
|
||||
},
|
||||
"errors": {
|
||||
"searchFailed": "Wyszukiwanie nie powiodło się: ${error}",
|
||||
"connectionTimeout": "Limit czasu połączenia przy ładowaniu ${context}",
|
||||
"connectionFailed": "Nie można połączyć z serwerem Plex",
|
||||
"failedToLoad": "Nie udało się załadować ${context}: ${error}",
|
||||
"noClientAvailable": "Brak dostępnego klienta",
|
||||
"authenticationFailed": "Uwierzytelnienie nie powiodło się: ${error}",
|
||||
"couldNotLaunchUrl": "Nie można otworzyć URL uwierzytelniania",
|
||||
"pleaseEnterToken": "Wprowadź token",
|
||||
"invalidToken": "Nieprawidłowy token",
|
||||
"failedToVerifyToken": "Nie udało się zweryfikować tokena: ${error}",
|
||||
"failedToSwitchProfile": "Nie udało się przełączyć na ${displayName}"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Biblioteki",
|
||||
"scanLibraryFiles": "Skanuj pliki biblioteki",
|
||||
"scanLibrary": "Skanuj bibliotekę",
|
||||
"analyze": "Analizuj",
|
||||
"analyzeLibrary": "Analizuj bibliotekę",
|
||||
"refreshMetadata": "Odśwież metadane",
|
||||
"emptyTrash": "Opróżnij kosz",
|
||||
"emptyingTrash": "Opróżnianie kosza dla \"${title}\"...",
|
||||
"trashEmptied": "Kosz opróżniony dla \"${title}\"",
|
||||
"failedToEmptyTrash": "Nie udało się opróżnić kosza: ${error}",
|
||||
"analyzing": "Analizowanie \"${title}\"...",
|
||||
"analysisStarted": "Analiza rozpoczęta dla \"${title}\"",
|
||||
"failedToAnalyze": "Nie udało się przeanalizować biblioteki: ${error}",
|
||||
"noLibrariesFound": "Nie znaleziono bibliotek",
|
||||
"thisLibraryIsEmpty": "Ta biblioteka jest pusta",
|
||||
"all": "Wszystkie",
|
||||
"clearAll": "Wyczyść wszystko",
|
||||
"scanLibraryConfirm": "Czy na pewno chcesz zeskanować \"${title}\"?",
|
||||
"analyzeLibraryConfirm": "Czy na pewno chcesz przeanalizować \"${title}\"?",
|
||||
"refreshMetadataConfirm": "Czy na pewno chcesz odświeżyć metadane dla \"${title}\"?",
|
||||
"emptyTrashConfirm": "Czy na pewno chcesz opróżnić kosz dla \"${title}\"?",
|
||||
"manageLibraries": "Zarządzaj bibliotekami",
|
||||
"sort": "Sortuj",
|
||||
"sortBy": "Sortuj wg",
|
||||
"filters": "Filtry",
|
||||
"confirmActionMessage": "Czy na pewno chcesz wykonać tę operację?",
|
||||
"showLibrary": "Pokaż bibliotekę",
|
||||
"hideLibrary": "Ukryj bibliotekę",
|
||||
"libraryOptions": "Opcje biblioteki",
|
||||
"content": "zawartość biblioteki",
|
||||
"selectLibrary": "Wybierz bibliotekę",
|
||||
"filtersWithCount": "Filtry (${count})",
|
||||
"noRecommendations": "Brak dostępnych rekomendacji",
|
||||
"noCollections": "Brak kolekcji w tej bibliotece",
|
||||
"noFoldersFound": "Nie znaleziono folderów",
|
||||
"folders": "foldery",
|
||||
"tabs": {
|
||||
"recommended": "Rekomendowane",
|
||||
"browse": "Przeglądaj",
|
||||
"collections": "Kolekcje",
|
||||
"playlists": "Playlisty"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Grupowanie",
|
||||
"all": "Wszystkie",
|
||||
"movies": "Filmy",
|
||||
"shows": "Seriale TV",
|
||||
"seasons": "Sezony",
|
||||
"episodes": "Odcinki",
|
||||
"folders": "Foldery"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "O aplikacji",
|
||||
"openSourceLicenses": "Licencje open source",
|
||||
"versionLabel": "Wersja ${version}",
|
||||
"appDescription": "Piękny klient Plex na Flutter",
|
||||
"viewLicensesDescription": "Zobacz licencje bibliotek zewnętrznych"
|
||||
},
|
||||
"serverSelection": {
|
||||
"allServerConnectionsFailed": "Nie udało się połączyć z żadnym serwerem. Sprawdź sieć i spróbuj ponownie.",
|
||||
"noServersFoundForAccount": "Nie znaleziono serwerów dla ${username} (${email})",
|
||||
"failedToLoadServers": "Nie udało się załadować serwerów: ${error}"
|
||||
},
|
||||
"hubDetail": {
|
||||
"title": "Tytuł",
|
||||
"releaseYear": "Rok premiery",
|
||||
"dateAdded": "Data dodania",
|
||||
"rating": "Ocena",
|
||||
"noItemsFound": "Nie znaleziono elementów"
|
||||
},
|
||||
"logs": {
|
||||
"clearLogs": "Wyczyść logi",
|
||||
"copyLogs": "Kopiuj logi",
|
||||
"uploadLogs": "Prześlij logi"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Powiązane pakiety",
|
||||
"license": "Licencja",
|
||||
"licenseNumber": "Licencja ${number}",
|
||||
"licensesCount": "${count} licencji"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Biblioteki",
|
||||
"downloads": "Pobrania",
|
||||
"liveTv": "TV na żywo"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "TV na żywo",
|
||||
"guide": "Przewodnik",
|
||||
"noChannels": "Brak dostępnych kanałów",
|
||||
"noDvr": "Brak skonfigurowanego DVR na żadnym serwerze",
|
||||
"noPrograms": "Brak danych o programach",
|
||||
"live": "NA ŻYWO",
|
||||
"reloadGuide": "Odśwież przewodnik",
|
||||
"now": "Teraz",
|
||||
"today": "Dzisiaj",
|
||||
"midnight": "Północ",
|
||||
"overnight": "Nocą",
|
||||
"morning": "Rano",
|
||||
"daytime": "W ciągu dnia",
|
||||
"evening": "Wieczorem",
|
||||
"lateNight": "Późna noc",
|
||||
"whatsOn": "Co leci",
|
||||
"watchChannel": "Oglądaj kanał",
|
||||
"favorites": "Ulubione",
|
||||
"reorderFavorites": "Zmień kolejność ulubionych",
|
||||
"joinSession": "Dołącz do trwającej sesji",
|
||||
"watchFromStart": "Oglądaj od początku (${minutes} min temu)",
|
||||
"watchLive": "Oglądaj na żywo",
|
||||
"goToLive": "Przejdź do na żywo"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Kolekcje",
|
||||
"collection": "Kolekcja",
|
||||
"empty": "Kolekcja jest pusta",
|
||||
"unknownLibrarySection": "Nie można usunąć: Nieznana sekcja biblioteki",
|
||||
"deleteCollection": "Usuń kolekcję",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć \"${title}\"? Tej operacji nie można cofnąć.",
|
||||
"deleted": "Kolekcja usunięta",
|
||||
"deleteFailed": "Nie udało się usunąć kolekcji",
|
||||
"deleteFailedWithError": "Nie udało się usunąć kolekcji: ${error}",
|
||||
"failedToLoadItems": "Nie udało się załadować elementów kolekcji: ${error}",
|
||||
"selectCollection": "Wybierz kolekcję",
|
||||
"collectionName": "Nazwa kolekcji",
|
||||
"enterCollectionName": "Wprowadź nazwę kolekcji",
|
||||
"addedToCollection": "Dodano do kolekcji",
|
||||
"errorAddingToCollection": "Nie udało się dodać do kolekcji",
|
||||
"created": "Kolekcja utworzona",
|
||||
"removeFromCollection": "Usuń z kolekcji",
|
||||
"removeFromCollectionConfirm": "Usunąć \"${title}\" z tej kolekcji?",
|
||||
"removedFromCollection": "Usunięto z kolekcji",
|
||||
"removeFromCollectionFailed": "Nie udało się usunąć z kolekcji",
|
||||
"removeFromCollectionError": "Błąd usuwania z kolekcji: ${error}",
|
||||
"searchCollections": "Szukaj kolekcji..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Playlisty",
|
||||
"playlist": "Playlista",
|
||||
"noPlaylists": "Nie znaleziono playlist",
|
||||
"create": "Utwórz playlistę",
|
||||
"playlistName": "Nazwa playlisty",
|
||||
"enterPlaylistName": "Wprowadź nazwę playlisty",
|
||||
"delete": "Usuń playlistę",
|
||||
"removeItem": "Usuń z playlisty",
|
||||
"smartPlaylist": "Inteligentna playlista",
|
||||
"itemCount": "${count} elementów",
|
||||
"oneItem": "1 element",
|
||||
"emptyPlaylist": "Ta playlista jest pusta",
|
||||
"deleteConfirm": "Usunąć playlistę?",
|
||||
"deleteMessage": "Czy na pewno chcesz usunąć \"${name}\"?",
|
||||
"created": "Playlista utworzona",
|
||||
"deleted": "Playlista usunięta",
|
||||
"itemAdded": "Dodano do playlisty",
|
||||
"itemRemoved": "Usunięto z playlisty",
|
||||
"selectPlaylist": "Wybierz playlistę",
|
||||
"errorCreating": "Nie udało się utworzyć playlisty",
|
||||
"errorDeleting": "Nie udało się usunąć playlisty",
|
||||
"errorLoading": "Nie udało się załadować playlist",
|
||||
"errorAdding": "Nie udało się dodać do playlisty",
|
||||
"errorReordering": "Nie udało się zmienić kolejności elementu playlisty",
|
||||
"errorRemoving": "Nie udało się usunąć z playlisty"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Oglądaj razem",
|
||||
"description": "Oglądaj treści zsynchronizowane z przyjaciółmi i rodziną",
|
||||
"createSession": "Utwórz sesję",
|
||||
"creating": "Tworzenie...",
|
||||
"joinSession": "Dołącz do sesji",
|
||||
"joining": "Dołączanie...",
|
||||
"controlMode": "Tryb kontroli",
|
||||
"controlModeQuestion": "Kto może kontrolować odtwarzanie?",
|
||||
"hostOnly": "Tylko host",
|
||||
"anyone": "Każdy",
|
||||
"hostingSession": "Hostowanie sesji",
|
||||
"inSession": "W sesji",
|
||||
"sessionCode": "Kod sesji",
|
||||
"hostControlsPlayback": "Host kontroluje odtwarzanie",
|
||||
"anyoneCanControl": "Każdy może kontrolować odtwarzanie",
|
||||
"hostControls": "Kontrola hosta",
|
||||
"anyoneControls": "Kontrola każdego",
|
||||
"participants": "Uczestnicy",
|
||||
"host": "Host",
|
||||
"hostBadge": "HOST",
|
||||
"youAreHost": "Jesteś hostem",
|
||||
"watchingWithOthers": "Oglądasz z innymi",
|
||||
"endSession": "Zakończ sesję",
|
||||
"leaveSession": "Opuść sesję",
|
||||
"endSessionQuestion": "Zakończyć sesję?",
|
||||
"leaveSessionQuestion": "Opuścić sesję?",
|
||||
"endSessionConfirm": "To zakończy sesję dla wszystkich uczestników.",
|
||||
"leaveSessionConfirm": "Zostaniesz usunięty z sesji.",
|
||||
"endSessionConfirmOverlay": "To zakończy sesję oglądania dla wszystkich uczestników.",
|
||||
"leaveSessionConfirmOverlay": "Zostaniesz odłączony od sesji oglądania.",
|
||||
"end": "Zakończ",
|
||||
"leave": "Opuść",
|
||||
"syncing": "Synchronizacja...",
|
||||
"joinWatchSession": "Dołącz do sesji oglądania",
|
||||
"enterCodeHint": "Wprowadź 5-znakowy kod",
|
||||
"pasteFromClipboard": "Wklej ze schowka",
|
||||
"pleaseEnterCode": "Wprowadź kod sesji",
|
||||
"codeMustBe5Chars": "Kod sesji musi mieć 5 znaków",
|
||||
"joinInstructions": "Wprowadź kod sesji udostępniony przez hosta, aby dołączyć do sesji oglądania.",
|
||||
"failedToCreate": "Nie udało się utworzyć sesji",
|
||||
"failedToJoin": "Nie udało się dołączyć do sesji",
|
||||
"sessionCodeCopied": "Kod sesji skopiowany do schowka",
|
||||
"relayUnreachable": "Serwer przekaźnika jest nieosiągalny. Może to być spowodowane blokadą przez twojego dostawcę internetu. Możesz spróbować, ale Oglądaj razem może nie działać.",
|
||||
"reconnectingToHost": "Ponowne łączenie z hostem...",
|
||||
"currentPlayback": "Bieżące odtwarzanie",
|
||||
"joinCurrentPlayback": "Dołącz do bieżącego odtwarzania",
|
||||
"joinCurrentPlaybackDescription": "Wróć do tego, co host aktualnie ogląda",
|
||||
"failedToOpenCurrentPlayback": "Nie udało się otworzyć bieżącego odtwarzania",
|
||||
"participantJoined": "${name} dołączył",
|
||||
"participantLeft": "${name} opuścił",
|
||||
"participantPaused": "${name} wstrzymał",
|
||||
"participantResumed": "${name} wznowił",
|
||||
"participantSeeked": "${name} przewinął",
|
||||
"participantBuffering": "${name} buforuje",
|
||||
"waitingForParticipants": "Oczekiwanie na załadowanie u innych...",
|
||||
"recentRooms": "Ostatnie pokoje",
|
||||
"renameRoom": "Zmień nazwę pokoju",
|
||||
"removeRoom": "Usuń"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Pobrania",
|
||||
"manage": "Zarządzaj",
|
||||
"tvShows": "Seriale TV",
|
||||
"movies": "Filmy",
|
||||
"noDownloads": "Brak pobrań",
|
||||
"noDownloadsDescription": "Pobrane treści pojawią się tutaj do oglądania offline",
|
||||
"downloadNow": "Pobierz",
|
||||
"deleteDownload": "Usuń pobranie",
|
||||
"retryDownload": "Ponów pobieranie",
|
||||
"downloadQueued": "Pobranie w kolejce",
|
||||
"serverErrorBitrate": "Błąd serwera — plik może przekraczać limit bitrate zdalnego streamingu",
|
||||
"episodesQueued": "${count} odcinków w kolejce pobierania",
|
||||
"downloadDeleted": "Pobranie usunięte",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć \"${title}\"? Spowoduje to usunięcie pobranego pliku z urządzenia.",
|
||||
"deletingWithProgress": "Usuwanie ${title}... (${current} z ${total})",
|
||||
"noDownloadsTree": "Brak pobrań",
|
||||
"pauseAll": "Wstrzymaj wszystko",
|
||||
"resumeAll": "Wznów wszystko",
|
||||
"deleteAll": "Usuń wszystko",
|
||||
"selectVersion": "Wybierz wersję",
|
||||
"allEpisodes": "Wszystkie odcinki",
|
||||
"unwatchedOnly": "Tylko nieobejrzane",
|
||||
"nextNUnwatched": "Następne ${count} nieobejrzanych",
|
||||
"customAmount": "Własna ilość...",
|
||||
"howManyEpisodes": "Ile odcinków?",
|
||||
"itemsQueued": "${count} elementów dodanych do kolejki pobierania",
|
||||
"keepSynced": "Synchronizuj na bieżąco",
|
||||
"downloadOnce": "Pobierz raz",
|
||||
"keepNUnwatched": "Zachowaj ${count} nieobejrzanych",
|
||||
"editSyncRule": "Edytuj regułę synchronizacji",
|
||||
"removeSyncRule": "Usuń regułę synchronizacji",
|
||||
"removeSyncRuleConfirm": "Zatrzymać synchronizację \"${title}\"? Pobrane odcinki zostaną zachowane.",
|
||||
"syncRuleCreated": "Reguła synchronizacji utworzona — zachowywanie ${count} nieobejrzanych odcinków",
|
||||
"syncRuleUpdated": "Reguła synchronizacji zaktualizowana",
|
||||
"syncRuleRemoved": "Reguła synchronizacji usunięta",
|
||||
"syncedNewEpisodes": "Zsynchronizowano ${count} nowych odcinków dla ${title}",
|
||||
"activeSyncRules": "Reguły synchronizacji",
|
||||
"noSyncRules": "Brak reguł synchronizacji",
|
||||
"manageSyncRule": "Zarządzaj synchronizacją",
|
||||
"editEpisodeCount": "Liczba odcinków",
|
||||
"editSyncFilter": "Filtr synchronizacji",
|
||||
"syncAllItems": "Synchronizuję wszystkie elementy",
|
||||
"syncUnwatchedItems": "Synchronizuję nieobejrzane elementy",
|
||||
"syncRuleListCreated": "Utworzono regułę synchronizacji"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shadery",
|
||||
"noShaderDescription": "Bez ulepszenia wideo",
|
||||
"nvscalerDescription": "Skalowanie obrazu NVIDIA dla ostrzejszego wideo",
|
||||
"qualityFast": "Szybki",
|
||||
"qualityHQ": "Wysoka jakość",
|
||||
"mode": "Tryb",
|
||||
"importShader": "Importuj shader",
|
||||
"customShaderDescription": "Niestandardowy shader GLSL",
|
||||
"shaderImported": "Shader zaimportowany",
|
||||
"shaderImportFailed": "Nie udało się zaimportować shadera",
|
||||
"deleteShader": "Usuń shader",
|
||||
"deleteShaderConfirm": "Usunąć \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Pilot zdalny",
|
||||
"connectedTo": "Połączono z ${name}",
|
||||
"session": {
|
||||
"startingServer": "Uruchamianie serwera zdalnego...",
|
||||
"failedToCreate": "Nie udało się uruchomić serwera zdalnego:",
|
||||
"hostAddress": "Adres hosta",
|
||||
"connected": "Połączono",
|
||||
"serverRunning": "Serwer zdalny aktywny",
|
||||
"serverStopped": "Serwer zdalny zatrzymany",
|
||||
"serverRunningDescription": "Urządzenia mobilne w sieci mogą wykryć tę aplikację i się z nią połączyć",
|
||||
"serverStoppedDescription": "Uruchom serwer, aby umożliwić połączenie urządzeń mobilnych",
|
||||
"usePhoneToControl": "Użyj urządzenia mobilnego, aby sterować tą aplikacją",
|
||||
"startServer": "Uruchom serwer",
|
||||
"stopServer": "Zatrzymaj serwer",
|
||||
"minimize": "Minimalizuj"
|
||||
},
|
||||
"pairing": {
|
||||
"discoveryDescription": "Urządzenia w sieci z Plezy na tym samym koncie Plex pojawią się automatycznie",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"connecting": "Łączenie...",
|
||||
"searchingForDevices": "Szukanie urządzeń...",
|
||||
"noDevicesFound": "Nie znaleziono urządzeń w sieci",
|
||||
"noDevicesHint": "Upewnij się, że Plezy jest otwarte na komputerze i oba urządzenia są w tej samej sieci WiFi",
|
||||
"availableDevices": "Dostępne urządzenia",
|
||||
"manualConnection": "Połączenie ręczne",
|
||||
"cryptoInitFailed": "Nie udało się zainicjować bezpiecznego połączenia. Upewnij się, że jesteś zalogowany na konto Plex.",
|
||||
"validationHostRequired": "Wprowadź adres hosta",
|
||||
"validationHostFormat": "Format musi być IP:port (np. 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "Przekroczono limit czasu połączenia. Upewnij się, że oba urządzenia są w tej samej sieci.",
|
||||
"sessionNotFound": "Nie znaleziono urządzenia. Upewnij się, że Plezy działa na hoście.",
|
||||
"authFailed": "Uwierzytelnianie nie powiodło się. Upewnij się, że oba urządzenia używają tego samego konta Plex.",
|
||||
"failedToConnect": "Nie udało się połączyć: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
"disconnectConfirm": "Czy chcesz się rozłączyć od sesji zdalnej?",
|
||||
"reconnecting": "Ponowne łączenie...",
|
||||
"attemptOf": "Próba ${current} z 5",
|
||||
"retryNow": "Ponów teraz",
|
||||
"tabRemote": "Pilot",
|
||||
"tabPlay": "Odtwórz",
|
||||
"tabMore": "Więcej",
|
||||
"menu": "Menu",
|
||||
"tabNavigation": "Nawigacja",
|
||||
"tabDiscover": "Odkryj",
|
||||
"tabLibraries": "Biblioteki",
|
||||
"tabSearch": "Szukaj",
|
||||
"tabDownloads": "Pobrania",
|
||||
"tabSettings": "Ustawienia",
|
||||
"previous": "Poprzedni",
|
||||
"playPause": "Odtwórz/Pauza",
|
||||
"next": "Następny",
|
||||
"seekBack": "Przewiń wstecz",
|
||||
"stop": "Stop",
|
||||
"seekForward": "Przewiń w przód",
|
||||
"volume": "Głośność",
|
||||
"volumeDown": "Ciszej",
|
||||
"volumeUp": "Głośniej",
|
||||
"fullscreen": "Pełny ekran",
|
||||
"subtitles": "Napisy",
|
||||
"audio": "Audio",
|
||||
"searchHint": "Szukaj na komputerze..."
|
||||
}
|
||||
},
|
||||
"videoSettings": {
|
||||
"playbackSettings": "Ustawienia odtwarzania",
|
||||
"playbackSpeed": "Prędkość odtwarzania",
|
||||
"sleepTimer": "Wyłącznik czasowy",
|
||||
"audioSync": "Synchronizacja audio",
|
||||
"subtitleSync": "Synchronizacja napisów",
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Wyjście audio",
|
||||
"performanceOverlay": "Nakładka wydajności",
|
||||
"audioPassthrough": "Bezpośrednie audio",
|
||||
"audioNormalization": "Normalizacja audio"
|
||||
},
|
||||
"externalPlayer": {
|
||||
"title": "Zewnętrzny odtwarzacz",
|
||||
"useExternalPlayer": "Użyj zewnętrznego odtwarzacza",
|
||||
"useExternalPlayerDescription": "Otwieraj wideo w zewnętrznej aplikacji zamiast wbudowanego odtwarzacza",
|
||||
"selectPlayer": "Wybierz odtwarzacz",
|
||||
"customPlayers": "Niestandardowe odtwarzacze",
|
||||
"systemDefault": "Domyślny systemowy",
|
||||
"addCustomPlayer": "Dodaj niestandardowy odtwarzacz",
|
||||
"playerName": "Nazwa odtwarzacza",
|
||||
"playerCommand": "Polecenie",
|
||||
"playerPackage": "Nazwa pakietu",
|
||||
"playerUrlScheme": "Schemat URL",
|
||||
"off": "Wyłączony",
|
||||
"launchFailed": "Nie udało się otworzyć zewnętrznego odtwarzacza",
|
||||
"appNotInstalled": "${name} nie jest zainstalowany",
|
||||
"playInExternalPlayer": "Odtwórz w zewnętrznym odtwarzaczu"
|
||||
},
|
||||
"metadataEdit": {
|
||||
"editMetadata": "Edytuj...",
|
||||
"screenTitle": "Edytuj metadane",
|
||||
"basicInfo": "Podstawowe informacje",
|
||||
"artwork": "Grafika",
|
||||
"advancedSettings": "Ustawienia zaawansowane",
|
||||
"title": "Tytuł",
|
||||
"sortTitle": "Tytuł do sortowania",
|
||||
"originalTitle": "Tytuł oryginalny",
|
||||
"releaseDate": "Data premiery",
|
||||
"contentRating": "Klasyfikacja wiekowa",
|
||||
"studio": "Studio",
|
||||
"tagline": "Tagline",
|
||||
"summary": "Opis",
|
||||
"poster": "Plakat",
|
||||
"background": "Tło",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Kwadratowy obraz",
|
||||
"selectPoster": "Wybierz plakat",
|
||||
"selectBackground": "Wybierz tło",
|
||||
"selectLogo": "Wybierz logo",
|
||||
"selectSquareArt": "Wybierz kwadratowy obraz",
|
||||
"fromUrl": "Z URL",
|
||||
"uploadFile": "Prześlij plik",
|
||||
"enterImageUrl": "Wprowadź URL obrazu",
|
||||
"imageUrl": "URL obrazu",
|
||||
"metadataUpdated": "Metadane zaktualizowane",
|
||||
"metadataUpdateFailed": "Nie udało się zaktualizować metadanych",
|
||||
"artworkUpdated": "Grafika zaktualizowana",
|
||||
"artworkUpdateFailed": "Nie udało się zaktualizować grafiki",
|
||||
"noArtworkAvailable": "Brak dostępnej grafiki",
|
||||
"notSet": "Nie ustawiono",
|
||||
"libraryDefault": "Domyślne biblioteki",
|
||||
"accountDefault": "Domyślne konta",
|
||||
"seriesDefault": "Domyślne serialu",
|
||||
"episodeSorting": "Sortowanie odcinków",
|
||||
"oldestFirst": "Najstarsze najpierw",
|
||||
"newestFirst": "Najnowsze najpierw",
|
||||
"keep": "Zachowaj",
|
||||
"allEpisodes": "Wszystkie odcinki",
|
||||
"latestEpisodes": "${count} najnowszych odcinków",
|
||||
"latestEpisode": "Najnowszy odcinek",
|
||||
"episodesAddedPastDays": "Odcinki dodane w ciągu ostatnich ${count} dni",
|
||||
"deleteAfterPlaying": "Usuń odcinki po odtworzeniu",
|
||||
"never": "Nigdy",
|
||||
"afterADay": "Po jednym dniu",
|
||||
"afterAWeek": "Po tygodniu",
|
||||
"afterAMonth": "Po miesiącu",
|
||||
"onNextRefresh": "Przy następnym odświeżeniu",
|
||||
"seasons": "Sezony",
|
||||
"show": "Pokaż",
|
||||
"hide": "Ukryj",
|
||||
"episodeOrdering": "Kolejność odcinków",
|
||||
"tmdbAiring": "The Movie Database (Emisja)",
|
||||
"tvdbAiring": "TheTVDB (Emisja)",
|
||||
"tvdbAbsolute": "TheTVDB (Absolutna)",
|
||||
"metadataLanguage": "Język metadanych",
|
||||
"useOriginalTitle": "Użyj oryginalnego tytułu",
|
||||
"preferredAudioLanguage": "Preferowany język audio",
|
||||
"preferredSubtitleLanguage": "Preferowany język napisów",
|
||||
"subtitleMode": "Tryb automatycznego wyboru napisów",
|
||||
"manuallySelected": "Wybrany ręcznie",
|
||||
"shownWithForeignAudio": "Wyświetlane przy obcojęzycznym audio",
|
||||
"alwaysEnabled": "Zawsze włączone",
|
||||
"tags": "Tagi",
|
||||
"addTag": "Dodaj tag",
|
||||
"genre": "Gatunek",
|
||||
"director": "Reżyser",
|
||||
"writer": "Scenarzysta",
|
||||
"producer": "Producent",
|
||||
"country": "Kraj",
|
||||
"collection": "Kolekcja",
|
||||
"label": "Etykieta",
|
||||
"style": "Styl",
|
||||
"mood": "Nastrój"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Zadania serwera",
|
||||
"failedToLoad": "Nie udało się załadować zadań",
|
||||
"noTasks": "Brak uruchomionych zadań"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Połączono",
|
||||
"connectedAs": "Połączono jako @${username}",
|
||||
"disconnectConfirm": "Rozłączyć konto Trakt?",
|
||||
"disconnectConfirmBody": "Plezy przestanie wysyłać zdarzenia odtwarzania do Trakt. Możesz połączyć się ponownie w dowolnej chwili.",
|
||||
"scrobble": "Scrobbling w czasie rzeczywistym",
|
||||
"scrobbleDescription": "Wysyłaj zdarzenia odtwarzania, pauzy i zatrzymania do Trakt podczas odtwarzania.",
|
||||
"watchedSync": "Synchronizuj status obejrzane",
|
||||
"watchedSyncDescription": "Gdy oznaczysz pozycje jako obejrzane w Plezy, zostaną też oznaczone w Trakt.",
|
||||
"deviceCodeTitle": "Aktywuj Plezy w Trakt",
|
||||
"deviceCodeBody": "Otwórz ${url} i wpisz ten kod:",
|
||||
"openTraktActivate": "Otwórz Trakt, aby aktywować",
|
||||
"waitingForAuthorization": "Czekanie na autoryzację…",
|
||||
"codeCopied": "Kod skopiowany",
|
||||
"connectFailed": "Nie udało się połączyć z Trakt. Spróbuj ponownie."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Plezy"
|
||||
},
|
||||
"auth": {
|
||||
"signInWithPlex": "Entrar com Plex",
|
||||
"showQRCode": "Mostrar QR Code",
|
||||
"authenticate": "Autenticar",
|
||||
"authenticationTimeout": "A autenticação expirou. Tente novamente.",
|
||||
"scanQRToSignIn": "Escaneie este QR code para entrar",
|
||||
"waitingForAuth": "Aguardando autenticação...\nConclua o login no seu navegador.",
|
||||
"useBrowser": "Usar navegador"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancelar",
|
||||
"save": "Salvar",
|
||||
"close": "Fechar",
|
||||
"clear": "Limpar",
|
||||
"reset": "Redefinir",
|
||||
"later": "Depois",
|
||||
"submit": "Enviar",
|
||||
"confirm": "Confirmar",
|
||||
"retry": "Tentar novamente",
|
||||
"logout": "Sair",
|
||||
"unknown": "Desconhecido",
|
||||
"refresh": "Atualizar",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"delete": "Excluir",
|
||||
"shuffle": "Aleatório",
|
||||
"addTo": "Adicionar a...",
|
||||
"createNew": "Criar novo",
|
||||
"connect": "Conectar",
|
||||
"disconnect": "Desconectar",
|
||||
"play": "Reproduzir",
|
||||
"pause": "Pausar",
|
||||
"resume": "Retomar",
|
||||
"error": "Erro",
|
||||
"search": "Buscar",
|
||||
"home": "Início",
|
||||
"back": "Voltar",
|
||||
"settings": "Configurações",
|
||||
"mute": "Silenciar",
|
||||
"ok": "OK",
|
||||
"reconnect": "Reconectar",
|
||||
"exitConfirmTitle": "Sair do app?",
|
||||
"exitConfirmMessage": "Tem certeza que deseja sair?",
|
||||
"dontAskAgain": "Não perguntar novamente",
|
||||
"exit": "Sair",
|
||||
"viewAll": "Ver Tudo",
|
||||
"checkingNetwork": "Verificando rede...",
|
||||
"refreshingServers": "Atualizando servidores...",
|
||||
"loadingServers": "Carregando servidores...",
|
||||
"connectingToServers": "Conectando aos servidores...",
|
||||
"startingOfflineMode": "Iniciando modo offline...",
|
||||
"loading": "Carregando..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licenças",
|
||||
"switchProfile": "Trocar Perfil",
|
||||
"subtitleStyling": "Estilo de Legendas",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"update": {
|
||||
"available": "Atualização Disponível",
|
||||
"versionAvailable": "Versão ${version} está disponível",
|
||||
"currentVersion": "Atual: ${version}",
|
||||
"skipVersion": "Pular Esta Versão",
|
||||
"viewRelease": "Ver Lançamento",
|
||||
"latestVersion": "Você está na versão mais recente",
|
||||
"checkFailed": "Falha ao verificar atualizações"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"supportDeveloper": "Apoie o Plezy",
|
||||
"supportDeveloperDescription": "Doe via Liberapay para financiar o desenvolvimento",
|
||||
"language": "Idioma",
|
||||
"theme": "Tema",
|
||||
"appearance": "Aparência",
|
||||
"videoPlayback": "Reprodução de Vídeo",
|
||||
"videoPlaybackDescription": "Configurar comportamento de reprodução",
|
||||
"advanced": "Avançado",
|
||||
"episodePosterMode": "Estilo do Poster de Episódio",
|
||||
"seriesPoster": "Poster da Série",
|
||||
"seasonPoster": "Poster da Temporada",
|
||||
"episodeThumbnail": "Miniatura",
|
||||
"showHeroSectionDescription": "Exibir carrossel de conteúdo em destaque na tela inicial",
|
||||
"secondsLabel": "Segundos",
|
||||
"minutesLabel": "Minutos",
|
||||
"secondsShort": "s",
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Insira a duração (${min}-${max})",
|
||||
"systemTheme": "Sistema",
|
||||
"lightTheme": "Claro",
|
||||
"darkTheme": "Escuro",
|
||||
"oledTheme": "OLED",
|
||||
"libraryDensity": "Densidade da Biblioteca",
|
||||
"compact": "Compacto",
|
||||
"comfortable": "Confortável",
|
||||
"viewMode": "Modo de Visualização",
|
||||
"gridView": "Grade",
|
||||
"listView": "Lista",
|
||||
"showHeroSection": "Mostrar Seção de Destaque",
|
||||
"useGlobalHubs": "Usar Layout Plex Home",
|
||||
"useGlobalHubsDescription": "Mostrar hubs da página inicial como o cliente oficial Plex. Quando desativado, mostra recomendações por biblioteca.",
|
||||
"showServerNameOnHubs": "Mostrar Nome do Servidor nos Hubs",
|
||||
"showServerNameOnHubsDescription": "Sempre exibir o nome do servidor nos títulos dos hubs. Quando desativado, mostra apenas para nomes duplicados.",
|
||||
"alwaysKeepSidebarOpen": "Manter Barra Lateral Sempre Aberta",
|
||||
"alwaysKeepSidebarOpenDescription": "A barra lateral fica expandida e a área de conteúdo se ajusta",
|
||||
"showUnwatchedCount": "Mostrar Contagem de Não Assistidos",
|
||||
"showUnwatchedCountDescription": "Exibir contagem de episódios não assistidos em séries e temporadas",
|
||||
"hideSpoilers": "Ocultar Spoilers de Episódios Não Assistidos",
|
||||
"hideSpoilersDescription": "Desfocar miniaturas e ocultar descrições de episódios que você ainda não assistiu",
|
||||
"playerBackend": "Backend do Player",
|
||||
"exoPlayer": "ExoPlayer (Recomendado)",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Decodificação por Hardware",
|
||||
"hardwareDecodingDescription": "Usar aceleração por hardware quando disponível",
|
||||
"bufferSize": "Tamanho do Buffer",
|
||||
"bufferSizeMB": "${size}MB",
|
||||
"bufferSizeAuto": "Automático (Recomendado)",
|
||||
"bufferSizeWarning": "Seu dispositivo tem ${heap}MB de memória. Um buffer de ${size}MB pode causar problemas de reprodução.",
|
||||
"subtitleStyling": "Estilo de Legendas",
|
||||
"subtitleStylingDescription": "Personalizar aparência das legendas",
|
||||
"smallSkipDuration": "Duração do Avanço Curto",
|
||||
"largeSkipDuration": "Duração do Avanço Longo",
|
||||
"rewindOnResume": "Rebobinar ao retomar",
|
||||
"secondsUnit": "${seconds} segundos",
|
||||
"defaultSleepTimer": "Timer de Sono Padrão",
|
||||
"minutesUnit": "${minutes} minutos",
|
||||
"rememberTrackSelections": "Lembrar seleção de faixas por série/filme",
|
||||
"rememberTrackSelectionsDescription": "Salvar automaticamente preferências de idioma de áudio e legenda ao trocar faixas durante a reprodução",
|
||||
"clickVideoTogglesPlayback": "Clicar no vídeo para alternar reprodução/pausa",
|
||||
"clickVideoTogglesPlaybackDescription": "Se ativado, clicar no player reproduz/pausa o vídeo. Caso contrário, clicar mostra/oculta os controles.",
|
||||
"videoPlayerControls": "Controles do Player de Vídeo",
|
||||
"keyboardShortcuts": "Atalhos de Teclado",
|
||||
"keyboardShortcutsDescription": "Personalizar atalhos de teclado",
|
||||
"videoPlayerNavigation": "Navegação do Player de Vídeo",
|
||||
"videoPlayerNavigationDescription": "Usar teclas de seta para navegar nos controles do player",
|
||||
"watchTogetherRelay": "Relay do Assistir Juntos",
|
||||
"watchTogetherRelayDescription": "Definir um servidor relay personalizado para Assistir Juntos. Todos os participantes devem usar o mesmo servidor.",
|
||||
"watchTogetherRelayHint": "https://meu-relay.exemplo.com.br",
|
||||
"crashReporting": "Relatório de Erros",
|
||||
"crashReportingDescription": "Enviar relatórios de erros para ajudar a melhorar o app",
|
||||
"debugLogging": "Log de Depuração",
|
||||
"debugLoggingDescription": "Ativar log detalhado para solução de problemas",
|
||||
"viewLogs": "Ver Logs",
|
||||
"viewLogsDescription": "Ver logs do aplicativo",
|
||||
"clearCache": "Limpar Cache",
|
||||
"clearCacheDescription": "Isso limpará todas as imagens e dados em cache. O app pode demorar mais para carregar conteúdo após limpar o cache.",
|
||||
"clearCacheSuccess": "Cache limpo com sucesso",
|
||||
"resetSettings": "Redefinir Configurações",
|
||||
"resetSettingsDescription": "Isso redefinirá todas as configurações para os valores padrão. Esta ação não pode ser desfeita.",
|
||||
"resetSettingsSuccess": "Configurações redefinidas com sucesso",
|
||||
"shortcutsReset": "Atalhos redefinidos para o padrão",
|
||||
"about": "Sobre",
|
||||
"aboutDescription": "Informações do app e licenças",
|
||||
"updates": "Atualizações",
|
||||
"updateAvailable": "Atualização Disponível",
|
||||
"checkForUpdates": "Verificar Atualizações",
|
||||
"validationErrorEnterNumber": "Insira um número válido",
|
||||
"validationErrorDuration": "A duração deve ser entre ${min} e ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Atalho já atribuído a ${action}",
|
||||
"shortcutUpdated": "Atalho atualizado para ${action}",
|
||||
"autoSkip": "Pular Automaticamente",
|
||||
"autoSkipIntro": "Pular Intro Automaticamente",
|
||||
"autoSkipIntroDescription": "Pular marcadores de intro automaticamente após alguns segundos",
|
||||
"autoSkipCredits": "Pular Créditos Automaticamente",
|
||||
"autoSkipCreditsDescription": "Pular créditos automaticamente e reproduzir próximo episódio",
|
||||
"autoSkipDelay": "Atraso do Pulo Automático",
|
||||
"autoSkipDelayDescription": "Aguardar ${seconds} segundos antes de pular automaticamente",
|
||||
"introPattern": "Padrão de marcador de intro",
|
||||
"introPatternDescription": "Expressão regular para corresponder marcadores de intro nos títulos dos capítulos",
|
||||
"creditsPattern": "Padrão de marcador de créditos",
|
||||
"creditsPatternDescription": "Expressão regular para corresponder marcadores de créditos nos títulos dos capítulos",
|
||||
"invalidRegex": "Expressão regular inválida",
|
||||
"downloads": "Downloads",
|
||||
"downloadLocationDescription": "Escolha onde armazenar conteúdo baixado",
|
||||
"downloadLocationDefault": "Padrão (Armazenamento do App)",
|
||||
"downloadLocationCustom": "Local Personalizado",
|
||||
"selectFolder": "Selecionar Pasta",
|
||||
"resetToDefault": "Redefinir para Padrão",
|
||||
"currentPath": "Atual: ${path}",
|
||||
"downloadLocationChanged": "Local de download alterado",
|
||||
"downloadLocationReset": "Local de download redefinido para padrão",
|
||||
"downloadLocationInvalid": "A pasta selecionada não permite gravação",
|
||||
"downloadLocationSelectError": "Falha ao selecionar pasta",
|
||||
"downloadOnWifiOnly": "Baixar apenas no WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Impedir downloads quando em dados móveis",
|
||||
"autoRemoveWatchedDownloads": "Remover downloads assistidos automaticamente",
|
||||
"autoRemoveWatchedDownloadsDescription": "Excluir automaticamente episódios e filmes baixados quando marcados como assistidos",
|
||||
"cellularDownloadBlocked": "Downloads estão desativados em dados móveis. Conecte ao WiFi ou altere a configuração.",
|
||||
"maxVolume": "Volume Máximo",
|
||||
"maxVolumeDescription": "Permitir aumento de volume acima de 100% para mídias silenciosas",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Mostrar o que você está assistindo no Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Sincronizar histórico de exibição com Trakt",
|
||||
"companionRemoteServer": "Servidor de controlo remoto",
|
||||
"companionRemoteServerDescription": "Permitir que dispositivos móveis na sua rede controlem esta aplicação",
|
||||
"autoPip": "Picture-in-Picture Automático",
|
||||
"autoPipDescription": "Entrar automaticamente em picture-in-picture ao sair do app durante a reprodução",
|
||||
"matchContentFrameRate": "Corresponder Taxa de Quadros do Conteúdo",
|
||||
"matchContentFrameRateDescription": "Ajustar a taxa de atualização da tela para corresponder ao conteúdo de vídeo, reduzindo tremulação e economizando bateria",
|
||||
"matchRefreshRate": "Corresponder Taxa de Atualização",
|
||||
"matchRefreshRateDescription": "Alterar a taxa de atualização da tela para corresponder ao conteúdo de vídeo em tela cheia",
|
||||
"matchDynamicRange": "Corresponder Faixa Dinâmica",
|
||||
"matchDynamicRangeDescription": "Ativar automaticamente o HDR para conteúdo HDR e voltar ao SDR ao sair do reprodutor",
|
||||
"displaySwitchDelay": "Atraso na Troca de Tela",
|
||||
"tunneledPlayback": "Reprodução Tunelizada",
|
||||
"tunneledPlaybackDescription": "Usar tunelamento de vídeo acelerado por hardware. Desative se você vir uma tela preta com áudio em conteúdo HDR",
|
||||
"requireProfileSelectionOnOpen": "Pedir perfil ao abrir o app",
|
||||
"requireProfileSelectionOnOpenDescription": "Mostrar seleção de perfil toda vez que o app for aberto",
|
||||
"confirmExitOnBack": "Confirmar antes de sair",
|
||||
"confirmExitOnBackDescription": "Mostrar diálogo de confirmação ao pressionar voltar para sair do app",
|
||||
"forceTvMode": "Forçar modo TV",
|
||||
"forceTvModeDescription": "Usar o layout de TV independentemente da detecção automática. Útil em dispositivos Android TV que não reportam o recurso leanback. Reinicia o app ao alterar.",
|
||||
"autoHidePerformanceOverlay": "Ocultar overlay de desempenho automaticamente",
|
||||
"autoHidePerformanceOverlayDescription": "Desvanecer o overlay de desempenho com os controles de reprodução",
|
||||
"showNavBarLabels": "Mostrar Rótulos da Barra de Navegação",
|
||||
"showNavBarLabelsDescription": "Exibir rótulos de texto sob os ícones da barra de navegação",
|
||||
"liveTvDefaultFavorites": "Canais favoritos por padrão",
|
||||
"liveTvDefaultFavoritesDescription": "Mostrar apenas canais favoritos ao abrir TV ao vivo",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Buscar filmes, séries, músicas...",
|
||||
"tryDifferentTerm": "Tente um termo de busca diferente",
|
||||
"searchYourMedia": "Buscar suas mídias",
|
||||
"enterTitleActorOrKeyword": "Insira um título, ator ou palavra-chave"
|
||||
},
|
||||
"hotkeys": {
|
||||
"setShortcutFor": "Definir Atalho para ${actionName}",
|
||||
"clearShortcut": "Limpar atalho",
|
||||
"actions": {
|
||||
"playPause": "Reproduzir/Pausar",
|
||||
"volumeUp": "Aumentar Volume",
|
||||
"volumeDown": "Diminuir Volume",
|
||||
"seekForward": "Avançar (${seconds}s)",
|
||||
"seekBackward": "Retroceder (${seconds}s)",
|
||||
"fullscreenToggle": "Alternar Tela Cheia",
|
||||
"muteToggle": "Alternar Silêncio",
|
||||
"subtitleToggle": "Alternar Legendas",
|
||||
"audioTrackNext": "Próxima Faixa de Áudio",
|
||||
"subtitleTrackNext": "Próxima Faixa de Legenda",
|
||||
"chapterNext": "Próximo Capítulo",
|
||||
"chapterPrevious": "Capítulo Anterior",
|
||||
"speedIncrease": "Aumentar Velocidade",
|
||||
"speedDecrease": "Diminuir Velocidade",
|
||||
"speedReset": "Redefinir Velocidade",
|
||||
"subSeekNext": "Ir para Próxima Legenda",
|
||||
"subSeekPrev": "Ir para Legenda Anterior",
|
||||
"shaderToggle": "Alternar Shaders",
|
||||
"skipMarker": "Pular Intro/Créditos"
|
||||
}
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Info do Arquivo",
|
||||
"video": "Vídeo",
|
||||
"audio": "Áudio",
|
||||
"file": "Arquivo",
|
||||
"advanced": "Avançado",
|
||||
"codec": "Codec",
|
||||
"resolution": "Resolução",
|
||||
"bitrate": "Taxa de Bits",
|
||||
"frameRate": "Taxa de Quadros",
|
||||
"aspectRatio": "Proporção",
|
||||
"profile": "Perfil",
|
||||
"bitDepth": "Profundidade de Bits",
|
||||
"colorSpace": "Espaço de Cor",
|
||||
"colorRange": "Faixa de Cor",
|
||||
"colorPrimaries": "Primárias de Cor",
|
||||
"chromaSubsampling": "Subamostragem de Croma",
|
||||
"channels": "Canais",
|
||||
"subtitles": "Legendas",
|
||||
"overallBitrate": "Taxa de bits total",
|
||||
"path": "Caminho",
|
||||
"size": "Tamanho",
|
||||
"container": "Container",
|
||||
"duration": "Duração",
|
||||
"optimizedForStreaming": "Otimizado para Streaming",
|
||||
"has64bitOffsets": "Offsets de 64 bits"
|
||||
},
|
||||
"mediaMenu": {
|
||||
"markAsWatched": "Marcar como Assistido",
|
||||
"markAsUnwatched": "Marcar como Não Assistido",
|
||||
"removeFromContinueWatching": "Remover de Continuar Assistindo",
|
||||
"goToSeries": "Ir para a série",
|
||||
"goToSeason": "Ir para a temporada",
|
||||
"shufflePlay": "Reprodução Aleatória",
|
||||
"fileInfo": "Info do Arquivo",
|
||||
"deleteFromServer": "Excluir do servidor",
|
||||
"confirmDelete": "Isso excluirá permanentemente esta mídia e seus arquivos do seu servidor. Esta ação não pode ser desfeita.",
|
||||
"deleteMultipleWarning": "Isso inclui todos os episódios e seus arquivos.",
|
||||
"mediaDeletedSuccessfully": "Item de mídia excluído com sucesso",
|
||||
"mediaFailedToDelete": "Falha ao excluir item de mídia",
|
||||
"rate": "Avaliar",
|
||||
"playFromBeginning": "Reproduzir do início",
|
||||
"playVersion": "Reproduzir versão..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, filme",
|
||||
"mediaCardShow": "${title}, série de TV",
|
||||
"mediaCardEpisode": "${title}, ${episodeInfo}",
|
||||
"mediaCardSeason": "${title}, ${seasonInfo}",
|
||||
"mediaCardWatched": "assistido",
|
||||
"mediaCardPartiallyWatched": "${percent} por cento assistido",
|
||||
"mediaCardUnwatched": "não assistido",
|
||||
"tapToPlay": "Toque para reproduzir"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Reprodução aleatória",
|
||||
"playTrailer": "Reproduzir trailer",
|
||||
"markAsWatched": "Marcar como assistido",
|
||||
"markAsUnwatched": "Marcar como não assistido"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Áudio",
|
||||
"subtitlesLabel": "Legendas",
|
||||
"resetToZero": "Redefinir para 0ms",
|
||||
"addTime": "+${amount}${unit}",
|
||||
"minusTime": "-${amount}${unit}",
|
||||
"playsLater": "${label} reproduz depois",
|
||||
"playsEarlier": "${label} reproduz antes",
|
||||
"noOffset": "Sem deslocamento",
|
||||
"letterbox": "Letterbox",
|
||||
"fillScreen": "Preencher tela",
|
||||
"stretch": "Esticar",
|
||||
"lockRotation": "Travar rotação",
|
||||
"unlockRotation": "Destravar rotação",
|
||||
"timerActive": "Timer Ativo",
|
||||
"playbackWillPauseIn": "A reprodução pausará em ${duration}",
|
||||
"stillWatching": "Ainda assistindo?",
|
||||
"pausingIn": "Pausando em ${seconds}s",
|
||||
"continueWatching": "Continuar",
|
||||
"autoPlayNext": "Reproduzir Próximo Automaticamente",
|
||||
"playNext": "Reproduzir Próximo",
|
||||
"playButton": "Reproduzir",
|
||||
"pauseButton": "Pausar",
|
||||
"seekBackwardButton": "Retroceder ${seconds} segundos",
|
||||
"seekForwardButton": "Avançar ${seconds} segundos",
|
||||
"previousButton": "Episódio anterior",
|
||||
"nextButton": "Próximo episódio",
|
||||
"previousChapterButton": "Capítulo anterior",
|
||||
"nextChapterButton": "Próximo capítulo",
|
||||
"muteButton": "Silenciar",
|
||||
"unmuteButton": "Ativar som",
|
||||
"settingsButton": "Configurações de vídeo",
|
||||
"tracksButton": "Áudio e Legendas",
|
||||
"chaptersButton": "Capítulos",
|
||||
"versionsButton": "Versões do vídeo",
|
||||
"pipButton": "Modo Picture-in-Picture",
|
||||
"aspectRatioButton": "Proporção",
|
||||
"ambientLighting": "Iluminação ambiente",
|
||||
"fullscreenButton": "Entrar em tela cheia",
|
||||
"exitFullscreenButton": "Sair da tela cheia",
|
||||
"alwaysOnTopButton": "Sempre no topo",
|
||||
"rotationLockButton": "Travar rotação",
|
||||
"lockScreen": "Travar tela",
|
||||
"screenLockButton": "Travar tela",
|
||||
"longPressToUnlock": "Pressione e segure para destravar",
|
||||
"timelineSlider": "Linha do tempo do vídeo",
|
||||
"volumeSlider": "Nível de volume",
|
||||
"endsAt": "Termina às ${time}",
|
||||
"pipActive": "Reproduzindo em Picture-in-Picture",
|
||||
"pipFailed": "Falha ao iniciar picture-in-picture",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Requer Android 8.0 ou superior",
|
||||
"iosVersion": "Requer iOS 15.0 ou superior",
|
||||
"permissionDisabled": "A permissão de picture-in-picture está desativada. Ative em Configurações > Apps > Plezy > Picture-in-picture",
|
||||
"notSupported": "O dispositivo não suporta modo picture-in-picture",
|
||||
"voSwitchFailed": "Falha ao trocar saída de vídeo para picture-in-picture",
|
||||
"failed": "Falha ao iniciar picture-in-picture",
|
||||
"unknown": "Ocorreu um erro: ${error}"
|
||||
},
|
||||
"chapters": "Capítulos",
|
||||
"noChaptersAvailable": "Nenhum capítulo disponível",
|
||||
"queue": "Fila",
|
||||
"noQueueItems": "Nenhum item na fila",
|
||||
"searchSubtitles": "Pesquisar legendas",
|
||||
"language": "Idioma",
|
||||
"noSubtitlesFound": "Nenhuma legenda encontrada",
|
||||
"subtitleDownloaded": "Legenda baixada",
|
||||
"subtitleDownloadFailed": "Falha ao baixar legenda",
|
||||
"searchLanguages": "Pesquisar idiomas..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
"restricted": "Restrito",
|
||||
"protected": "Protegido",
|
||||
"current": "ATUAL"
|
||||
},
|
||||
"messages": {
|
||||
"markedAsWatched": "Marcado como assistido",
|
||||
"markedAsUnwatched": "Marcado como não assistido",
|
||||
"markedAsWatchedOffline": "Marcado como assistido (será sincronizado quando online)",
|
||||
"markedAsUnwatchedOffline": "Marcado como não assistido (será sincronizado quando online)",
|
||||
"autoRemovedWatchedDownload": "Removido automaticamente: ${title}",
|
||||
"removedFromContinueWatching": "Removido de Continuar Assistindo",
|
||||
"errorLoading": "Erro: ${error}",
|
||||
"fileInfoNotAvailable": "Informações do arquivo não disponíveis",
|
||||
"errorLoadingFileInfo": "Erro ao carregar info do arquivo: ${error}",
|
||||
"errorLoadingSeries": "Erro ao carregar série",
|
||||
"errorLoadingSeason": "Erro ao carregar temporada",
|
||||
"musicNotSupported": "Reprodução de música ainda não é suportada",
|
||||
"logsCleared": "Logs limpos",
|
||||
"logsCopied": "Logs copiados para a área de transferência",
|
||||
"noLogsAvailable": "Nenhum log disponível",
|
||||
"libraryScanning": "Escaneando \"${title}\"...",
|
||||
"libraryScanStarted": "Escaneamento da biblioteca iniciado para \"${title}\"",
|
||||
"libraryScanFailed": "Falha ao escanear biblioteca: ${error}",
|
||||
"metadataRefreshing": "Atualizando metadados de \"${title}\"...",
|
||||
"metadataRefreshStarted": "Atualização de metadados iniciada para \"${title}\"",
|
||||
"metadataRefreshFailed": "Falha ao atualizar metadados: ${error}",
|
||||
"logoutConfirm": "Tem certeza que deseja sair?",
|
||||
"noSeasonsFound": "Nenhuma temporada encontrada",
|
||||
"noEpisodesFound": "Nenhum episódio encontrado na primeira temporada",
|
||||
"noEpisodesFoundGeneral": "Nenhum episódio encontrado",
|
||||
"noResultsFound": "Nenhum resultado encontrado",
|
||||
"sleepTimerSet": "Timer de sono definido para ${label}",
|
||||
"noItemsAvailable": "Nenhum item disponível",
|
||||
"failedToCreatePlayQueueNoItems": "Falha ao criar fila de reprodução - sem itens",
|
||||
"failedPlayback": "Falha ao ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Alternando para player compatível...",
|
||||
"serverLimitTitle": "Falha na reprodução",
|
||||
"serverLimitBody": "O servidor retornou um erro (HTTP 500). Isso geralmente significa que o proprietário do servidor definiu um limite de largura de banda ou transcodificação que está rejeitando sua sessão. Não há nada a ser feito pelo cliente — o proprietário do servidor precisa ajustar as configurações.",
|
||||
"logsUploaded": "Logs enviados",
|
||||
"logsUploadFailed": "Falha ao enviar logs",
|
||||
"logId": "ID do Log"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"text": "Texto",
|
||||
"border": "Borda",
|
||||
"background": "Fundo",
|
||||
"fontSize": "Tamanho da Fonte",
|
||||
"textColor": "Cor do Texto",
|
||||
"borderSize": "Tamanho da Borda",
|
||||
"borderColor": "Cor da Borda",
|
||||
"backgroundOpacity": "Opacidade do Fundo",
|
||||
"backgroundColor": "Cor de Fundo",
|
||||
"position": "Posição",
|
||||
"assOverride": "Substituição ASS",
|
||||
"bold": "Negrito",
|
||||
"italic": "Itálico"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "mpv.conf",
|
||||
"description": "Configurações avançadas do player de vídeo",
|
||||
"presets": "Predefinições",
|
||||
"noPresets": "Nenhuma predefinição salva",
|
||||
"saveAsPreset": "Salvar como Predefinição...",
|
||||
"presetName": "Nome da Predefinição",
|
||||
"presetNameHint": "Insira um nome para esta predefinição",
|
||||
"loadPreset": "Carregar",
|
||||
"deletePreset": "Excluir",
|
||||
"presetSaved": "Predefinição salva",
|
||||
"presetLoaded": "Predefinição carregada",
|
||||
"presetDeleted": "Predefinição excluída",
|
||||
"confirmDeletePreset": "Tem certeza que deseja excluir esta predefinição?",
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Confirmar Ação"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Descobrir",
|
||||
"switchProfile": "Trocar Perfil",
|
||||
"noContentAvailable": "Nenhum conteúdo disponível",
|
||||
"addMediaToLibraries": "Adicione mídias às suas bibliotecas",
|
||||
"continueWatching": "Continuar Assistindo",
|
||||
"playEpisode": "S${season}E${episode}",
|
||||
"overview": "Sinopse",
|
||||
"cast": "Elenco",
|
||||
"extras": "Trailers e Extras",
|
||||
"studio": "Estúdio",
|
||||
"rating": "Avaliação",
|
||||
"movie": "Filme",
|
||||
"tvShow": "Série de TV",
|
||||
"minutesLeft": "${minutes} min restantes"
|
||||
},
|
||||
"errors": {
|
||||
"searchFailed": "Falha na busca: ${error}",
|
||||
"connectionTimeout": "Tempo de conexão esgotado ao carregar ${context}",
|
||||
"connectionFailed": "Não foi possível conectar ao servidor Plex",
|
||||
"failedToLoad": "Falha ao carregar ${context}: ${error}",
|
||||
"noClientAvailable": "Nenhum cliente disponível",
|
||||
"authenticationFailed": "Falha na autenticação: ${error}",
|
||||
"couldNotLaunchUrl": "Não foi possível abrir a URL de autenticação",
|
||||
"pleaseEnterToken": "Insira um token",
|
||||
"invalidToken": "Token inválido",
|
||||
"failedToVerifyToken": "Falha ao verificar token: ${error}",
|
||||
"failedToSwitchProfile": "Falha ao trocar para ${displayName}"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Bibliotecas",
|
||||
"scanLibraryFiles": "Escanear Arquivos da Biblioteca",
|
||||
"scanLibrary": "Escanear Biblioteca",
|
||||
"analyze": "Analisar",
|
||||
"analyzeLibrary": "Analisar Biblioteca",
|
||||
"refreshMetadata": "Atualizar Metadados",
|
||||
"emptyTrash": "Esvaziar Lixeira",
|
||||
"emptyingTrash": "Esvaziando lixeira de \"${title}\"...",
|
||||
"trashEmptied": "Lixeira esvaziada de \"${title}\"",
|
||||
"failedToEmptyTrash": "Falha ao esvaziar lixeira: ${error}",
|
||||
"analyzing": "Analisando \"${title}\"...",
|
||||
"analysisStarted": "Análise iniciada para \"${title}\"",
|
||||
"failedToAnalyze": "Falha ao analisar biblioteca: ${error}",
|
||||
"noLibrariesFound": "Nenhuma biblioteca encontrada",
|
||||
"thisLibraryIsEmpty": "Esta biblioteca está vazia",
|
||||
"all": "Todos",
|
||||
"clearAll": "Limpar Tudo",
|
||||
"scanLibraryConfirm": "Tem certeza que deseja escanear \"${title}\"?",
|
||||
"analyzeLibraryConfirm": "Tem certeza que deseja analisar \"${title}\"?",
|
||||
"refreshMetadataConfirm": "Tem certeza que deseja atualizar os metadados de \"${title}\"?",
|
||||
"emptyTrashConfirm": "Tem certeza que deseja esvaziar a lixeira de \"${title}\"?",
|
||||
"manageLibraries": "Gerenciar Bibliotecas",
|
||||
"sort": "Ordenar",
|
||||
"sortBy": "Ordenar Por",
|
||||
"filters": "Filtros",
|
||||
"confirmActionMessage": "Tem certeza que deseja realizar esta ação?",
|
||||
"showLibrary": "Mostrar biblioteca",
|
||||
"hideLibrary": "Ocultar biblioteca",
|
||||
"libraryOptions": "Opções da biblioteca",
|
||||
"content": "conteúdo da biblioteca",
|
||||
"selectLibrary": "Selecionar biblioteca",
|
||||
"filtersWithCount": "Filtros (${count})",
|
||||
"noRecommendations": "Nenhuma recomendação disponível",
|
||||
"noCollections": "Nenhuma coleção nesta biblioteca",
|
||||
"noFoldersFound": "Nenhuma pasta encontrada",
|
||||
"folders": "pastas",
|
||||
"tabs": {
|
||||
"recommended": "Recomendados",
|
||||
"browse": "Navegar",
|
||||
"collections": "Coleções",
|
||||
"playlists": "Playlists"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Agrupamento",
|
||||
"all": "Todos",
|
||||
"movies": "Filmes",
|
||||
"shows": "Séries de TV",
|
||||
"seasons": "Temporadas",
|
||||
"episodes": "Episódios",
|
||||
"folders": "Pastas"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "Sobre",
|
||||
"openSourceLicenses": "Licenças Open Source",
|
||||
"versionLabel": "Versão ${version}",
|
||||
"appDescription": "Um belo cliente Plex para Flutter",
|
||||
"viewLicensesDescription": "Ver licenças de bibliotecas de terceiros"
|
||||
},
|
||||
"serverSelection": {
|
||||
"allServerConnectionsFailed": "Falha ao conectar a qualquer servidor. Verifique sua rede e tente novamente.",
|
||||
"noServersFoundForAccount": "Nenhum servidor encontrado para ${username} (${email})",
|
||||
"failedToLoadServers": "Falha ao carregar servidores: ${error}"
|
||||
},
|
||||
"hubDetail": {
|
||||
"title": "Título",
|
||||
"releaseYear": "Ano de Lançamento",
|
||||
"dateAdded": "Data de Adição",
|
||||
"rating": "Avaliação",
|
||||
"noItemsFound": "Nenhum item encontrado"
|
||||
},
|
||||
"logs": {
|
||||
"clearLogs": "Limpar Logs",
|
||||
"copyLogs": "Copiar Logs",
|
||||
"uploadLogs": "Enviar Logs"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Pacotes Relacionados",
|
||||
"license": "Licença",
|
||||
"licenseNumber": "Licença ${number}",
|
||||
"licensesCount": "${count} licenças"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Bibliotecas",
|
||||
"downloads": "Downloads",
|
||||
"liveTv": "TV ao Vivo"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "TV ao Vivo",
|
||||
"guide": "Guia",
|
||||
"noChannels": "Nenhum canal disponível",
|
||||
"noDvr": "Nenhum DVR configurado em nenhum servidor",
|
||||
"noPrograms": "Nenhum dado de programação disponível",
|
||||
"live": "AO VIVO",
|
||||
"reloadGuide": "Recarregar Guia",
|
||||
"now": "Agora",
|
||||
"today": "Hoje",
|
||||
"midnight": "Meia-noite",
|
||||
"overnight": "Madrugada",
|
||||
"morning": "Manhã",
|
||||
"daytime": "Dia",
|
||||
"evening": "Noite",
|
||||
"lateNight": "Madrugada",
|
||||
"whatsOn": "O que Está Passando",
|
||||
"watchChannel": "Assistir Canal",
|
||||
"favorites": "Favoritos",
|
||||
"reorderFavorites": "Reordenar favoritos",
|
||||
"joinSession": "Entrar na sessão em andamento",
|
||||
"watchFromStart": "Assistir do início (${minutes} min atrás)",
|
||||
"watchLive": "Assistir ao vivo",
|
||||
"goToLive": "Ir para o ao vivo"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Coleções",
|
||||
"collection": "Coleção",
|
||||
"empty": "A coleção está vazia",
|
||||
"unknownLibrarySection": "Não é possível excluir: Seção de biblioteca desconhecida",
|
||||
"deleteCollection": "Excluir Coleção",
|
||||
"deleteConfirm": "Tem certeza que deseja excluir \"${title}\"? Esta ação não pode ser desfeita.",
|
||||
"deleted": "Coleção excluída",
|
||||
"deleteFailed": "Falha ao excluir coleção",
|
||||
"deleteFailedWithError": "Falha ao excluir coleção: ${error}",
|
||||
"failedToLoadItems": "Falha ao carregar itens da coleção: ${error}",
|
||||
"selectCollection": "Selecionar Coleção",
|
||||
"collectionName": "Nome da Coleção",
|
||||
"enterCollectionName": "Insira o nome da coleção",
|
||||
"addedToCollection": "Adicionado à coleção",
|
||||
"errorAddingToCollection": "Falha ao adicionar à coleção",
|
||||
"created": "Coleção criada",
|
||||
"removeFromCollection": "Remover da coleção",
|
||||
"removeFromCollectionConfirm": "Remover \"${title}\" desta coleção?",
|
||||
"removedFromCollection": "Removido da coleção",
|
||||
"removeFromCollectionFailed": "Falha ao remover da coleção",
|
||||
"removeFromCollectionError": "Erro ao remover da coleção: ${error}",
|
||||
"searchCollections": "Pesquisar coleções..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Playlists",
|
||||
"playlist": "Playlist",
|
||||
"noPlaylists": "Nenhuma playlist encontrada",
|
||||
"create": "Criar Playlist",
|
||||
"playlistName": "Nome da Playlist",
|
||||
"enterPlaylistName": "Insira o nome da playlist",
|
||||
"delete": "Excluir Playlist",
|
||||
"removeItem": "Remover da Playlist",
|
||||
"smartPlaylist": "Playlist Inteligente",
|
||||
"itemCount": "${count} itens",
|
||||
"oneItem": "1 item",
|
||||
"emptyPlaylist": "Esta playlist está vazia",
|
||||
"deleteConfirm": "Excluir Playlist?",
|
||||
"deleteMessage": "Tem certeza que deseja excluir \"${name}\"?",
|
||||
"created": "Playlist criada",
|
||||
"deleted": "Playlist excluída",
|
||||
"itemAdded": "Adicionado à playlist",
|
||||
"itemRemoved": "Removido da playlist",
|
||||
"selectPlaylist": "Selecionar Playlist",
|
||||
"errorCreating": "Falha ao criar playlist",
|
||||
"errorDeleting": "Falha ao excluir playlist",
|
||||
"errorLoading": "Falha ao carregar playlists",
|
||||
"errorAdding": "Falha ao adicionar à playlist",
|
||||
"errorReordering": "Falha ao reordenar item da playlist",
|
||||
"errorRemoving": "Falha ao remover da playlist"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Assistir Juntos",
|
||||
"description": "Assista conteúdo sincronizado com amigos e família",
|
||||
"createSession": "Criar Sessão",
|
||||
"creating": "Criando...",
|
||||
"joinSession": "Entrar na Sessão",
|
||||
"joining": "Entrando...",
|
||||
"controlMode": "Modo de Controle",
|
||||
"controlModeQuestion": "Quem pode controlar a reprodução?",
|
||||
"hostOnly": "Apenas o Anfitrião",
|
||||
"anyone": "Qualquer pessoa",
|
||||
"hostingSession": "Hospedando Sessão",
|
||||
"inSession": "Em Sessão",
|
||||
"sessionCode": "Código da Sessão",
|
||||
"hostControlsPlayback": "Anfitrião controla a reprodução",
|
||||
"anyoneCanControl": "Qualquer pessoa pode controlar a reprodução",
|
||||
"hostControls": "Controle do anfitrião",
|
||||
"anyoneControls": "Controle de todos",
|
||||
"participants": "Participantes",
|
||||
"host": "Anfitrião",
|
||||
"hostBadge": "HOST",
|
||||
"youAreHost": "Você é o anfitrião",
|
||||
"watchingWithOthers": "Assistindo com outros",
|
||||
"endSession": "Encerrar Sessão",
|
||||
"leaveSession": "Sair da Sessão",
|
||||
"endSessionQuestion": "Encerrar Sessão?",
|
||||
"leaveSessionQuestion": "Sair da Sessão?",
|
||||
"endSessionConfirm": "Isso encerrará a sessão para todos os participantes.",
|
||||
"leaveSessionConfirm": "Você será removido da sessão.",
|
||||
"endSessionConfirmOverlay": "Isso encerrará a sessão de visualização para todos os participantes.",
|
||||
"leaveSessionConfirmOverlay": "Você será desconectado da sessão de visualização.",
|
||||
"end": "Encerrar",
|
||||
"leave": "Sair",
|
||||
"syncing": "Sincronizando...",
|
||||
"joinWatchSession": "Entrar na Sessão",
|
||||
"enterCodeHint": "Insira o código de 5 caracteres",
|
||||
"pasteFromClipboard": "Colar da área de transferência",
|
||||
"pleaseEnterCode": "Insira um código de sessão",
|
||||
"codeMustBe5Chars": "O código da sessão deve ter 5 caracteres",
|
||||
"joinInstructions": "Insira o código da sessão compartilhado pelo anfitrião para entrar na sessão.",
|
||||
"failedToCreate": "Falha ao criar sessão",
|
||||
"failedToJoin": "Falha ao entrar na sessão",
|
||||
"sessionCodeCopied": "Código da sessão copiado para a área de transferência",
|
||||
"relayUnreachable": "O servidor de retransmissão está inacessível. Isso pode ser causado pelo seu provedor bloqueando a conexão. Você ainda pode tentar, mas o Assistir Juntos pode não funcionar.",
|
||||
"reconnectingToHost": "Reconectando ao anfitrião...",
|
||||
"currentPlayback": "Reprodução Atual",
|
||||
"joinCurrentPlayback": "Entrar na Reprodução Atual",
|
||||
"joinCurrentPlaybackDescription": "Voltar ao que o anfitrião está assistindo agora",
|
||||
"failedToOpenCurrentPlayback": "Falha ao abrir reprodução atual",
|
||||
"participantJoined": "${name} entrou",
|
||||
"participantLeft": "${name} saiu",
|
||||
"participantPaused": "${name} pausou",
|
||||
"participantResumed": "${name} retomou",
|
||||
"participantSeeked": "${name} avançou",
|
||||
"participantBuffering": "${name} está carregando",
|
||||
"waitingForParticipants": "Aguardando outros carregarem...",
|
||||
"recentRooms": "Salas recentes",
|
||||
"renameRoom": "Renomear sala",
|
||||
"removeRoom": "Remover"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"manage": "Gerenciar",
|
||||
"tvShows": "Séries de TV",
|
||||
"movies": "Filmes",
|
||||
"noDownloads": "Nenhum download ainda",
|
||||
"noDownloadsDescription": "Conteúdo baixado aparecerá aqui para visualização offline",
|
||||
"downloadNow": "Baixar",
|
||||
"deleteDownload": "Excluir download",
|
||||
"retryDownload": "Tentar download novamente",
|
||||
"downloadQueued": "Download na fila",
|
||||
"serverErrorBitrate": "Erro do servidor — o arquivo pode exceder o limite de bitrate de streaming remoto",
|
||||
"episodesQueued": "${count} episódios na fila de download",
|
||||
"downloadDeleted": "Download excluído",
|
||||
"deleteConfirm": "Tem certeza que deseja excluir \"${title}\"? Isso removerá o arquivo baixado do seu dispositivo.",
|
||||
"deletingWithProgress": "Excluindo ${title}... (${current} de ${total})",
|
||||
"noDownloadsTree": "Nenhum download",
|
||||
"pauseAll": "Pausar todos",
|
||||
"resumeAll": "Retomar todos",
|
||||
"deleteAll": "Excluir todos",
|
||||
"selectVersion": "Selecionar versão",
|
||||
"allEpisodes": "Todos os episódios",
|
||||
"unwatchedOnly": "Apenas não assistidos",
|
||||
"nextNUnwatched": "Próximos ${count} não assistidos",
|
||||
"customAmount": "Quantidade personalizada...",
|
||||
"howManyEpisodes": "Quantos episódios?",
|
||||
"itemsQueued": "${count} itens na fila de download",
|
||||
"keepSynced": "Manter sincronizado",
|
||||
"downloadOnce": "Baixar uma vez",
|
||||
"keepNUnwatched": "Manter ${count} não assistidos",
|
||||
"editSyncRule": "Editar regra de sincronização",
|
||||
"removeSyncRule": "Remover regra de sincronização",
|
||||
"removeSyncRuleConfirm": "Parar de sincronizar \"${title}\"? Os episódios baixados serão mantidos.",
|
||||
"syncRuleCreated": "Regra de sincronização criada — mantendo ${count} episódios não assistidos",
|
||||
"syncRuleUpdated": "Regra de sincronização atualizada",
|
||||
"syncRuleRemoved": "Regra de sincronização removida",
|
||||
"syncedNewEpisodes": "${count} novos episódios sincronizados para ${title}",
|
||||
"activeSyncRules": "Regras de sincronização",
|
||||
"noSyncRules": "Nenhuma regra de sincronização",
|
||||
"manageSyncRule": "Gerenciar sincronização",
|
||||
"editEpisodeCount": "Número de episódios",
|
||||
"editSyncFilter": "Filtro de sincronização",
|
||||
"syncAllItems": "Sincronizando todos os itens",
|
||||
"syncUnwatchedItems": "Sincronizando itens não vistos",
|
||||
"syncRuleListCreated": "Regra de sincronização criada"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shaders",
|
||||
"noShaderDescription": "Sem aprimoramento de vídeo",
|
||||
"nvscalerDescription": "Escalonamento de imagem NVIDIA para vídeo mais nítido",
|
||||
"qualityFast": "Rápido",
|
||||
"qualityHQ": "Alta Qualidade",
|
||||
"mode": "Modo",
|
||||
"importShader": "Importar Shader",
|
||||
"customShaderDescription": "Shader GLSL personalizado",
|
||||
"shaderImported": "Shader importado",
|
||||
"shaderImportFailed": "Falha ao importar shader",
|
||||
"deleteShader": "Excluir Shader",
|
||||
"deleteShaderConfirm": "Excluir \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Controle Remoto",
|
||||
"connectedTo": "Conectado a ${name}",
|
||||
"session": {
|
||||
"startingServer": "A iniciar servidor remoto...",
|
||||
"failedToCreate": "Falha ao iniciar o servidor remoto:",
|
||||
"hostAddress": "Endereço do host",
|
||||
"connected": "Conectado",
|
||||
"serverRunning": "Servidor remoto ativo",
|
||||
"serverStopped": "Servidor remoto parado",
|
||||
"serverRunningDescription": "Dispositivos móveis na sua rede podem descobrir e conectar-se a esta aplicação",
|
||||
"serverStoppedDescription": "Inicie o servidor para permitir que dispositivos móveis se conectem",
|
||||
"usePhoneToControl": "Use o seu dispositivo móvel para controlar esta aplicação",
|
||||
"startServer": "Iniciar servidor",
|
||||
"stopServer": "Parar servidor",
|
||||
"minimize": "Minimizar"
|
||||
},
|
||||
"pairing": {
|
||||
"discoveryDescription": "Dispositivos na sua rede a executar Plezy com a mesma conta Plex aparecerão automaticamente",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"connecting": "A conectar...",
|
||||
"searchingForDevices": "A procurar dispositivos...",
|
||||
"noDevicesFound": "Nenhum dispositivo encontrado na sua rede",
|
||||
"noDevicesHint": "Certifique-se de que o Plezy está aberto no seu desktop e que ambos os dispositivos estão na mesma rede WiFi",
|
||||
"availableDevices": "Dispositivos disponíveis",
|
||||
"manualConnection": "Conexão manual",
|
||||
"cryptoInitFailed": "Não foi possível inicializar a conexão segura. Certifique-se de que está conectado a uma conta Plex.",
|
||||
"validationHostRequired": "Introduza o endereço do host",
|
||||
"validationHostFormat": "O formato deve ser IP:porta (ex. 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "Tempo de conexão esgotado. Certifique-se de que ambos os dispositivos estão na mesma rede.",
|
||||
"sessionNotFound": "Dispositivo não encontrado. Certifique-se de que o Plezy está em execução no host.",
|
||||
"authFailed": "Autenticação falhou. Certifique-se de que ambos os dispositivos usam a mesma conta Plex.",
|
||||
"failedToConnect": "Falha ao conectar: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
"disconnectConfirm": "Deseja desconectar da sessão remota?",
|
||||
"reconnecting": "Reconectando...",
|
||||
"attemptOf": "Tentativa ${current} de 5",
|
||||
"retryNow": "Tentar Agora",
|
||||
"tabRemote": "Remoto",
|
||||
"tabPlay": "Reproduzir",
|
||||
"tabMore": "Mais",
|
||||
"menu": "Menu",
|
||||
"tabNavigation": "Navegação",
|
||||
"tabDiscover": "Descobrir",
|
||||
"tabLibraries": "Bibliotecas",
|
||||
"tabSearch": "Buscar",
|
||||
"tabDownloads": "Downloads",
|
||||
"tabSettings": "Configurações",
|
||||
"previous": "Anterior",
|
||||
"playPause": "Reproduzir/Pausar",
|
||||
"next": "Próximo",
|
||||
"seekBack": "Retroceder",
|
||||
"stop": "Parar",
|
||||
"seekForward": "Avançar",
|
||||
"volume": "Volume",
|
||||
"volumeDown": "Diminuir",
|
||||
"volumeUp": "Aumentar",
|
||||
"fullscreen": "Tela Cheia",
|
||||
"subtitles": "Legendas",
|
||||
"audio": "Áudio",
|
||||
"searchHint": "Buscar no desktop..."
|
||||
}
|
||||
},
|
||||
"videoSettings": {
|
||||
"playbackSettings": "Configurações de Reprodução",
|
||||
"playbackSpeed": "Velocidade de Reprodução",
|
||||
"sleepTimer": "Timer de Sono",
|
||||
"audioSync": "Sincronia de Áudio",
|
||||
"subtitleSync": "Sincronia de Legendas",
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Saída de Áudio",
|
||||
"performanceOverlay": "Overlay de Desempenho",
|
||||
"audioPassthrough": "Passagem de Áudio",
|
||||
"audioNormalization": "Normalização de Áudio"
|
||||
},
|
||||
"externalPlayer": {
|
||||
"title": "Player Externo",
|
||||
"useExternalPlayer": "Usar Player Externo",
|
||||
"useExternalPlayerDescription": "Abrir vídeos em um app externo em vez do player integrado",
|
||||
"selectPlayer": "Selecionar Player",
|
||||
"customPlayers": "Players Personalizados",
|
||||
"systemDefault": "Padrão do Sistema",
|
||||
"addCustomPlayer": "Adicionar Player Personalizado",
|
||||
"playerName": "Nome do Player",
|
||||
"playerCommand": "Comando",
|
||||
"playerPackage": "Nome do Pacote",
|
||||
"playerUrlScheme": "Esquema de URL",
|
||||
"off": "Desativado",
|
||||
"launchFailed": "Falha ao abrir player externo",
|
||||
"appNotInstalled": "${name} não está instalado",
|
||||
"playInExternalPlayer": "Reproduzir no Player Externo"
|
||||
},
|
||||
"metadataEdit": {
|
||||
"editMetadata": "Editar...",
|
||||
"screenTitle": "Editar Metadados",
|
||||
"basicInfo": "Informações Básicas",
|
||||
"artwork": "Arte",
|
||||
"advancedSettings": "Configurações Avançadas",
|
||||
"title": "Título",
|
||||
"sortTitle": "Título para Ordenação",
|
||||
"originalTitle": "Título Original",
|
||||
"releaseDate": "Data de Lançamento",
|
||||
"contentRating": "Classificação Indicativa",
|
||||
"studio": "Estúdio",
|
||||
"tagline": "Tagline",
|
||||
"summary": "Sinopse",
|
||||
"poster": "Poster",
|
||||
"background": "Plano de Fundo",
|
||||
"logo": "Logo",
|
||||
"squareArt": "Imagem Quadrada",
|
||||
"selectPoster": "Selecionar Poster",
|
||||
"selectBackground": "Selecionar Plano de Fundo",
|
||||
"selectLogo": "Selecionar Logo",
|
||||
"selectSquareArt": "Selecionar Imagem Quadrada",
|
||||
"fromUrl": "Da URL",
|
||||
"uploadFile": "Enviar Arquivo",
|
||||
"enterImageUrl": "Insira a URL da imagem",
|
||||
"imageUrl": "URL da Imagem",
|
||||
"metadataUpdated": "Metadados atualizados",
|
||||
"metadataUpdateFailed": "Falha ao atualizar metadados",
|
||||
"artworkUpdated": "Arte atualizada",
|
||||
"artworkUpdateFailed": "Falha ao atualizar arte",
|
||||
"noArtworkAvailable": "Nenhuma arte disponível",
|
||||
"notSet": "Não definido",
|
||||
"libraryDefault": "Padrão da biblioteca",
|
||||
"accountDefault": "Padrão da conta",
|
||||
"seriesDefault": "Padrão da série",
|
||||
"episodeSorting": "Ordenação de Episódios",
|
||||
"oldestFirst": "Mais antigos primeiro",
|
||||
"newestFirst": "Mais recentes primeiro",
|
||||
"keep": "Manter",
|
||||
"allEpisodes": "Todos os episódios",
|
||||
"latestEpisodes": "${count} episódios mais recentes",
|
||||
"latestEpisode": "Episódio mais recente",
|
||||
"episodesAddedPastDays": "Episódios adicionados nos últimos ${count} dias",
|
||||
"deleteAfterPlaying": "Excluir Episódios Após Reproduzir",
|
||||
"never": "Nunca",
|
||||
"afterADay": "Após um dia",
|
||||
"afterAWeek": "Após uma semana",
|
||||
"afterAMonth": "Após um mês",
|
||||
"onNextRefresh": "Na próxima atualização",
|
||||
"seasons": "Temporadas",
|
||||
"show": "Mostrar",
|
||||
"hide": "Ocultar",
|
||||
"episodeOrdering": "Ordenação de Episódios",
|
||||
"tmdbAiring": "The Movie Database (Exibição)",
|
||||
"tvdbAiring": "TheTVDB (Exibição)",
|
||||
"tvdbAbsolute": "TheTVDB (Absoluto)",
|
||||
"metadataLanguage": "Idioma dos Metadados",
|
||||
"useOriginalTitle": "Usar Título Original",
|
||||
"preferredAudioLanguage": "Idioma de Áudio Preferido",
|
||||
"preferredSubtitleLanguage": "Idioma de Legenda Preferido",
|
||||
"subtitleMode": "Modo de Seleção Automática de Legendas",
|
||||
"manuallySelected": "Seleção manual",
|
||||
"shownWithForeignAudio": "Exibir com áudio estrangeiro",
|
||||
"alwaysEnabled": "Sempre ativado",
|
||||
"tags": "Tags",
|
||||
"addTag": "Adicionar tag",
|
||||
"genre": "Gênero",
|
||||
"director": "Diretor",
|
||||
"writer": "Roteirista",
|
||||
"producer": "Produtor",
|
||||
"country": "País",
|
||||
"collection": "Coleção",
|
||||
"label": "Rótulo",
|
||||
"style": "Estilo",
|
||||
"mood": "Humor"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Tarefas do servidor",
|
||||
"failedToLoad": "Falha ao carregar tarefas",
|
||||
"noTasks": "Nenhuma tarefa em execução"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Conectado",
|
||||
"connectedAs": "Conectado como @${username}",
|
||||
"disconnectConfirm": "Desconectar conta do Trakt?",
|
||||
"disconnectConfirmBody": "O Plezy deixará de enviar eventos de reprodução ao Trakt. Você pode reconectar a qualquer momento.",
|
||||
"scrobble": "Scrobbling em tempo real",
|
||||
"scrobbleDescription": "Envia eventos de reprodução, pausa e parada ao Trakt durante a exibição.",
|
||||
"watchedSync": "Sincronizar status de assistido",
|
||||
"watchedSyncDescription": "Ao marcar itens como assistidos no Plezy, eles também serão marcados no Trakt.",
|
||||
"deviceCodeTitle": "Ativar o Plezy no Trakt",
|
||||
"deviceCodeBody": "Acesse ${url} e digite este código:",
|
||||
"openTraktActivate": "Abrir o Trakt para ativar",
|
||||
"waitingForAuthorization": "Aguardando autorização…",
|
||||
"codeCopied": "Código copiado",
|
||||
"connectFailed": "Não foi possível conectar ao Trakt. Tente novamente."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Plezy"
|
||||
},
|
||||
"auth": {
|
||||
"signInWithPlex": "Войти через Plex",
|
||||
"showQRCode": "Показать QR-код",
|
||||
"authenticate": "Аутентификация",
|
||||
"authenticationTimeout": "Время аутентификации истекло. Попробуйте снова.",
|
||||
"scanQRToSignIn": "Отсканируйте QR-код для входа",
|
||||
"waitingForAuth": "Ожидание аутентификации...\nЗавершите вход в браузере.",
|
||||
"useBrowser": "Использовать браузер"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"close": "Закрыть",
|
||||
"clear": "Очистить",
|
||||
"reset": "Сбросить",
|
||||
"later": "Позже",
|
||||
"submit": "Отправить",
|
||||
"confirm": "Подтвердить",
|
||||
"retry": "Повторить",
|
||||
"logout": "Выйти",
|
||||
"unknown": "Неизвестно",
|
||||
"refresh": "Обновить",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"delete": "Удалить",
|
||||
"shuffle": "Перемешать",
|
||||
"addTo": "Добавить в...",
|
||||
"createNew": "Создать новый",
|
||||
"connect": "Подключить",
|
||||
"disconnect": "Отключить",
|
||||
"play": "Воспроизвести",
|
||||
"pause": "Пауза",
|
||||
"resume": "Продолжить",
|
||||
"error": "Ошибка",
|
||||
"search": "Поиск",
|
||||
"home": "Главная",
|
||||
"back": "Назад",
|
||||
"settings": "Настройки",
|
||||
"mute": "Без звука",
|
||||
"ok": "OK",
|
||||
"reconnect": "Переподключить",
|
||||
"exitConfirmTitle": "Выйти из приложения?",
|
||||
"exitConfirmMessage": "Вы уверены, что хотите выйти?",
|
||||
"dontAskAgain": "Больше не спрашивать",
|
||||
"exit": "Выход",
|
||||
"viewAll": "Показать все",
|
||||
"checkingNetwork": "Проверка сети...",
|
||||
"refreshingServers": "Обновление серверов...",
|
||||
"loadingServers": "Загрузка серверов...",
|
||||
"connectingToServers": "Подключение к серверам...",
|
||||
"startingOfflineMode": "Запуск автономного режима...",
|
||||
"loading": "Загрузка..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Лицензии",
|
||||
"switchProfile": "Сменить профиль",
|
||||
"subtitleStyling": "Стиль субтитров",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Логи"
|
||||
},
|
||||
"update": {
|
||||
"available": "Доступно обновление",
|
||||
"versionAvailable": "Доступна версия ${version}",
|
||||
"currentVersion": "Текущая: ${version}",
|
||||
"skipVersion": "Пропустить эту версию",
|
||||
"viewRelease": "Посмотреть релиз",
|
||||
"latestVersion": "У вас последняя версия",
|
||||
"checkFailed": "Не удалось проверить обновления"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
"supportDeveloper": "Поддержать Plezy",
|
||||
"supportDeveloperDescription": "Пожертвуйте через Liberapay на развитие",
|
||||
"language": "Язык",
|
||||
"theme": "Тема",
|
||||
"appearance": "Внешний вид",
|
||||
"videoPlayback": "Воспроизведение видео",
|
||||
"videoPlaybackDescription": "Настройка поведения воспроизведения",
|
||||
"advanced": "Дополнительно",
|
||||
"episodePosterMode": "Стиль постера эпизода",
|
||||
"seriesPoster": "Постер сериала",
|
||||
"seasonPoster": "Постер сезона",
|
||||
"episodeThumbnail": "Миниатюра",
|
||||
"showHeroSectionDescription": "Показывать карусель избранного контента на главном экране",
|
||||
"secondsLabel": "Секунды",
|
||||
"minutesLabel": "Минуты",
|
||||
"secondsShort": "с",
|
||||
"minutesShort": "м",
|
||||
"durationHint": "Введите длительность (${min}-${max})",
|
||||
"systemTheme": "Системная",
|
||||
"lightTheme": "Светлая",
|
||||
"darkTheme": "Тёмная",
|
||||
"oledTheme": "OLED",
|
||||
"libraryDensity": "Плотность библиотеки",
|
||||
"compact": "Компактный",
|
||||
"comfortable": "Комфортный",
|
||||
"viewMode": "Режим просмотра",
|
||||
"gridView": "Сетка",
|
||||
"listView": "Список",
|
||||
"showHeroSection": "Показать раздел избранного",
|
||||
"useGlobalHubs": "Использовать макет Plex Home",
|
||||
"useGlobalHubsDescription": "Показывать хабы главной страницы как в официальном клиенте Plex. При выключении показывает рекомендации по библиотекам.",
|
||||
"showServerNameOnHubs": "Показывать имя сервера в хабах",
|
||||
"showServerNameOnHubsDescription": "Всегда показывать имя сервера в заголовках хабов. При выключении показывает только для дублирующихся имён.",
|
||||
"alwaysKeepSidebarOpen": "Всегда держать боковую панель открытой",
|
||||
"alwaysKeepSidebarOpenDescription": "Боковая панель остаётся развёрнутой, область контента подстраивается",
|
||||
"showUnwatchedCount": "Показывать количество непросмотренных",
|
||||
"showUnwatchedCountDescription": "Отображать количество непросмотренных эпизодов для сериалов и сезонов",
|
||||
"hideSpoilers": "Скрыть спойлеры непросмотренных эпизодов",
|
||||
"hideSpoilersDescription": "Размывать миниатюры и скрывать описания эпизодов, которые вы ещё не смотрели",
|
||||
"playerBackend": "Бэкенд плеера",
|
||||
"exoPlayer": "ExoPlayer (Рекомендуется)",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Аппаратное декодирование",
|
||||
"hardwareDecodingDescription": "Использовать аппаратное ускорение, когда доступно",
|
||||
"bufferSize": "Размер буфера",
|
||||
"bufferSizeMB": "${size}МБ",
|
||||
"bufferSizeAuto": "Авто (Рекомендуется)",
|
||||
"bufferSizeWarning": "У вашего устройства ${heap}МБ памяти. Буфер ${size}МБ может вызвать проблемы с воспроизведением.",
|
||||
"subtitleStyling": "Стиль субтитров",
|
||||
"subtitleStylingDescription": "Настроить внешний вид субтитров",
|
||||
"smallSkipDuration": "Малая перемотка",
|
||||
"largeSkipDuration": "Большая перемотка",
|
||||
"rewindOnResume": "Перемотка при возобновлении",
|
||||
"secondsUnit": "${seconds} секунд",
|
||||
"defaultSleepTimer": "Таймер сна по умолчанию",
|
||||
"minutesUnit": "${minutes} минут",
|
||||
"rememberTrackSelections": "Запоминать выбор дорожек для каждого сериала/фильма",
|
||||
"rememberTrackSelectionsDescription": "Автоматически сохранять предпочтения языка аудио и субтитров при переключении дорожек во время воспроизведения",
|
||||
"clickVideoTogglesPlayback": "Клик по видео для переключения воспроизведения/паузы",
|
||||
"clickVideoTogglesPlaybackDescription": "Если включено, клик по видеоплееру воспроизводит/ставит на паузу. В противном случае показывает/скрывает элементы управления.",
|
||||
"videoPlayerControls": "Элементы управления плеером",
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
"keyboardShortcutsDescription": "Настроить горячие клавиши",
|
||||
"videoPlayerNavigation": "Навигация видеоплеера",
|
||||
"videoPlayerNavigationDescription": "Использовать клавиши стрелок для навигации по элементам управления плеером",
|
||||
"watchTogetherRelay": "Relay совместного просмотра",
|
||||
"watchTogetherRelayDescription": "Указать пользовательский relay-сервер для совместного просмотра. Все участники должны использовать один и тот же сервер.",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"crashReporting": "Отчёты об ошибках",
|
||||
"crashReportingDescription": "Отправлять отчёты об ошибках для улучшения приложения",
|
||||
"debugLogging": "Журнал отладки",
|
||||
"debugLoggingDescription": "Включить подробное журналирование для устранения неполадок",
|
||||
"viewLogs": "Просмотр логов",
|
||||
"viewLogsDescription": "Просмотр логов приложения",
|
||||
"clearCache": "Очистить кэш",
|
||||
"clearCacheDescription": "Это удалит все кэшированные изображения и данные. После очистки кэша приложение может загружать контент дольше.",
|
||||
"clearCacheSuccess": "Кэш успешно очищен",
|
||||
"resetSettings": "Сбросить настройки",
|
||||
"resetSettingsDescription": "Все настройки будут сброшены до значений по умолчанию. Это действие нельзя отменить.",
|
||||
"resetSettingsSuccess": "Настройки успешно сброшены",
|
||||
"shortcutsReset": "Горячие клавиши сброшены по умолчанию",
|
||||
"about": "О приложении",
|
||||
"aboutDescription": "Информация о приложении и лицензии",
|
||||
"updates": "Обновления",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"checkForUpdates": "Проверить обновления",
|
||||
"validationErrorEnterNumber": "Введите корректное число",
|
||||
"validationErrorDuration": "Длительность должна быть от ${min} до ${max} ${unit}",
|
||||
"shortcutAlreadyAssigned": "Клавиша уже назначена для ${action}",
|
||||
"shortcutUpdated": "Клавиша обновлена для ${action}",
|
||||
"autoSkip": "Автопропуск",
|
||||
"autoSkipIntro": "Автопропуск вступления",
|
||||
"autoSkipIntroDescription": "Автоматически пропускать маркеры вступления через несколько секунд",
|
||||
"autoSkipCredits": "Автопропуск титров",
|
||||
"autoSkipCreditsDescription": "Автоматически пропускать титры и воспроизводить следующий эпизод",
|
||||
"autoSkipDelay": "Задержка автопропуска",
|
||||
"autoSkipDelayDescription": "Подождать ${seconds} секунд перед автопропуском",
|
||||
"introPattern": "Шаблон маркера вступления",
|
||||
"introPatternDescription": "Регулярное выражение для распознавания маркеров вступления в заголовках глав",
|
||||
"creditsPattern": "Шаблон маркера титров",
|
||||
"creditsPatternDescription": "Регулярное выражение для распознавания маркеров титров в заголовках глав",
|
||||
"invalidRegex": "Недопустимое регулярное выражение",
|
||||
"downloads": "Загрузки",
|
||||
"downloadLocationDescription": "Выберите место для хранения загруженного контента",
|
||||
"downloadLocationDefault": "По умолчанию (Хранилище приложения)",
|
||||
"downloadLocationCustom": "Другое расположение",
|
||||
"selectFolder": "Выбрать папку",
|
||||
"resetToDefault": "Сбросить по умолчанию",
|
||||
"currentPath": "Текущий: ${path}",
|
||||
"downloadLocationChanged": "Место загрузки изменено",
|
||||
"downloadLocationReset": "Место загрузки сброшено по умолчанию",
|
||||
"downloadLocationInvalid": "Выбранная папка недоступна для записи",
|
||||
"downloadLocationSelectError": "Не удалось выбрать папку",
|
||||
"downloadOnWifiOnly": "Загружать только по WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Запретить загрузку по мобильным данным",
|
||||
"autoRemoveWatchedDownloads": "Автоудаление просмотренных загрузок",
|
||||
"autoRemoveWatchedDownloadsDescription": "Автоматически удалять загруженные эпизоды и фильмы после просмотра",
|
||||
"cellularDownloadBlocked": "Загрузка по мобильным данным отключена. Подключитесь к WiFi или измените настройку.",
|
||||
"maxVolume": "Максимальная громкость",
|
||||
"maxVolumeDescription": "Разрешить усиление громкости выше 100% для тихих медиа",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Показывать, что вы смотрите, в Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Синхронизировать историю просмотров с Trakt",
|
||||
"companionRemoteServer": "Сервер удалённого управления",
|
||||
"companionRemoteServerDescription": "Разрешить мобильным устройствам в сети управлять этим приложением",
|
||||
"autoPip": "Автоматический «картинка в картинке»",
|
||||
"autoPipDescription": "Автоматически переходить в режим «картинка в картинке» при выходе из приложения во время воспроизведения",
|
||||
"matchContentFrameRate": "Соответствие частоты кадров контента",
|
||||
"matchContentFrameRateDescription": "Настроить частоту обновления дисплея под видеоконтент, уменьшая дрожание и экономя батарею",
|
||||
"matchRefreshRate": "Соответствие частоты обновления",
|
||||
"matchRefreshRateDescription": "Переключать частоту обновления дисплея под видеоконтент в полноэкранном режиме",
|
||||
"matchDynamicRange": "Соответствие динамического диапазона",
|
||||
"matchDynamicRangeDescription": "Автоматически включать HDR для HDR-контента и возвращать SDR при выходе из плеера",
|
||||
"displaySwitchDelay": "Задержка переключения дисплея",
|
||||
"tunneledPlayback": "Туннельное воспроизведение",
|
||||
"tunneledPlaybackDescription": "Использовать аппаратный видеотуннелинг. Отключите, если видите чёрный экран со звуком при HDR-контенте",
|
||||
"requireProfileSelectionOnOpen": "Запрашивать профиль при запуске",
|
||||
"requireProfileSelectionOnOpenDescription": "Показывать выбор профиля при каждом открытии приложения",
|
||||
"confirmExitOnBack": "Подтверждать выход",
|
||||
"confirmExitOnBackDescription": "Показывать диалог подтверждения при нажатии «назад» для выхода из приложения",
|
||||
"forceTvMode": "Принудительный режим ТВ",
|
||||
"forceTvModeDescription": "Использовать макет ТВ независимо от автоматического определения. Полезно на устройствах Android TV, которые не сообщают о функции leanback. Приложение перезапустится при изменении.",
|
||||
"autoHidePerformanceOverlay": "Автоскрытие оверлея производительности",
|
||||
"autoHidePerformanceOverlayDescription": "Скрывать оверлей производительности вместе с элементами управления воспроизведением",
|
||||
"showNavBarLabels": "Показывать подписи панели навигации",
|
||||
"showNavBarLabelsDescription": "Отображать текстовые подписи под иконками панели навигации",
|
||||
"liveTvDefaultFavorites": "Избранные каналы по умолчанию",
|
||||
"liveTvDefaultFavoritesDescription": "Показывать только избранные каналы при открытии ТВ",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Поиск фильмов, сериалов, музыки...",
|
||||
"tryDifferentTerm": "Попробуйте другой запрос",
|
||||
"searchYourMedia": "Поиск в вашей медиатеке",
|
||||
"enterTitleActorOrKeyword": "Введите название, актёра или ключевое слово"
|
||||
},
|
||||
"hotkeys": {
|
||||
"setShortcutFor": "Назначить клавишу для ${actionName}",
|
||||
"clearShortcut": "Очистить клавишу",
|
||||
"actions": {
|
||||
"playPause": "Воспроизведение/Пауза",
|
||||
"volumeUp": "Громкость выше",
|
||||
"volumeDown": "Громкость ниже",
|
||||
"seekForward": "Перемотка вперёд (${seconds}с)",
|
||||
"seekBackward": "Перемотка назад (${seconds}с)",
|
||||
"fullscreenToggle": "Полноэкранный режим",
|
||||
"muteToggle": "Вкл./выкл. звук",
|
||||
"subtitleToggle": "Вкл./выкл. субтитры",
|
||||
"audioTrackNext": "Следующая аудиодорожка",
|
||||
"subtitleTrackNext": "Следующая дорожка субтитров",
|
||||
"chapterNext": "Следующая глава",
|
||||
"chapterPrevious": "Предыдущая глава",
|
||||
"speedIncrease": "Увеличить скорость",
|
||||
"speedDecrease": "Уменьшить скорость",
|
||||
"speedReset": "Сбросить скорость",
|
||||
"subSeekNext": "К следующему субтитру",
|
||||
"subSeekPrev": "К предыдущему субтитру",
|
||||
"shaderToggle": "Вкл./выкл. шейдеры",
|
||||
"skipMarker": "Пропустить вступление/титры"
|
||||
}
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Информация о файле",
|
||||
"video": "Видео",
|
||||
"audio": "Аудио",
|
||||
"file": "Файл",
|
||||
"advanced": "Дополнительно",
|
||||
"codec": "Кодек",
|
||||
"resolution": "Разрешение",
|
||||
"bitrate": "Битрейт",
|
||||
"frameRate": "Частота кадров",
|
||||
"aspectRatio": "Соотношение сторон",
|
||||
"profile": "Профиль",
|
||||
"bitDepth": "Глубина цвета",
|
||||
"colorSpace": "Цветовое пространство",
|
||||
"colorRange": "Цветовой диапазон",
|
||||
"colorPrimaries": "Цветовые первичные",
|
||||
"chromaSubsampling": "Субдискретизация цветности",
|
||||
"channels": "Каналы",
|
||||
"subtitles": "Субтитры",
|
||||
"overallBitrate": "Общий битрейт",
|
||||
"path": "Путь",
|
||||
"size": "Размер",
|
||||
"container": "Контейнер",
|
||||
"duration": "Длительность",
|
||||
"optimizedForStreaming": "Оптимизировано для стриминга",
|
||||
"has64bitOffsets": "64-битные смещения"
|
||||
},
|
||||
"mediaMenu": {
|
||||
"markAsWatched": "Отметить как просмотренное",
|
||||
"markAsUnwatched": "Отметить как непросмотренное",
|
||||
"removeFromContinueWatching": "Удалить из «Продолжить просмотр»",
|
||||
"goToSeries": "Перейти к сериалу",
|
||||
"goToSeason": "Перейти к сезону",
|
||||
"shufflePlay": "Случайное воспроизведение",
|
||||
"fileInfo": "Информация о файле",
|
||||
"deleteFromServer": "Удалить с сервера",
|
||||
"confirmDelete": "Это навсегда удалит этот медиафайл и его файлы с вашего сервера. Это действие нельзя отменить.",
|
||||
"deleteMultipleWarning": "Это включает все эпизоды и их файлы.",
|
||||
"mediaDeletedSuccessfully": "Медиаэлемент успешно удалён",
|
||||
"mediaFailedToDelete": "Не удалось удалить медиаэлемент",
|
||||
"rate": "Оценить",
|
||||
"playFromBeginning": "Воспроизвести сначала",
|
||||
"playVersion": "Воспроизвести версию..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, фильм",
|
||||
"mediaCardShow": "${title}, сериал",
|
||||
"mediaCardEpisode": "${title}, ${episodeInfo}",
|
||||
"mediaCardSeason": "${title}, ${seasonInfo}",
|
||||
"mediaCardWatched": "просмотрено",
|
||||
"mediaCardPartiallyWatched": "просмотрено ${percent} процентов",
|
||||
"mediaCardUnwatched": "не просмотрено",
|
||||
"tapToPlay": "Нажмите для воспроизведения"
|
||||
},
|
||||
"tooltips": {
|
||||
"shufflePlay": "Случайное воспроизведение",
|
||||
"playTrailer": "Воспроизвести трейлер",
|
||||
"markAsWatched": "Отметить как просмотренное",
|
||||
"markAsUnwatched": "Отметить как непросмотренное"
|
||||
},
|
||||
"videoControls": {
|
||||
"audioLabel": "Аудио",
|
||||
"subtitlesLabel": "Субтитры",
|
||||
"resetToZero": "Сбросить до 0мс",
|
||||
"addTime": "+${amount}${unit}",
|
||||
"minusTime": "-${amount}${unit}",
|
||||
"playsLater": "${label} воспроизводится позже",
|
||||
"playsEarlier": "${label} воспроизводится раньше",
|
||||
"noOffset": "Без смещения",
|
||||
"letterbox": "Letterbox",
|
||||
"fillScreen": "Заполнить экран",
|
||||
"stretch": "Растянуть",
|
||||
"lockRotation": "Заблокировать поворот",
|
||||
"unlockRotation": "Разблокировать поворот",
|
||||
"timerActive": "Таймер активен",
|
||||
"playbackWillPauseIn": "Воспроизведение будет приостановлено через ${duration}",
|
||||
"stillWatching": "Всё ещё смотрите?",
|
||||
"pausingIn": "Пауза через ${seconds}с",
|
||||
"continueWatching": "Продолжить",
|
||||
"autoPlayNext": "Автовоспроизведение следующего",
|
||||
"playNext": "Следующее",
|
||||
"playButton": "Воспроизвести",
|
||||
"pauseButton": "Пауза",
|
||||
"seekBackwardButton": "Перемотка назад на ${seconds} секунд",
|
||||
"seekForwardButton": "Перемотка вперёд на ${seconds} секунд",
|
||||
"previousButton": "Предыдущий эпизод",
|
||||
"nextButton": "Следующий эпизод",
|
||||
"previousChapterButton": "Предыдущая глава",
|
||||
"nextChapterButton": "Следующая глава",
|
||||
"muteButton": "Без звука",
|
||||
"unmuteButton": "Включить звук",
|
||||
"settingsButton": "Настройки видео",
|
||||
"tracksButton": "Аудио и субтитры",
|
||||
"chaptersButton": "Главы",
|
||||
"versionsButton": "Версии видео",
|
||||
"pipButton": "Режим «картинка в картинке»",
|
||||
"aspectRatioButton": "Соотношение сторон",
|
||||
"ambientLighting": "Фоновая подсветка",
|
||||
"fullscreenButton": "Полноэкранный режим",
|
||||
"exitFullscreenButton": "Выйти из полноэкранного режима",
|
||||
"alwaysOnTopButton": "Всегда поверх",
|
||||
"rotationLockButton": "Блокировка поворота",
|
||||
"lockScreen": "Заблокировать экран",
|
||||
"screenLockButton": "Блокировка экрана",
|
||||
"longPressToUnlock": "Удерживайте для разблокировки",
|
||||
"timelineSlider": "Временная шкала",
|
||||
"volumeSlider": "Уровень громкости",
|
||||
"endsAt": "Закончится в ${time}",
|
||||
"pipActive": "Воспроизводится в режиме «картинка в картинке»",
|
||||
"pipFailed": "Не удалось запустить режим «картинка в картинке»",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Требуется Android 8.0 или новее",
|
||||
"iosVersion": "Требуется iOS 15.0 или новее",
|
||||
"permissionDisabled": "Разрешение «картинка в картинке» отключено. Включите в Настройки > Приложения > Plezy > Картинка в картинке",
|
||||
"notSupported": "Устройство не поддерживает режим «картинка в картинке»",
|
||||
"voSwitchFailed": "Не удалось переключить видеовыход для «картинки в картинке»",
|
||||
"failed": "Не удалось запустить режим «картинка в картинке»",
|
||||
"unknown": "Произошла ошибка: ${error}"
|
||||
},
|
||||
"chapters": "Главы",
|
||||
"noChaptersAvailable": "Главы недоступны",
|
||||
"queue": "Очередь",
|
||||
"noQueueItems": "В очереди нет элементов",
|
||||
"searchSubtitles": "Поиск субтитров",
|
||||
"language": "Язык",
|
||||
"noSubtitlesFound": "Субтитры не найдены",
|
||||
"subtitleDownloaded": "Субтитры загружены",
|
||||
"subtitleDownloadFailed": "Не удалось загрузить субтитры",
|
||||
"searchLanguages": "Поиск языков..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Администратор",
|
||||
"restricted": "Ограниченный",
|
||||
"protected": "Защищённый",
|
||||
"current": "ТЕКУЩИЙ"
|
||||
},
|
||||
"messages": {
|
||||
"markedAsWatched": "Отмечено как просмотренное",
|
||||
"markedAsUnwatched": "Отмечено как непросмотренное",
|
||||
"markedAsWatchedOffline": "Отмечено как просмотренное (синхронизируется при подключении)",
|
||||
"markedAsUnwatchedOffline": "Отмечено как непросмотренное (синхронизируется при подключении)",
|
||||
"autoRemovedWatchedDownload": "Автоудалено: ${title}",
|
||||
"removedFromContinueWatching": "Удалено из «Продолжить просмотр»",
|
||||
"errorLoading": "Ошибка: ${error}",
|
||||
"fileInfoNotAvailable": "Информация о файле недоступна",
|
||||
"errorLoadingFileInfo": "Ошибка загрузки информации о файле: ${error}",
|
||||
"errorLoadingSeries": "Ошибка загрузки сериала",
|
||||
"errorLoadingSeason": "Ошибка загрузки сезона",
|
||||
"musicNotSupported": "Воспроизведение музыки пока не поддерживается",
|
||||
"logsCleared": "Логи очищены",
|
||||
"logsCopied": "Логи скопированы в буфер обмена",
|
||||
"noLogsAvailable": "Логи отсутствуют",
|
||||
"libraryScanning": "Сканирование \"${title}\"...",
|
||||
"libraryScanStarted": "Сканирование библиотеки начато для \"${title}\"",
|
||||
"libraryScanFailed": "Не удалось отсканировать библиотеку: ${error}",
|
||||
"metadataRefreshing": "Обновление метаданных \"${title}\"...",
|
||||
"metadataRefreshStarted": "Обновление метаданных начато для \"${title}\"",
|
||||
"metadataRefreshFailed": "Не удалось обновить метаданные: ${error}",
|
||||
"logoutConfirm": "Вы уверены, что хотите выйти?",
|
||||
"noSeasonsFound": "Сезоны не найдены",
|
||||
"noEpisodesFound": "Эпизоды в первом сезоне не найдены",
|
||||
"noEpisodesFoundGeneral": "Эпизоды не найдены",
|
||||
"noResultsFound": "Результаты не найдены",
|
||||
"sleepTimerSet": "Таймер сна установлен на ${label}",
|
||||
"noItemsAvailable": "Нет доступных элементов",
|
||||
"failedToCreatePlayQueueNoItems": "Не удалось создать очередь воспроизведения — нет элементов",
|
||||
"failedPlayback": "Не удалось ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Переключение на совместимый плеер...",
|
||||
"serverLimitTitle": "Ошибка воспроизведения",
|
||||
"serverLimitBody": "Сервер вернул ошибку (HTTP 500). Обычно это означает, что владелец сервера установил лимит пропускной способности или транскодирования, из-за которого ваш сеанс отклоняется. Со стороны клиента сделать ничего нельзя — владельцу сервера нужно изменить настройки.",
|
||||
"logsUploaded": "Логи загружены",
|
||||
"logsUploadFailed": "Не удалось загрузить логи",
|
||||
"logId": "ID лога"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"text": "Текст",
|
||||
"border": "Обводка",
|
||||
"background": "Фон",
|
||||
"fontSize": "Размер шрифта",
|
||||
"textColor": "Цвет текста",
|
||||
"borderSize": "Размер обводки",
|
||||
"borderColor": "Цвет обводки",
|
||||
"backgroundOpacity": "Прозрачность фона",
|
||||
"backgroundColor": "Цвет фона",
|
||||
"position": "Позиция",
|
||||
"assOverride": "Переопределение ASS",
|
||||
"bold": "Жирный",
|
||||
"italic": "Курсив"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "mpv.conf",
|
||||
"description": "Расширенные настройки видеоплеера",
|
||||
"presets": "Пресеты",
|
||||
"noPresets": "Нет сохранённых пресетов",
|
||||
"saveAsPreset": "Сохранить как пресет...",
|
||||
"presetName": "Название пресета",
|
||||
"presetNameHint": "Введите название для пресета",
|
||||
"loadPreset": "Загрузить",
|
||||
"deletePreset": "Удалить",
|
||||
"presetSaved": "Пресет сохранён",
|
||||
"presetLoaded": "Пресет загружен",
|
||||
"presetDeleted": "Пресет удалён",
|
||||
"confirmDeletePreset": "Вы уверены, что хотите удалить этот пресет?",
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Подтвердить действие"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Обзор",
|
||||
"switchProfile": "Сменить профиль",
|
||||
"noContentAvailable": "Контент недоступен",
|
||||
"addMediaToLibraries": "Добавьте медиафайлы в ваши библиотеки",
|
||||
"continueWatching": "Продолжить просмотр",
|
||||
"playEpisode": "S${season}E${episode}",
|
||||
"overview": "Обзор",
|
||||
"cast": "В ролях",
|
||||
"extras": "Трейлеры и доп. материалы",
|
||||
"studio": "Студия",
|
||||
"rating": "Рейтинг",
|
||||
"movie": "Фильм",
|
||||
"tvShow": "Сериал",
|
||||
"minutesLeft": "Осталось ${minutes} мин"
|
||||
},
|
||||
"errors": {
|
||||
"searchFailed": "Ошибка поиска: ${error}",
|
||||
"connectionTimeout": "Таймаут подключения при загрузке ${context}",
|
||||
"connectionFailed": "Не удаётся подключиться к серверу Plex",
|
||||
"failedToLoad": "Не удалось загрузить ${context}: ${error}",
|
||||
"noClientAvailable": "Клиент недоступен",
|
||||
"authenticationFailed": "Ошибка аутентификации: ${error}",
|
||||
"couldNotLaunchUrl": "Не удалось открыть URL аутентификации",
|
||||
"pleaseEnterToken": "Введите токен",
|
||||
"invalidToken": "Недействительный токен",
|
||||
"failedToVerifyToken": "Не удалось проверить токен: ${error}",
|
||||
"failedToSwitchProfile": "Не удалось переключиться на ${displayName}"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Библиотеки",
|
||||
"scanLibraryFiles": "Сканировать файлы библиотеки",
|
||||
"scanLibrary": "Сканировать библиотеку",
|
||||
"analyze": "Анализировать",
|
||||
"analyzeLibrary": "Анализировать библиотеку",
|
||||
"refreshMetadata": "Обновить метаданные",
|
||||
"emptyTrash": "Очистить корзину",
|
||||
"emptyingTrash": "Очистка корзины для \"${title}\"...",
|
||||
"trashEmptied": "Корзина очищена для \"${title}\"",
|
||||
"failedToEmptyTrash": "Не удалось очистить корзину: ${error}",
|
||||
"analyzing": "Анализ \"${title}\"...",
|
||||
"analysisStarted": "Анализ начат для \"${title}\"",
|
||||
"failedToAnalyze": "Не удалось проанализировать библиотеку: ${error}",
|
||||
"noLibrariesFound": "Библиотеки не найдены",
|
||||
"thisLibraryIsEmpty": "Эта библиотека пуста",
|
||||
"all": "Все",
|
||||
"clearAll": "Очистить все",
|
||||
"scanLibraryConfirm": "Вы уверены, что хотите сканировать \"${title}\"?",
|
||||
"analyzeLibraryConfirm": "Вы уверены, что хотите проанализировать \"${title}\"?",
|
||||
"refreshMetadataConfirm": "Вы уверены, что хотите обновить метаданные для \"${title}\"?",
|
||||
"emptyTrashConfirm": "Вы уверены, что хотите очистить корзину для \"${title}\"?",
|
||||
"manageLibraries": "Управление библиотеками",
|
||||
"sort": "Сортировка",
|
||||
"sortBy": "Сортировать по",
|
||||
"filters": "Фильтры",
|
||||
"confirmActionMessage": "Вы уверены, что хотите выполнить это действие?",
|
||||
"showLibrary": "Показать библиотеку",
|
||||
"hideLibrary": "Скрыть библиотеку",
|
||||
"libraryOptions": "Параметры библиотеки",
|
||||
"content": "содержимое библиотеки",
|
||||
"selectLibrary": "Выбрать библиотеку",
|
||||
"filtersWithCount": "Фильтры (${count})",
|
||||
"noRecommendations": "Рекомендации недоступны",
|
||||
"noCollections": "В этой библиотеке нет коллекций",
|
||||
"noFoldersFound": "Папки не найдены",
|
||||
"folders": "папки",
|
||||
"tabs": {
|
||||
"recommended": "Рекомендуемые",
|
||||
"browse": "Обзор",
|
||||
"collections": "Коллекции",
|
||||
"playlists": "Плейлисты"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Группировка",
|
||||
"all": "Все",
|
||||
"movies": "Фильмы",
|
||||
"shows": "Сериалы",
|
||||
"seasons": "Сезоны",
|
||||
"episodes": "Эпизоды",
|
||||
"folders": "Папки"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "О приложении",
|
||||
"openSourceLicenses": "Лицензии открытого ПО",
|
||||
"versionLabel": "Версия ${version}",
|
||||
"appDescription": "Красивый клиент Plex на Flutter",
|
||||
"viewLicensesDescription": "Просмотр лицензий сторонних библиотек"
|
||||
},
|
||||
"serverSelection": {
|
||||
"allServerConnectionsFailed": "Не удалось подключиться ни к одному серверу. Проверьте сеть и попробуйте снова.",
|
||||
"noServersFoundForAccount": "Серверы не найдены для ${username} (${email})",
|
||||
"failedToLoadServers": "Не удалось загрузить серверы: ${error}"
|
||||
},
|
||||
"hubDetail": {
|
||||
"title": "Название",
|
||||
"releaseYear": "Год выпуска",
|
||||
"dateAdded": "Дата добавления",
|
||||
"rating": "Рейтинг",
|
||||
"noItemsFound": "Элементы не найдены"
|
||||
},
|
||||
"logs": {
|
||||
"clearLogs": "Очистить логи",
|
||||
"copyLogs": "Скопировать логи",
|
||||
"uploadLogs": "Загрузить логи"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Связанные пакеты",
|
||||
"license": "Лицензия",
|
||||
"licenseNumber": "Лицензия ${number}",
|
||||
"licensesCount": "${count} лицензий"
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Библиотеки",
|
||||
"downloads": "Загрузки",
|
||||
"liveTv": "ТВ в прямом эфире"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "ТВ в прямом эфире",
|
||||
"guide": "Программа",
|
||||
"noChannels": "Нет доступных каналов",
|
||||
"noDvr": "DVR не настроен ни на одном сервере",
|
||||
"noPrograms": "Нет данных о программах",
|
||||
"live": "ЭФИР",
|
||||
"reloadGuide": "Перезагрузить программу",
|
||||
"now": "Сейчас",
|
||||
"today": "Сегодня",
|
||||
"midnight": "Полночь",
|
||||
"overnight": "Ночь",
|
||||
"morning": "Утро",
|
||||
"daytime": "День",
|
||||
"evening": "Вечер",
|
||||
"lateNight": "Поздний вечер",
|
||||
"whatsOn": "Что идёт",
|
||||
"watchChannel": "Смотреть канал",
|
||||
"favorites": "Избранное",
|
||||
"reorderFavorites": "Изменить порядок избранного",
|
||||
"joinSession": "Присоединиться к текущему сеансу",
|
||||
"watchFromStart": "Смотреть сначала (${minutes} мин. назад)",
|
||||
"watchLive": "Смотреть в прямом эфире",
|
||||
"goToLive": "К прямому эфиру"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Коллекции",
|
||||
"collection": "Коллекция",
|
||||
"empty": "Коллекция пуста",
|
||||
"unknownLibrarySection": "Невозможно удалить: неизвестный раздел библиотеки",
|
||||
"deleteCollection": "Удалить коллекцию",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить \"${title}\"? Это действие нельзя отменить.",
|
||||
"deleted": "Коллекция удалена",
|
||||
"deleteFailed": "Не удалось удалить коллекцию",
|
||||
"deleteFailedWithError": "Не удалось удалить коллекцию: ${error}",
|
||||
"failedToLoadItems": "Не удалось загрузить элементы коллекции: ${error}",
|
||||
"selectCollection": "Выбрать коллекцию",
|
||||
"collectionName": "Название коллекции",
|
||||
"enterCollectionName": "Введите название коллекции",
|
||||
"addedToCollection": "Добавлено в коллекцию",
|
||||
"errorAddingToCollection": "Не удалось добавить в коллекцию",
|
||||
"created": "Коллекция создана",
|
||||
"removeFromCollection": "Удалить из коллекции",
|
||||
"removeFromCollectionConfirm": "Удалить \"${title}\" из этой коллекции?",
|
||||
"removedFromCollection": "Удалено из коллекции",
|
||||
"removeFromCollectionFailed": "Не удалось удалить из коллекции",
|
||||
"removeFromCollectionError": "Ошибка удаления из коллекции: ${error}",
|
||||
"searchCollections": "Поиск коллекций..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Плейлисты",
|
||||
"playlist": "Плейлист",
|
||||
"noPlaylists": "Плейлисты не найдены",
|
||||
"create": "Создать плейлист",
|
||||
"playlistName": "Название плейлиста",
|
||||
"enterPlaylistName": "Введите название плейлиста",
|
||||
"delete": "Удалить плейлист",
|
||||
"removeItem": "Удалить из плейлиста",
|
||||
"smartPlaylist": "Умный плейлист",
|
||||
"itemCount": "${count} элементов",
|
||||
"oneItem": "1 элемент",
|
||||
"emptyPlaylist": "Этот плейлист пуст",
|
||||
"deleteConfirm": "Удалить плейлист?",
|
||||
"deleteMessage": "Вы уверены, что хотите удалить \"${name}\"?",
|
||||
"created": "Плейлист создан",
|
||||
"deleted": "Плейлист удалён",
|
||||
"itemAdded": "Добавлено в плейлист",
|
||||
"itemRemoved": "Удалено из плейлиста",
|
||||
"selectPlaylist": "Выбрать плейлист",
|
||||
"errorCreating": "Не удалось создать плейлист",
|
||||
"errorDeleting": "Не удалось удалить плейлист",
|
||||
"errorLoading": "Не удалось загрузить плейлисты",
|
||||
"errorAdding": "Не удалось добавить в плейлист",
|
||||
"errorReordering": "Не удалось переупорядочить элемент плейлиста",
|
||||
"errorRemoving": "Не удалось удалить из плейлиста"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Смотреть вместе",
|
||||
"description": "Смотрите контент синхронно с друзьями и семьёй",
|
||||
"createSession": "Создать сессию",
|
||||
"creating": "Создание...",
|
||||
"joinSession": "Присоединиться к сессии",
|
||||
"joining": "Подключение...",
|
||||
"controlMode": "Режим управления",
|
||||
"controlModeQuestion": "Кто может управлять воспроизведением?",
|
||||
"hostOnly": "Только хост",
|
||||
"anyone": "Все",
|
||||
"hostingSession": "Хостинг сессии",
|
||||
"inSession": "В сессии",
|
||||
"sessionCode": "Код сессии",
|
||||
"hostControlsPlayback": "Хост управляет воспроизведением",
|
||||
"anyoneCanControl": "Любой может управлять воспроизведением",
|
||||
"hostControls": "Управление хоста",
|
||||
"anyoneControls": "Управление для всех",
|
||||
"participants": "Участники",
|
||||
"host": "Хост",
|
||||
"hostBadge": "HOST",
|
||||
"youAreHost": "Вы — хост",
|
||||
"watchingWithOthers": "Смотрите с другими",
|
||||
"endSession": "Завершить сессию",
|
||||
"leaveSession": "Покинуть сессию",
|
||||
"endSessionQuestion": "Завершить сессию?",
|
||||
"leaveSessionQuestion": "Покинуть сессию?",
|
||||
"endSessionConfirm": "Это завершит сессию для всех участников.",
|
||||
"leaveSessionConfirm": "Вы будете удалены из сессии.",
|
||||
"endSessionConfirmOverlay": "Это завершит сеанс просмотра для всех участников.",
|
||||
"leaveSessionConfirmOverlay": "Вы будете отключены от сеанса просмотра.",
|
||||
"end": "Завершить",
|
||||
"leave": "Покинуть",
|
||||
"syncing": "Синхронизация...",
|
||||
"joinWatchSession": "Присоединиться к просмотру",
|
||||
"enterCodeHint": "Введите 5-символьный код",
|
||||
"pasteFromClipboard": "Вставить из буфера обмена",
|
||||
"pleaseEnterCode": "Введите код сессии",
|
||||
"codeMustBe5Chars": "Код сессии должен содержать 5 символов",
|
||||
"joinInstructions": "Введите код сессии, предоставленный хостом, чтобы присоединиться к просмотру.",
|
||||
"failedToCreate": "Не удалось создать сессию",
|
||||
"failedToJoin": "Не удалось присоединиться к сессии",
|
||||
"sessionCodeCopied": "Код сессии скопирован в буфер обмена",
|
||||
"relayUnreachable": "Ретранслятор недоступен. Возможно, ваш провайдер блокирует подключение. Вы можете попробовать, но «Смотреть вместе» может не работать.",
|
||||
"reconnectingToHost": "Переподключение к хосту...",
|
||||
"currentPlayback": "Текущее воспроизведение",
|
||||
"joinCurrentPlayback": "Присоединиться к текущему воспроизведению",
|
||||
"joinCurrentPlaybackDescription": "Вернуться к тому, что сейчас смотрит хост",
|
||||
"failedToOpenCurrentPlayback": "Не удалось открыть текущее воспроизведение",
|
||||
"participantJoined": "${name} присоединился",
|
||||
"participantLeft": "${name} вышел",
|
||||
"participantPaused": "${name} поставил на паузу",
|
||||
"participantResumed": "${name} возобновил",
|
||||
"participantSeeked": "${name} перемотал",
|
||||
"participantBuffering": "${name} буферизует",
|
||||
"waitingForParticipants": "Ожидание загрузки у других...",
|
||||
"recentRooms": "Недавние комнаты",
|
||||
"renameRoom": "Переименовать комнату",
|
||||
"removeRoom": "Удалить"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Загрузки",
|
||||
"manage": "Управление",
|
||||
"tvShows": "Сериалы",
|
||||
"movies": "Фильмы",
|
||||
"noDownloads": "Загрузок пока нет",
|
||||
"noDownloadsDescription": "Загруженный контент появится здесь для просмотра офлайн",
|
||||
"downloadNow": "Загрузить",
|
||||
"deleteDownload": "Удалить загрузку",
|
||||
"retryDownload": "Повторить загрузку",
|
||||
"downloadQueued": "Загрузка поставлена в очередь",
|
||||
"serverErrorBitrate": "Ошибка сервера — файл может превышать лимит битрейта удалённого стриминга",
|
||||
"episodesQueued": "${count} эпизодов поставлено в очередь загрузки",
|
||||
"downloadDeleted": "Загрузка удалена",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить \"${title}\"? Загруженный файл будет удалён с устройства.",
|
||||
"deletingWithProgress": "Удаление ${title}... (${current} из ${total})",
|
||||
"noDownloadsTree": "Нет загрузок",
|
||||
"pauseAll": "Приостановить все",
|
||||
"resumeAll": "Возобновить все",
|
||||
"deleteAll": "Удалить все",
|
||||
"selectVersion": "Выбрать версию",
|
||||
"allEpisodes": "Все эпизоды",
|
||||
"unwatchedOnly": "Только непросмотренные",
|
||||
"nextNUnwatched": "Следующие ${count} непросмотренных",
|
||||
"customAmount": "Указать количество...",
|
||||
"howManyEpisodes": "Сколько эпизодов?",
|
||||
"itemsQueued": "${count} элементов добавлено в очередь загрузки",
|
||||
"keepSynced": "Синхронизировать",
|
||||
"downloadOnce": "Скачать один раз",
|
||||
"keepNUnwatched": "Хранить ${count} непросмотренных",
|
||||
"editSyncRule": "Редактировать правило синхронизации",
|
||||
"removeSyncRule": "Удалить правило синхронизации",
|
||||
"removeSyncRuleConfirm": "Прекратить синхронизацию «${title}»? Скачанные эпизоды будут сохранены.",
|
||||
"syncRuleCreated": "Правило синхронизации создано — хранится ${count} непросмотренных эпизодов",
|
||||
"syncRuleUpdated": "Правило синхронизации обновлено",
|
||||
"syncRuleRemoved": "Правило синхронизации удалено",
|
||||
"syncedNewEpisodes": "Синхронизировано ${count} новых эпизодов для ${title}",
|
||||
"activeSyncRules": "Правила синхронизации",
|
||||
"noSyncRules": "Нет правил синхронизации",
|
||||
"manageSyncRule": "Управление синхронизацией",
|
||||
"editEpisodeCount": "Количество эпизодов",
|
||||
"editSyncFilter": "Фильтр синхронизации",
|
||||
"syncAllItems": "Синхронизация всех элементов",
|
||||
"syncUnwatchedItems": "Синхронизация непросмотренных элементов",
|
||||
"syncRuleListCreated": "Правило синхронизации создано"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Шейдеры",
|
||||
"noShaderDescription": "Без улучшения видео",
|
||||
"nvscalerDescription": "Масштабирование NVIDIA для более чёткого видео",
|
||||
"qualityFast": "Быстрый",
|
||||
"qualityHQ": "Высокое качество",
|
||||
"mode": "Режим",
|
||||
"importShader": "Импортировать шейдер",
|
||||
"customShaderDescription": "Пользовательский GLSL шейдер",
|
||||
"shaderImported": "Шейдер импортирован",
|
||||
"shaderImportFailed": "Не удалось импортировать шейдер",
|
||||
"deleteShader": "Удалить шейдер",
|
||||
"deleteShaderConfirm": "Удалить \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Пульт управления",
|
||||
"connectedTo": "Подключено к ${name}",
|
||||
"session": {
|
||||
"startingServer": "Запуск удалённого сервера...",
|
||||
"failedToCreate": "Не удалось запустить удалённый сервер:",
|
||||
"hostAddress": "Адрес хоста",
|
||||
"connected": "Подключено",
|
||||
"serverRunning": "Удалённый сервер активен",
|
||||
"serverStopped": "Удалённый сервер остановлен",
|
||||
"serverRunningDescription": "Мобильные устройства в вашей сети могут обнаруживать это приложение и подключаться к нему",
|
||||
"serverStoppedDescription": "Запустите сервер, чтобы разрешить подключение мобильных устройств",
|
||||
"usePhoneToControl": "Используйте мобильное устройство для управления этим приложением",
|
||||
"startServer": "Запустить сервер",
|
||||
"stopServer": "Остановить сервер",
|
||||
"minimize": "Свернуть"
|
||||
},
|
||||
"pairing": {
|
||||
"discoveryDescription": "Устройства в вашей сети с Plezy на том же аккаунте Plex появятся автоматически",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"connecting": "Подключение...",
|
||||
"searchingForDevices": "Поиск устройств...",
|
||||
"noDevicesFound": "Устройства в вашей сети не найдены",
|
||||
"noDevicesHint": "Убедитесь, что Plezy открыт на вашем компьютере и оба устройства находятся в одной сети WiFi",
|
||||
"availableDevices": "Доступные устройства",
|
||||
"manualConnection": "Ручное подключение",
|
||||
"cryptoInitFailed": "Не удалось инициализировать безопасное соединение. Убедитесь, что вы вошли в аккаунт Plex.",
|
||||
"validationHostRequired": "Введите адрес хоста",
|
||||
"validationHostFormat": "Формат должен быть IP:порт (например, 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "Время подключения истекло. Убедитесь, что оба устройства находятся в одной сети.",
|
||||
"sessionNotFound": "Устройство не найдено. Убедитесь, что Plezy запущен на хосте.",
|
||||
"authFailed": "Ошибка аутентификации. Убедитесь, что оба устройства используют один и тот же аккаунт Plex.",
|
||||
"failedToConnect": "Не удалось подключиться: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
"disconnectConfirm": "Отключиться от удалённой сессии?",
|
||||
"reconnecting": "Переподключение...",
|
||||
"attemptOf": "Попытка ${current} из 5",
|
||||
"retryNow": "Повторить сейчас",
|
||||
"tabRemote": "Пульт",
|
||||
"tabPlay": "Воспроизведение",
|
||||
"tabMore": "Ещё",
|
||||
"menu": "Меню",
|
||||
"tabNavigation": "Навигация",
|
||||
"tabDiscover": "Обзор",
|
||||
"tabLibraries": "Библиотеки",
|
||||
"tabSearch": "Поиск",
|
||||
"tabDownloads": "Загрузки",
|
||||
"tabSettings": "Настройки",
|
||||
"previous": "Предыдущий",
|
||||
"playPause": "Воспроизведение/Пауза",
|
||||
"next": "Следующий",
|
||||
"seekBack": "Назад",
|
||||
"stop": "Стоп",
|
||||
"seekForward": "Вперёд",
|
||||
"volume": "Громкость",
|
||||
"volumeDown": "Тише",
|
||||
"volumeUp": "Громче",
|
||||
"fullscreen": "Полноэкранный",
|
||||
"subtitles": "Субтитры",
|
||||
"audio": "Аудио",
|
||||
"searchHint": "Поиск на десктопе..."
|
||||
}
|
||||
},
|
||||
"videoSettings": {
|
||||
"playbackSettings": "Настройки воспроизведения",
|
||||
"playbackSpeed": "Скорость воспроизведения",
|
||||
"sleepTimer": "Таймер сна",
|
||||
"audioSync": "Синхронизация аудио",
|
||||
"subtitleSync": "Синхронизация субтитров",
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Аудиовыход",
|
||||
"performanceOverlay": "Оверлей производительности",
|
||||
"audioPassthrough": "Сквозной вывод аудио",
|
||||
"audioNormalization": "Нормализация аудио"
|
||||
},
|
||||
"externalPlayer": {
|
||||
"title": "Внешний плеер",
|
||||
"useExternalPlayer": "Использовать внешний плеер",
|
||||
"useExternalPlayerDescription": "Открывать видео во внешнем приложении вместо встроенного плеера",
|
||||
"selectPlayer": "Выбрать плеер",
|
||||
"customPlayers": "Свои плееры",
|
||||
"systemDefault": "Системный по умолчанию",
|
||||
"addCustomPlayer": "Добавить свой плеер",
|
||||
"playerName": "Название плеера",
|
||||
"playerCommand": "Команда",
|
||||
"playerPackage": "Имя пакета",
|
||||
"playerUrlScheme": "URL-схема",
|
||||
"off": "Выкл.",
|
||||
"launchFailed": "Не удалось открыть внешний плеер",
|
||||
"appNotInstalled": "${name} не установлен",
|
||||
"playInExternalPlayer": "Воспроизвести во внешнем плеере"
|
||||
},
|
||||
"metadataEdit": {
|
||||
"editMetadata": "Редактировать...",
|
||||
"screenTitle": "Редактировать метаданные",
|
||||
"basicInfo": "Основная информация",
|
||||
"artwork": "Обложка",
|
||||
"advancedSettings": "Дополнительные настройки",
|
||||
"title": "Название",
|
||||
"sortTitle": "Название для сортировки",
|
||||
"originalTitle": "Оригинальное название",
|
||||
"releaseDate": "Дата выпуска",
|
||||
"contentRating": "Возрастной рейтинг",
|
||||
"studio": "Студия",
|
||||
"tagline": "Слоган",
|
||||
"summary": "Описание",
|
||||
"poster": "Постер",
|
||||
"background": "Фон",
|
||||
"logo": "Логотип",
|
||||
"squareArt": "Квадратное изображение",
|
||||
"selectPoster": "Выбрать постер",
|
||||
"selectBackground": "Выбрать фон",
|
||||
"selectLogo": "Выбрать логотип",
|
||||
"selectSquareArt": "Выбрать квадратное изображение",
|
||||
"fromUrl": "По URL",
|
||||
"uploadFile": "Загрузить файл",
|
||||
"enterImageUrl": "Введите URL изображения",
|
||||
"imageUrl": "URL изображения",
|
||||
"metadataUpdated": "Метаданные обновлены",
|
||||
"metadataUpdateFailed": "Не удалось обновить метаданные",
|
||||
"artworkUpdated": "Обложка обновлена",
|
||||
"artworkUpdateFailed": "Не удалось обновить обложку",
|
||||
"noArtworkAvailable": "Обложки недоступны",
|
||||
"notSet": "Не задано",
|
||||
"libraryDefault": "По умолчанию библиотеки",
|
||||
"accountDefault": "По умолчанию аккаунта",
|
||||
"seriesDefault": "По умолчанию сериала",
|
||||
"episodeSorting": "Сортировка эпизодов",
|
||||
"oldestFirst": "Сначала старые",
|
||||
"newestFirst": "Сначала новые",
|
||||
"keep": "Сохранять",
|
||||
"allEpisodes": "Все эпизоды",
|
||||
"latestEpisodes": "${count} последних эпизодов",
|
||||
"latestEpisode": "Последний эпизод",
|
||||
"episodesAddedPastDays": "Эпизоды, добавленные за последние ${count} дней",
|
||||
"deleteAfterPlaying": "Удалять эпизоды после просмотра",
|
||||
"never": "Никогда",
|
||||
"afterADay": "Через день",
|
||||
"afterAWeek": "Через неделю",
|
||||
"afterAMonth": "Через месяц",
|
||||
"onNextRefresh": "При следующем обновлении",
|
||||
"seasons": "Сезоны",
|
||||
"show": "Показать",
|
||||
"hide": "Скрыть",
|
||||
"episodeOrdering": "Порядок эпизодов",
|
||||
"tmdbAiring": "The Movie Database (Эфирный)",
|
||||
"tvdbAiring": "TheTVDB (Эфирный)",
|
||||
"tvdbAbsolute": "TheTVDB (Абсолютный)",
|
||||
"metadataLanguage": "Язык метаданных",
|
||||
"useOriginalTitle": "Использовать оригинальное название",
|
||||
"preferredAudioLanguage": "Предпочитаемый язык аудио",
|
||||
"preferredSubtitleLanguage": "Предпочитаемый язык субтитров",
|
||||
"subtitleMode": "Автовыбор субтитров",
|
||||
"manuallySelected": "Выбор вручную",
|
||||
"shownWithForeignAudio": "Показывать при иноязычном аудио",
|
||||
"alwaysEnabled": "Всегда включены",
|
||||
"tags": "Теги",
|
||||
"addTag": "Добавить тег",
|
||||
"genre": "Жанр",
|
||||
"director": "Режиссёр",
|
||||
"writer": "Сценарист",
|
||||
"producer": "Продюсер",
|
||||
"country": "Страна",
|
||||
"collection": "Коллекция",
|
||||
"label": "Метка",
|
||||
"style": "Стиль",
|
||||
"mood": "Настроение"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Задачи сервера",
|
||||
"failedToLoad": "Не удалось загрузить задачи",
|
||||
"noTasks": "Нет выполняемых задач"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Подключено",
|
||||
"connectedAs": "Подключено как @${username}",
|
||||
"disconnectConfirm": "Отключить аккаунт Trakt?",
|
||||
"disconnectConfirmBody": "Plezy перестанет отправлять события воспроизведения в Trakt. Вы можете подключиться снова в любое время.",
|
||||
"scrobble": "Скробблинг в реальном времени",
|
||||
"scrobbleDescription": "Отправлять события воспроизведения, паузы и остановки в Trakt во время просмотра.",
|
||||
"watchedSync": "Синхронизация статуса просмотра",
|
||||
"watchedSyncDescription": "Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt.",
|
||||
"deviceCodeTitle": "Активация Plezy в Trakt",
|
||||
"deviceCodeBody": "Откройте ${url} и введите этот код:",
|
||||
"openTraktActivate": "Открыть Trakt для активации",
|
||||
"waitingForAuthorization": "Ожидание авторизации…",
|
||||
"codeCopied": "Код скопирован",
|
||||
"connectFailed": "Не удалось подключиться к Trakt. Попробуйте ещё раз."
|
||||
}
|
||||
}
|
||||
+93
-3
@@ -3,10 +3,10 @@
|
||||
/// Source: lib/i18n
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 9
|
||||
/// Strings: 7173 (797 per locale)
|
||||
/// Locales: 15
|
||||
/// Strings: 13260 (884 per locale)
|
||||
///
|
||||
/// Built on 2026-02-24 at 21:50 UTC
|
||||
/// Built on 2026-04-19 at 14:14 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
@@ -18,12 +18,18 @@ import 'package:slang/generated.dart';
|
||||
import 'package:slang_flutter/slang_flutter.dart';
|
||||
export 'package:slang_flutter/slang_flutter.dart';
|
||||
|
||||
import 'strings_da.g.dart' deferred as l_da;
|
||||
import 'strings_de.g.dart' deferred as l_de;
|
||||
import 'strings_es.g.dart' deferred as l_es;
|
||||
import 'strings_fr.g.dart' deferred as l_fr;
|
||||
import 'strings_it.g.dart' deferred as l_it;
|
||||
import 'strings_ja.g.dart' deferred as l_ja;
|
||||
import 'strings_ko.g.dart' deferred as l_ko;
|
||||
import 'strings_nb.g.dart' deferred as l_nb;
|
||||
import 'strings_nl.g.dart' deferred as l_nl;
|
||||
import 'strings_pl.g.dart' deferred as l_pl;
|
||||
import 'strings_pt.g.dart' deferred as l_pt;
|
||||
import 'strings_ru.g.dart' deferred as l_ru;
|
||||
import 'strings_sv.g.dart' deferred as l_sv;
|
||||
import 'strings_zh.g.dart' deferred as l_zh;
|
||||
part 'strings_en.g.dart';
|
||||
@@ -36,12 +42,18 @@ part 'strings_en.g.dart';
|
||||
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
|
||||
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||
en(languageCode: 'en'),
|
||||
da(languageCode: 'da'),
|
||||
de(languageCode: 'de'),
|
||||
es(languageCode: 'es'),
|
||||
fr(languageCode: 'fr'),
|
||||
it(languageCode: 'it'),
|
||||
ja(languageCode: 'ja'),
|
||||
ko(languageCode: 'ko'),
|
||||
nb(languageCode: 'nb'),
|
||||
nl(languageCode: 'nl'),
|
||||
pl(languageCode: 'pl'),
|
||||
pt(languageCode: 'pt'),
|
||||
ru(languageCode: 'ru'),
|
||||
sv(languageCode: 'sv'),
|
||||
zh(languageCode: 'zh');
|
||||
|
||||
@@ -68,6 +80,13 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.da:
|
||||
await l_da.loadLibrary();
|
||||
return l_da.TranslationsDa(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.de:
|
||||
await l_de.loadLibrary();
|
||||
return l_de.TranslationsDe(
|
||||
@@ -96,6 +115,13 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.ja:
|
||||
await l_ja.loadLibrary();
|
||||
return l_ja.TranslationsJa(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.ko:
|
||||
await l_ko.loadLibrary();
|
||||
return l_ko.TranslationsKo(
|
||||
@@ -103,6 +129,13 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.nb:
|
||||
await l_nb.loadLibrary();
|
||||
return l_nb.TranslationsNb(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.nl:
|
||||
await l_nl.loadLibrary();
|
||||
return l_nl.TranslationsNl(
|
||||
@@ -110,6 +143,27 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.pl:
|
||||
await l_pl.loadLibrary();
|
||||
return l_pl.TranslationsPl(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.pt:
|
||||
await l_pt.loadLibrary();
|
||||
return l_pt.TranslationsPt(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.ru:
|
||||
await l_ru.loadLibrary();
|
||||
return l_ru.TranslationsRu(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.sv:
|
||||
await l_sv.loadLibrary();
|
||||
return l_sv.TranslationsSv(
|
||||
@@ -140,6 +194,12 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.da:
|
||||
return l_da.TranslationsDa(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.de:
|
||||
return l_de.TranslationsDe(
|
||||
overrides: overrides,
|
||||
@@ -164,18 +224,48 @@ enum AppLocale with BaseAppLocale<AppLocale, Translations> {
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.ja:
|
||||
return l_ja.TranslationsJa(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.ko:
|
||||
return l_ko.TranslationsKo(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.nb:
|
||||
return l_nb.TranslationsNb(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.nl:
|
||||
return l_nl.TranslationsNl(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.pl:
|
||||
return l_pl.TranslationsPl(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.pt:
|
||||
return l_pt.TranslationsPt(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.ru:
|
||||
return l_ru.TranslationsRu(
|
||||
overrides: overrides,
|
||||
cardinalResolver: cardinalResolver,
|
||||
ordinalResolver: ordinalResolver,
|
||||
);
|
||||
case AppLocale.sv:
|
||||
return l_sv.TranslationsSv(
|
||||
overrides: overrides,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+513
-339
File diff suppressed because it is too large
Load Diff
+718
-385
File diff suppressed because it is too large
Load Diff
+412
-238
File diff suppressed because it is too large
Load Diff
+428
-254
File diff suppressed because it is too large
Load Diff
+511
-337
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+406
-232
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+515
-341
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+511
-337
File diff suppressed because it is too large
Load Diff
+511
-337
File diff suppressed because it is too large
Load Diff
+248
-165
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "Logga in med Plex",
|
||||
"showQRCode": "Visa QR-kod",
|
||||
"authenticate": "Autentisera",
|
||||
"debugEnterToken": "Debug: Ange Plex-token",
|
||||
"plexTokenLabel": "Plex-autentiseringstoken",
|
||||
"plexTokenHint": "Ange din Plex.tv-token",
|
||||
"authenticationTimeout": "Autentisering tog för lång tid. Försök igen.",
|
||||
"scanQRToSignIn": "Skanna QR-koden för att logga in",
|
||||
"waitingForAuth": "Väntar på autentisering...\nVänligen slutför inloggning i din webbläsare.",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "Ta bort",
|
||||
"shuffle": "Blanda",
|
||||
"addTo": "Lägg till i...",
|
||||
"remove": "Ta bort",
|
||||
"paste": "Klistra in",
|
||||
"createNew": "Skapa ny",
|
||||
"connect": "Anslut",
|
||||
"disconnect": "Koppla från",
|
||||
"play": "Spela",
|
||||
@@ -43,10 +39,9 @@
|
||||
"search": "Sök",
|
||||
"home": "Hem",
|
||||
"back": "Tillbaka",
|
||||
"settings": "Inställningar",
|
||||
"settings": "Mer",
|
||||
"mute": "Ljud av",
|
||||
"ok": "OK",
|
||||
"loading": "Laddar...",
|
||||
"reconnect": "Återanslut",
|
||||
"exitConfirmTitle": "Avsluta appen?",
|
||||
"exitConfirmMessage": "Är du säker på att du vill avsluta?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "Uppdaterar servrar...",
|
||||
"loadingServers": "Laddar servrar...",
|
||||
"connectingToServers": "Ansluter till servrar...",
|
||||
"startingOfflineMode": "Startar offlineläge..."
|
||||
"startingOfflineMode": "Startar offlineläge...",
|
||||
"loading": "Laddar..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "Licenser",
|
||||
"switchProfile": "Byt profil",
|
||||
"subtitleStyling": "Undertext-styling",
|
||||
"mpvConfig": "MPV-konfiguration",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "Loggar"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Inställningar",
|
||||
"supportDeveloper": "Stöd Plezy",
|
||||
"supportDeveloperDescription": "Donera via Liberapay för att finansiera utvecklingen",
|
||||
"language": "Språk",
|
||||
"theme": "Tema",
|
||||
"appearance": "Utseende",
|
||||
"videoPlayback": "Videouppspelning",
|
||||
"videoPlaybackDescription": "Konfigurera uppspelningsbeteende",
|
||||
"advanced": "Avancerat",
|
||||
"episodePosterMode": "Avsnittsaffisch-stil",
|
||||
"seriesPoster": "Serieaffisch",
|
||||
"seriesPosterDescription": "Visa seriens affisch för alla avsnitt",
|
||||
"seasonPoster": "Säsongsaffisch",
|
||||
"seasonPosterDescription": "Visa säsongens affisch för avsnitt",
|
||||
"episodeThumbnail": "Avsnittsminiatyr",
|
||||
"episodeThumbnailDescription": "Visa 16:9 skärmbild från avsnittet",
|
||||
"episodeThumbnail": "Miniatyr",
|
||||
"showHeroSectionDescription": "Visa utvalda innehållskarusell på startsidan",
|
||||
"secondsLabel": "Sekunder",
|
||||
"minutesLabel": "Minuter",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "Ange tid (${min}-${max})",
|
||||
"systemTheme": "System",
|
||||
"systemThemeDescription": "Följ systeminställningar",
|
||||
"lightTheme": "Ljust",
|
||||
"darkTheme": "Mörkt",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "Rent svart för OLED-skärmar",
|
||||
"libraryDensity": "Biblioteksdensitet",
|
||||
"compact": "Kompakt",
|
||||
"compactDescription": "Mindre kort, fler objekt synliga",
|
||||
"normal": "Normal",
|
||||
"normalDescription": "Standardstorlek",
|
||||
"comfortable": "Bekväm",
|
||||
"comfortableDescription": "Större kort, färre objekt synliga",
|
||||
"viewMode": "Visningsläge",
|
||||
"gridView": "Rutnät",
|
||||
"gridViewDescription": "Visa objekt i rutnätslayout",
|
||||
"listView": "Lista",
|
||||
"listViewDescription": "Visa objekt i listlayout",
|
||||
"showHeroSection": "Visa hjältesektion",
|
||||
"useGlobalHubs": "Använd Plex hem-layout",
|
||||
"useGlobalHubsDescription": "Visar startsidans hubbar som den officiella Plex-klienten. När av visas rekommendationer per bibliotek istället.",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "Gör miniatyrer suddiga och dölj beskrivningar för avsnitt du inte har sett ännu",
|
||||
"playerBackend": "Spelarmotor",
|
||||
"exoPlayer": "ExoPlayer (Rekommenderad)",
|
||||
"exoPlayerDescription": "Android-nativ spelare med bättre hårdvarustöd",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "Avancerad spelare med fler funktioner och ASS-undertextstöd",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "Hårdvaruavkodning",
|
||||
"hardwareDecodingDescription": "Använd hårdvaruacceleration när tillgängligt",
|
||||
"bufferSize": "Bufferstorlek",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "Anpassa undertextutseende",
|
||||
"smallSkipDuration": "Kort hoppvaraktighet",
|
||||
"largeSkipDuration": "Lång hoppvaraktighet",
|
||||
"rewindOnResume": "Spola tillbaka vid återupptagning",
|
||||
"secondsUnit": "${seconds} sekunder",
|
||||
"defaultSleepTimer": "Standard sovtimer",
|
||||
"minutesUnit": "${minutes} minuter",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "Anpassa tangentbordsgenvägar",
|
||||
"videoPlayerNavigation": "Navigering i videospelaren",
|
||||
"videoPlayerNavigationDescription": "Använd piltangenter för att navigera videospelarens kontroller",
|
||||
"watchTogetherRelay": "Titta Tillsammans-relay",
|
||||
"watchTogetherRelayDescription": "Ange en anpassad relay-server för Titta Tillsammans. Alla deltagare måste använda samma server.",
|
||||
"watchTogetherRelayHint": "https://min-relay.exempel.se",
|
||||
"crashReporting": "Kraschrapportering",
|
||||
"crashReportingDescription": "Skicka kraschrapporter för att förbättra appen",
|
||||
"debugLogging": "Felsökningsloggning",
|
||||
"debugLoggingDescription": "Aktivera detaljerad loggning för felsökning",
|
||||
"viewLogs": "Visa loggar",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "Hoppa automatiskt över credits och spela nästa avsnitt",
|
||||
"autoSkipDelay": "Fördröjning Auto Hoppa Över",
|
||||
"autoSkipDelayDescription": "Vänta ${seconds} sekunder innan automatisk överhoppning",
|
||||
"introPattern": "Intromarkörsmönster",
|
||||
"introPatternDescription": "Reguljärt uttryck för att matcha intromarkörer i kapiteltitlar",
|
||||
"creditsPattern": "Eftertextmarkörsmönster",
|
||||
"creditsPatternDescription": "Reguljärt uttryck för att matcha eftertextmarkörer i kapiteltitlar",
|
||||
"invalidRegex": "Ogiltigt reguljärt uttryck",
|
||||
"downloads": "Nedladdningar",
|
||||
"downloadLocationDescription": "Välj var nedladdat innehåll ska lagras",
|
||||
"downloadLocationDefault": "Standard (App-lagring)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "Kunde inte välja mapp",
|
||||
"downloadOnWifiOnly": "Ladda ner endast på WiFi",
|
||||
"downloadOnWifiOnlyDescription": "Förhindra nedladdningar vid användning av mobildata",
|
||||
"autoRemoveWatchedDownloads": "Ta bort sedda nedladdningar automatiskt",
|
||||
"autoRemoveWatchedDownloadsDescription": "Ta automatiskt bort nedladdade avsnitt och filmer när de markerats som sedda",
|
||||
"cellularDownloadBlocked": "Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen.",
|
||||
"maxVolume": "Maximal volym",
|
||||
"maxVolumeDescription": "Tillåt volym över 100% för tyst media",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord Rich Presence",
|
||||
"discordRichPresenceDescription": "Visa vad du tittar på i Discord",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "Synkronisera visningshistorik med Trakt",
|
||||
"companionRemoteServer": "Companion Remote-server",
|
||||
"companionRemoteServerDescription": "Tillåt mobila enheter i ditt nätverk att styra denna app",
|
||||
"autoPip": "Automatisk bild-i-bild",
|
||||
"autoPipDescription": "Aktivera bild-i-bild automatiskt när appen lämnas under uppspelning",
|
||||
"matchContentFrameRate": "Matcha innehållets bildfrekvens",
|
||||
"matchContentFrameRateDescription": "Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "Matcha uppdateringsfrekvens",
|
||||
"matchRefreshRateDescription": "Byt skärmens uppdateringsfrekvens för att matcha videoinnehåll i helskärm",
|
||||
"matchDynamicRange": "Matcha dynamiskt omfång",
|
||||
"matchDynamicRangeDescription": "Aktivera HDR automatiskt för HDR-innehåll och återgå till SDR när spelaren stängs",
|
||||
"displaySwitchDelay": "Fördröjning vid skärmbyte",
|
||||
"tunneledPlayback": "Tunneluppspelning",
|
||||
"tunneledPlaybackDescription": "Använd hårdvaruaccelererad videotunnling. Inaktivera om du ser en svart skärm med ljud vid HDR-innehåll",
|
||||
"requireProfileSelectionOnOpen": "Fråga efter profil vid appstart",
|
||||
"requireProfileSelectionOnOpenDescription": "Visa profilval varje gång appen öppnas",
|
||||
"confirmExitOnBack": "Bekräfta innan avslut",
|
||||
"confirmExitOnBackDescription": "Visa en bekräftelsedialog när du trycker tillbaka för att avsluta appen"
|
||||
"confirmExitOnBackDescription": "Visa en bekräftelsedialog när du trycker tillbaka för att avsluta appen",
|
||||
"forceTvMode": "Tvinga TV-läge",
|
||||
"forceTvModeDescription": "Använd TV-layouten oavsett automatisk identifiering. Användbart på Android TV-enheter som inte rapporterar leanback-funktionen. Startar om appen vid ändring.",
|
||||
"autoHidePerformanceOverlay": "Dölj prestandaöverlagring automatiskt",
|
||||
"autoHidePerformanceOverlayDescription": "Tona prestandaöverlagringen med uppspelningskontrollerna",
|
||||
"showNavBarLabels": "Visa navigeringsfältets etiketter",
|
||||
"showNavBarLabelsDescription": "Visa textetiketter under navigeringsfältets ikoner",
|
||||
"liveTvDefaultFavorites": "Standard till favoritkanaler",
|
||||
"liveTvDefaultFavoritesDescription": "Visa bara favoritkanaler när du öppnar Live TV",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Sök filmer, serier, musik...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "Hoppa över intro/eftertexter"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "Ange PIN",
|
||||
"showPin": "Visa PIN",
|
||||
"hidePin": "Dölj PIN"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "Filinformation",
|
||||
"video": "Video",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "Färggrunder",
|
||||
"chromaSubsampling": "Kroma-undersampling",
|
||||
"channels": "Kanaler",
|
||||
"subtitles": "Undertexter",
|
||||
"overallBitrate": "Total bithastighet",
|
||||
"path": "Sökväg",
|
||||
"size": "Storlek",
|
||||
"container": "Container",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "Detta inkluderar alla avsnitt och deras filer.",
|
||||
"mediaDeletedSuccessfully": "Mediaobjekt borttaget",
|
||||
"mediaFailedToDelete": "Kunde inte ta bort mediaobjekt",
|
||||
"rate": "Betygsätt"
|
||||
"rate": "Betygsätt",
|
||||
"playFromBeginning": "Spela från början",
|
||||
"playVersion": "Spela version..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, film",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "Lås upp rotation",
|
||||
"timerActive": "Timer aktiv",
|
||||
"playbackWillPauseIn": "Uppspelningen pausas om ${duration}",
|
||||
"sleepTimerCompleted": "Sovtimer slutförd - uppspelning pausad",
|
||||
"stillWatching": "Tittar du fortfarande?",
|
||||
"pausingIn": "Pausar om ${seconds}s",
|
||||
"continueWatching": "Fortsätt",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "Tysta",
|
||||
"unmuteButton": "Slå på ljud",
|
||||
"settingsButton": "Videoinställningar",
|
||||
"audioTrackButton": "Ljudspår",
|
||||
"subtitlesButton": "Undertexter",
|
||||
"tracksButton": "Ljud och undertexter",
|
||||
"chaptersButton": "Kapitel",
|
||||
"versionsButton": "Videoversioner",
|
||||
"pipButton": "Bild-i-bild läge",
|
||||
"aspectRatioButton": "Bildförhållande",
|
||||
"ambientLighting": "Ambientbelysning",
|
||||
"ambientLightingOn": "Aktivera ambientbelysning",
|
||||
"ambientLightingOff": "Inaktivera ambientbelysning",
|
||||
"fullscreenButton": "Aktivera helskärm",
|
||||
"exitFullscreenButton": "Avsluta helskärm",
|
||||
"alwaysOnTopButton": "Alltid överst",
|
||||
"rotationLockButton": "Rotationslås",
|
||||
"lockScreen": "Lås skärm",
|
||||
"screenLockButton": "Skärmlås",
|
||||
"longPressToUnlock": "Tryck länge för att låsa upp",
|
||||
"timelineSlider": "Videotidslinje",
|
||||
"volumeSlider": "Volymnivå",
|
||||
"endsAt": "Slutar ${time}",
|
||||
"pipActive": "Spelar i bild-i-bild",
|
||||
"pipFailed": "Bild-i-bild kunde inte starta",
|
||||
"pipErrors": {
|
||||
"androidVersion": "Kräver Android 8.0 eller nyare",
|
||||
"iosVersion": "Kräver iOS 15.0 eller nyare",
|
||||
"permissionDisabled": "Bild-i-bild-behörighet är inaktiverad. Aktivera den i Inställningar > Appar > Plezy > Bild-i-bild",
|
||||
"notSupported": "Denna enhet stöder inte bild-i-bild-läge",
|
||||
"voSwitchFailed": "Kunde inte byta videoutgång för bild-i-bild",
|
||||
"failed": "Bild-i-bild kunde inte starta",
|
||||
"unknown": "Ett fel uppstod: ${error}"
|
||||
},
|
||||
"chapters": "Kapitel",
|
||||
"noChaptersAvailable": "Inga kapitel tillgängliga",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "Kö",
|
||||
"noQueueItems": "Inga objekt i kön",
|
||||
"searchSubtitles": "Sök undertexter",
|
||||
"language": "Språk",
|
||||
"noSubtitlesFound": "Inga undertexter hittades",
|
||||
"subtitleDownloaded": "Undertext nedladdad",
|
||||
"subtitleDownloadFailed": "Kunde inte ladda ner undertext",
|
||||
"searchLanguages": "Sök språk..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "Markerad som osedd",
|
||||
"markedAsWatchedOffline": "Markerad som sedd (synkroniseras när online)",
|
||||
"markedAsUnwatchedOffline": "Markerad som osedd (synkroniseras när online)",
|
||||
"autoRemovedWatchedDownload": "Automatiskt borttagen: ${title}",
|
||||
"removedFromContinueWatching": "Borttagen från Fortsätt titta",
|
||||
"errorLoading": "Fel: ${error}",
|
||||
"fileInfoNotAvailable": "Filinformation inte tillgänglig",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "Det gick inte att skapa uppspelningskö – inga objekt",
|
||||
"failedPlayback": "Kunde inte ${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "Byter till kompatibel spelare...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "Uppspelningen misslyckades",
|
||||
"serverLimitBody": "Servern returnerade ett fel (HTTP 500). Det betyder oftast att serverägaren har satt en gräns för bandbredd eller transkodning som avvisar din session. Det finns inget du kan göra från klienten — serverägaren behöver justera sina inställningar.",
|
||||
"logsUploaded": "Loggar uppladdade",
|
||||
"logsUploadFailed": "Uppladdning av loggar misslyckades",
|
||||
"logId": "Logg-ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "Stilalternativ",
|
||||
"text": "Text",
|
||||
"border": "Kantlinje",
|
||||
"background": "Bakgrund",
|
||||
"fontSize": "Teckenstorlek",
|
||||
"textColor": "Textfärg",
|
||||
"borderSize": "Kantstorlek",
|
||||
"borderColor": "Kantfärg",
|
||||
"backgroundOpacity": "Bakgrundsopacitet",
|
||||
"backgroundColor": "Bakgrundsfärg",
|
||||
"position": "Position"
|
||||
"position": "Position",
|
||||
"assOverride": "ASS-åsidosättning",
|
||||
"bold": "Fet",
|
||||
"italic": "Kursiv"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV-konfiguration",
|
||||
"title": "mpv-konfiguration",
|
||||
"description": "Avancerade videospelares inställningar",
|
||||
"properties": "Egenskaper",
|
||||
"presets": "Förval",
|
||||
"noProperties": "Inga egenskaper konfigurerade",
|
||||
"noPresets": "Inga sparade förval",
|
||||
"addProperty": "Lägg till egenskap",
|
||||
"editProperty": "Redigera egenskap",
|
||||
"deleteProperty": "Ta bort egenskap",
|
||||
"propertyKey": "Egenskapsnyckel",
|
||||
"propertyKeyHint": "t.ex. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Egenskapsvärde",
|
||||
"propertyValueHint": "t.ex. auto, 256000000",
|
||||
"saveAsPreset": "Spara som förval...",
|
||||
"presetName": "Förvalnamn",
|
||||
"presetNameHint": "Ange ett namn för detta förval",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "Förval laddat",
|
||||
"presetDeleted": "Förval borttaget",
|
||||
"confirmDeletePreset": "Är du säker på att du vill ta bort detta förval?",
|
||||
"confirmDeleteProperty": "Är du säker på att du vill ta bort denna egenskap?",
|
||||
"entriesCount": "${count} poster"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Bekräfta åtgärd"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "Översikt",
|
||||
"cast": "Rollbesättning",
|
||||
"extras": "Trailers och Extra",
|
||||
"seasons": "Säsonger",
|
||||
"studio": "Studio",
|
||||
"rating": "Åldersgräns",
|
||||
"episodeCount": "${count} avsnitt",
|
||||
"watchedProgress": "${watched}/${total} sedda",
|
||||
"movie": "Film",
|
||||
"tvShow": "TV-serie",
|
||||
"minutesLeft": "${minutes} min kvar"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "Spellistor"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "Gruppering",
|
||||
"all": "Alla",
|
||||
"movies": "Filmer",
|
||||
"shows": "Serier",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "Rensa loggar",
|
||||
"copyLogs": "Kopiera loggar",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "Fel:",
|
||||
"stackTrace": "Stack trace:"
|
||||
"uploadLogs": "Ladda upp loggar"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "Relaterade paket",
|
||||
@@ -560,25 +585,17 @@
|
||||
},
|
||||
"navigation": {
|
||||
"libraries": "Bibliotek",
|
||||
"downloads": "Nedladdningar",
|
||||
"downloads": "Nerladdat",
|
||||
"liveTv": "Live-TV"
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "Live-TV",
|
||||
"channels": "Kanaler",
|
||||
"guide": "Programguide",
|
||||
"noChannels": "Inga kanaler tillgängliga",
|
||||
"noDvr": "Ingen DVR konfigurerad på någon server",
|
||||
"tuneFailed": "Kunde inte ställa in kanalen",
|
||||
"loading": "Laddar kanaler...",
|
||||
"nowPlaying": "Spelas nu",
|
||||
"noPrograms": "Ingen programdata tillgänglig",
|
||||
"channelNumber": "Kanal ${number}",
|
||||
"live": "LIVE",
|
||||
"hd": "HD",
|
||||
"premiere": "NY",
|
||||
"reloadGuide": "Ladda om programguide",
|
||||
"allChannels": "Alla kanaler",
|
||||
"now": "Nu",
|
||||
"today": "Idag",
|
||||
"midnight": "Midnatt",
|
||||
@@ -588,30 +605,41 @@
|
||||
"evening": "Kväll",
|
||||
"lateNight": "Sen kväll",
|
||||
"whatsOn": "På TV nu",
|
||||
"watchChannel": "Titta på kanal"
|
||||
"watchChannel": "Titta på kanal",
|
||||
"favorites": "Favoriter",
|
||||
"reorderFavorites": "Ordna om favoriter",
|
||||
"joinSession": "Gå med i pågående session",
|
||||
"watchFromStart": "Titta från början (${minutes} min sedan)",
|
||||
"watchLive": "Titta live",
|
||||
"goToLive": "Gå till live"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Nedladdningar",
|
||||
"manage": "Hantera",
|
||||
"tvShows": "TV-serier",
|
||||
"movies": "Filmer",
|
||||
"noDownloads": "Inga nedladdningar ännu",
|
||||
"noDownloadsDescription": "Nedladdat innehåll visas här för offline-visning",
|
||||
"downloadNow": "Ladda ner",
|
||||
"deleteDownload": "Ta bort nedladdning",
|
||||
"retryDownload": "Försök igen",
|
||||
"downloadQueued": "Nedladdning köad",
|
||||
"episodesQueued": "${count} avsnitt köade för nedladdning",
|
||||
"downloadDeleted": "Nedladdning borttagen",
|
||||
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Den nedladdade filen kommer att tas bort från din enhet.",
|
||||
"deletingWithProgress": "Tar bort ${title}... (${current} av ${total})",
|
||||
"noDownloadsTree": "Inga nedladdningar",
|
||||
"pauseAll": "Pausa alla",
|
||||
"resumeAll": "Återuppta alla",
|
||||
"deleteAll": "Ta bort alla"
|
||||
"collections": {
|
||||
"title": "Samlingar",
|
||||
"collection": "Samling",
|
||||
"empty": "Samlingen är tom",
|
||||
"unknownLibrarySection": "Kan inte ta bort: okänd bibliotekssektion",
|
||||
"deleteCollection": "Ta bort samling",
|
||||
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Detta går inte att ångra.",
|
||||
"deleted": "Samling borttagen",
|
||||
"deleteFailed": "Det gick inte att ta bort samlingen",
|
||||
"deleteFailedWithError": "Det gick inte att ta bort samlingen: ${error}",
|
||||
"failedToLoadItems": "Det gick inte att läsa in samlingsobjekt: ${error}",
|
||||
"selectCollection": "Välj samling",
|
||||
"collectionName": "Samlingsnamn",
|
||||
"enterCollectionName": "Ange samlingsnamn",
|
||||
"addedToCollection": "Tillagd i samling",
|
||||
"errorAddingToCollection": "Fel vid tillägg i samling",
|
||||
"created": "Samling skapad",
|
||||
"removeFromCollection": "Ta bort från samling",
|
||||
"removeFromCollectionConfirm": "Ta bort \"${title}\" från denna samling?",
|
||||
"removedFromCollection": "Borttagen från samling",
|
||||
"removeFromCollectionFailed": "Misslyckades med att ta bort från samling",
|
||||
"removeFromCollectionError": "Fel vid borttagning från samling: ${error}",
|
||||
"searchCollections": "Sök samlingar..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "Spellistor",
|
||||
"playlist": "Spellista",
|
||||
"noPlaylists": "Inga spellistor hittades",
|
||||
"create": "Skapa spellista",
|
||||
"playlistName": "Spellistans namn",
|
||||
@@ -629,38 +657,12 @@
|
||||
"itemAdded": "Tillagd i spellista",
|
||||
"itemRemoved": "Borttagen från spellista",
|
||||
"selectPlaylist": "Välj spellista",
|
||||
"createNewPlaylist": "Skapa ny spellista",
|
||||
"errorCreating": "Det gick inte att skapa spellista",
|
||||
"errorDeleting": "Det gick inte att ta bort spellista",
|
||||
"errorLoading": "Det gick inte att ladda spellistor",
|
||||
"errorAdding": "Det gick inte att lägga till i spellista",
|
||||
"errorReordering": "Det gick inte att omordna spellisteobjekt",
|
||||
"errorRemoving": "Det gick inte att ta bort från spellista",
|
||||
"playlist": "Spellista"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Samlingar",
|
||||
"collection": "Samling",
|
||||
"empty": "Samlingen är tom",
|
||||
"unknownLibrarySection": "Kan inte ta bort: okänd bibliotekssektion",
|
||||
"deleteCollection": "Ta bort samling",
|
||||
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Detta går inte att ångra.",
|
||||
"deleted": "Samling borttagen",
|
||||
"deleteFailed": "Det gick inte att ta bort samlingen",
|
||||
"deleteFailedWithError": "Det gick inte att ta bort samlingen: ${error}",
|
||||
"failedToLoadItems": "Det gick inte att läsa in samlingsobjekt: ${error}",
|
||||
"selectCollection": "Välj samling",
|
||||
"createNewCollection": "Skapa ny samling",
|
||||
"collectionName": "Samlingsnamn",
|
||||
"enterCollectionName": "Ange samlingsnamn",
|
||||
"addedToCollection": "Tillagd i samling",
|
||||
"errorAddingToCollection": "Fel vid tillägg i samling",
|
||||
"created": "Samling skapad",
|
||||
"removeFromCollection": "Ta bort från samling",
|
||||
"removeFromCollectionConfirm": "Ta bort \"${title}\" från denna samling?",
|
||||
"removedFromCollection": "Borttagen från samling",
|
||||
"removeFromCollectionFailed": "Misslyckades med att ta bort från samling",
|
||||
"removeFromCollectionError": "Fel vid borttagning från samling: ${error}"
|
||||
"errorRemoving": "Det gick inte att ta bort från spellista"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "Titta Tillsammans",
|
||||
@@ -697,18 +699,76 @@
|
||||
"leave": "Lämna",
|
||||
"syncing": "Synkroniserar...",
|
||||
"joinWatchSession": "Gå med i Tittarsession",
|
||||
"enterCodeHint": "Ange 8-teckens kod",
|
||||
"enterCodeHint": "Ange 5-teckens kod",
|
||||
"pasteFromClipboard": "Klistra in från urklipp",
|
||||
"pleaseEnterCode": "Vänligen ange en sessionskod",
|
||||
"codeMustBe8Chars": "Sessionskod måste vara 8 tecken",
|
||||
"codeMustBe5Chars": "Sessionskod måste vara 5 tecken",
|
||||
"joinInstructions": "Ange sessionskoden som delats av värden för att gå med i deras tittarsession.",
|
||||
"failedToCreate": "Det gick inte att skapa session",
|
||||
"failedToJoin": "Det gick inte att gå med i session",
|
||||
"sessionCodeCopied": "Sessionskod kopierad till urklipp",
|
||||
"relayUnreachable": "Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar.",
|
||||
"reconnectingToHost": "Återansluter till värd...",
|
||||
"currentPlayback": "Aktuell uppspelning",
|
||||
"joinCurrentPlayback": "Gå med i aktuell uppspelning",
|
||||
"joinCurrentPlaybackDescription": "Hoppa tillbaka till det värden tittar på just nu",
|
||||
"failedToOpenCurrentPlayback": "Kunde inte öppna aktuell uppspelning",
|
||||
"participantJoined": "${name} gick med",
|
||||
"participantLeft": "${name} lämnade"
|
||||
"participantLeft": "${name} lämnade",
|
||||
"participantPaused": "${name} pausade",
|
||||
"participantResumed": "${name} återupptog",
|
||||
"participantSeeked": "${name} spolade",
|
||||
"participantBuffering": "${name} buffrar",
|
||||
"waitingForParticipants": "Väntar på att andra laddar...",
|
||||
"recentRooms": "Senaste rum",
|
||||
"renameRoom": "Byt namn på rum",
|
||||
"removeRoom": "Ta bort"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Nedladdningar",
|
||||
"manage": "Hantera",
|
||||
"tvShows": "TV-serier",
|
||||
"movies": "Filmer",
|
||||
"noDownloads": "Inga nedladdningar ännu",
|
||||
"noDownloadsDescription": "Nedladdat innehåll visas här för offline-visning",
|
||||
"downloadNow": "Ladda ner",
|
||||
"deleteDownload": "Ta bort nedladdning",
|
||||
"retryDownload": "Försök igen",
|
||||
"downloadQueued": "Nedladdning köad",
|
||||
"serverErrorBitrate": "Serverfel — filen överskrider möjligen gränsen för fjärrströmning-bitrate",
|
||||
"episodesQueued": "${count} avsnitt köade för nedladdning",
|
||||
"downloadDeleted": "Nedladdning borttagen",
|
||||
"deleteConfirm": "Är du säker på att du vill ta bort \"${title}\"? Den nedladdade filen kommer att tas bort från din enhet.",
|
||||
"deletingWithProgress": "Tar bort ${title}... (${current} av ${total})",
|
||||
"noDownloadsTree": "Inga nedladdningar",
|
||||
"pauseAll": "Pausa alla",
|
||||
"resumeAll": "Återuppta alla",
|
||||
"deleteAll": "Ta bort alla",
|
||||
"selectVersion": "Välj version",
|
||||
"allEpisodes": "Alla avsnitt",
|
||||
"unwatchedOnly": "Endast osedda",
|
||||
"nextNUnwatched": "Nästa ${count} osedda",
|
||||
"customAmount": "Ange antal...",
|
||||
"howManyEpisodes": "Hur många avsnitt?",
|
||||
"itemsQueued": "${count} objekt köade för nedladdning",
|
||||
"keepSynced": "Håll synkroniserad",
|
||||
"downloadOnce": "Ladda ner en gång",
|
||||
"keepNUnwatched": "Behåll ${count} osedda",
|
||||
"editSyncRule": "Redigera synkregel",
|
||||
"removeSyncRule": "Ta bort synkregel",
|
||||
"removeSyncRuleConfirm": "Sluta synkronisera \"${title}\"? Nedladdade avsnitt behålls.",
|
||||
"syncRuleCreated": "Synkregel skapad — behåller ${count} osedda avsnitt",
|
||||
"syncRuleUpdated": "Synkregel uppdaterad",
|
||||
"syncRuleRemoved": "Synkregel borttagen",
|
||||
"syncedNewEpisodes": "Synkroniserade ${count} nya avsnitt för ${title}",
|
||||
"activeSyncRules": "Synkregler",
|
||||
"noSyncRules": "Inga synkregler",
|
||||
"manageSyncRule": "Hantera synkronisering",
|
||||
"editEpisodeCount": "Antal avsnitt",
|
||||
"editSyncFilter": "Synkroniseringsfilter",
|
||||
"syncAllItems": "Synkroniserar alla objekt",
|
||||
"syncUnwatchedItems": "Synkroniserar osedda objekt",
|
||||
"syncRuleListCreated": "Synkroniseringsregel skapad"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "Shaders",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "NVIDIA-bildskalning för skarpare video",
|
||||
"qualityFast": "Snabb",
|
||||
"qualityHQ": "Hög kvalitet",
|
||||
"mode": "Läge"
|
||||
"mode": "Läge",
|
||||
"importShader": "Importera shader",
|
||||
"customShaderDescription": "Anpassad GLSL-shader",
|
||||
"shaderImported": "Shader importerad",
|
||||
"shaderImportFailed": "Kunde inte importera shader",
|
||||
"deleteShader": "Ta bort shader",
|
||||
"deleteShaderConfirm": "Ta bort \"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "Anslut till enhet",
|
||||
"hostRemoteSession": "Starta fjärrsession",
|
||||
"controlThisDevice": "Styr den här enheten med din telefon",
|
||||
"remoteControl": "Fjärrkontroll",
|
||||
"controlDesktop": "Styr en datorenhet",
|
||||
"title": "Fjärrkontroll",
|
||||
"connectedTo": "Ansluten till ${name}",
|
||||
"session": {
|
||||
"creatingSession": "Skapar fjärrsession...",
|
||||
"failedToCreate": "Kunde inte skapa fjärrsession:",
|
||||
"noSession": "Ingen session tillgänglig",
|
||||
"scanQrCode": "Skanna QR-kod",
|
||||
"orEnterManually": "Eller ange manuellt",
|
||||
"startingServer": "Startar fjärrserver...",
|
||||
"failedToCreate": "Kunde inte starta fjärrserver:",
|
||||
"hostAddress": "Värdadress",
|
||||
"sessionId": "Sessions-ID",
|
||||
"pin": "PIN",
|
||||
"connected": "Ansluten",
|
||||
"waitingForConnection": "Väntar på anslutning...",
|
||||
"serverRunning": "Fjärrserver aktiv",
|
||||
"serverStopped": "Fjärrserver stoppad",
|
||||
"serverRunningDescription": "Mobila enheter i ditt nätverk kan upptäcka och ansluta till denna app",
|
||||
"serverStoppedDescription": "Starta servern för att tillåta mobila enheter att ansluta",
|
||||
"usePhoneToControl": "Använd din mobila enhet för att styra denna app",
|
||||
"copiedToClipboard": "${label} kopierad till urklipp",
|
||||
"copyToClipboard": "Kopiera till urklipp",
|
||||
"newSession": "Ny session",
|
||||
"startServer": "Starta server",
|
||||
"stopServer": "Stoppa server",
|
||||
"minimize": "Minimera"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "Skanna",
|
||||
"manual": "Manuell",
|
||||
"pairWithDesktop": "Parkoppla med dator",
|
||||
"enterSessionDetails": "Ange sessionsuppgifterna som visas på din datorenhet",
|
||||
"discoveryDescription": "Enheter i ditt nätverk som kör Plezy med samma Plex-konto visas automatiskt",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "Ange 8-teckens sessions-ID",
|
||||
"pinHint": "Ange 6-siffrig PIN",
|
||||
"connecting": "Ansluter...",
|
||||
"tips": "Tips",
|
||||
"tipDesktop": "Öppna Plezy på din dator och aktivera Companion Remote via inställningar eller meny",
|
||||
"tipScan": "Använd fliken Skanna för att snabbt parkoppla genom att skanna QR-koden på din dator",
|
||||
"tipWifi": "Se till att båda enheterna är på samma WiFi-nätverk",
|
||||
"cameraPermissionRequired": "Kamerabehörighet krävs för att skanna QR-koder.\nVänligen ge kameraåtkomst i enhetsinställningarna.",
|
||||
"cameraError": "Kunde inte starta kameran: ${error}",
|
||||
"scanInstruction": "Rikta kameran mot QR-koden som visas på din dator",
|
||||
"invalidQrCode": "Ogiltigt QR-kodformat",
|
||||
"validationHostRequired": "Ange en värdadress",
|
||||
"searchingForDevices": "Söker efter enheter...",
|
||||
"noDevicesFound": "Inga enheter hittades i ditt nätverk",
|
||||
"noDevicesHint": "Se till att Plezy är öppet på din dator och att båda enheterna är på samma WiFi-nätverk",
|
||||
"availableDevices": "Tillgängliga enheter",
|
||||
"manualConnection": "Manuell anslutning",
|
||||
"cryptoInitFailed": "Kunde inte initiera säker anslutning. Se till att du är inloggad på ett Plex-konto.",
|
||||
"validationHostRequired": "Ange värdadress",
|
||||
"validationHostFormat": "Format måste vara IP:port (t.ex. 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "Ange ett sessions-ID",
|
||||
"validationSessionIdLength": "Sessions-ID måste vara 8 tecken",
|
||||
"validationPinRequired": "Ange en PIN",
|
||||
"validationPinLength": "PIN måste vara 6 siffror",
|
||||
"connectionTimedOut": "Anslutningen tog för lång tid. Kontrollera sessions-ID och PIN.",
|
||||
"sessionNotFound": "Kunde inte hitta sessionen. Kontrollera dina uppgifter.",
|
||||
"connectionTimedOut": "Anslutningen tog för lång tid. Se till att båda enheterna är på samma nätverk.",
|
||||
"sessionNotFound": "Kunde inte hitta enheten. Se till att Plezy körs på värden.",
|
||||
"authFailed": "Autentisering misslyckades. Se till att båda enheterna använder samma Plex-konto.",
|
||||
"failedToConnect": "Kunde inte ansluta: ${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "Återansluter...",
|
||||
"attemptOf": "Försök ${current} av 5",
|
||||
"retryNow": "Försök nu",
|
||||
"connectionError": "Anslutningsfel",
|
||||
"notConnected": "Inte ansluten",
|
||||
"tabRemote": "Fjärrkontroll",
|
||||
"tabPlay": "Spela",
|
||||
"tabMore": "Mer",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "Använd extern spelare",
|
||||
"useExternalPlayerDescription": "Öppna videor i en extern app istället för den inbyggda spelaren",
|
||||
"selectPlayer": "Välj spelare",
|
||||
"customPlayers": "Anpassade spelare",
|
||||
"systemDefault": "Systemstandard",
|
||||
"addCustomPlayer": "Lägg till anpassad spelare",
|
||||
"playerName": "Spelarnamn",
|
||||
"playerCommand": "Kommando",
|
||||
"playerPackage": "Paketnamn",
|
||||
"playerUrlScheme": "URL-schema",
|
||||
"customPlayer": "Anpassad spelare",
|
||||
"off": "Av",
|
||||
"launchFailed": "Kunde inte öppna extern spelare",
|
||||
"appNotInstalled": "${name} är inte installerad",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "Sammanfattning",
|
||||
"poster": "Poster",
|
||||
"background": "Bakgrund",
|
||||
"logo": "Logotyp",
|
||||
"squareArt": "Kvadratisk bild",
|
||||
"selectPoster": "Välj poster",
|
||||
"selectBackground": "Välj bakgrund",
|
||||
"selectLogo": "Välj logotyp",
|
||||
"selectSquareArt": "Välj kvadratisk bild",
|
||||
"fromUrl": "Från URL",
|
||||
"uploadFile": "Ladda upp fil",
|
||||
"enterImageUrl": "Ange bild-URL",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "Automatiskt val av undertexter",
|
||||
"manuallySelected": "Manuellt vald",
|
||||
"shownWithForeignAudio": "Visas vid främmande ljud",
|
||||
"alwaysEnabled": "Alltid aktiverad"
|
||||
"alwaysEnabled": "Alltid aktiverad",
|
||||
"tags": "Taggar",
|
||||
"addTag": "Lägg till tagg",
|
||||
"genre": "Genre",
|
||||
"director": "Regissör",
|
||||
"writer": "Författare",
|
||||
"producer": "Producent",
|
||||
"country": "Land",
|
||||
"collection": "Samling",
|
||||
"label": "Etikett",
|
||||
"style": "Stil",
|
||||
"mood": "Stämning"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "Serveruppgifter",
|
||||
"failedToLoad": "Kunde inte ladda uppgifter",
|
||||
"noTasks": "Inga pågående uppgifter"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "Ansluten",
|
||||
"connectedAs": "Ansluten som @${username}",
|
||||
"disconnectConfirm": "Koppla från Trakt-konto?",
|
||||
"disconnectConfirmBody": "Plezy slutar skicka uppspelningshändelser till Trakt. Du kan ansluta igen när som helst.",
|
||||
"scrobble": "Realtids-scrobbling",
|
||||
"scrobbleDescription": "Skicka uppspelnings-, paus- och stopphändelser till Trakt under uppspelning.",
|
||||
"watchedSync": "Synkronisera tittad-status",
|
||||
"watchedSyncDescription": "När du markerar något som tittat i Plezy markeras det också på Trakt.",
|
||||
"deviceCodeTitle": "Aktivera Plezy på Trakt",
|
||||
"deviceCodeBody": "Besök ${url} och ange den här koden:",
|
||||
"openTraktActivate": "Öppna Trakt för att aktivera",
|
||||
"waitingForAuthorization": "Väntar på auktorisering…",
|
||||
"codeCopied": "Kod kopierad",
|
||||
"connectFailed": "Kunde inte ansluta till Trakt. Försök igen."
|
||||
}
|
||||
}
|
||||
|
||||
+247
-164
@@ -6,9 +6,6 @@
|
||||
"signInWithPlex": "使用 Plex 登录",
|
||||
"showQRCode": "显示二维码",
|
||||
"authenticate": "验证",
|
||||
"debugEnterToken": "调试:输入 Plex Token",
|
||||
"plexTokenLabel": "Plex 授权令牌 (Auth Token)",
|
||||
"plexTokenHint": "输入你的 Plex.tv 令牌",
|
||||
"authenticationTimeout": "验证超时。请重试。",
|
||||
"scanQRToSignIn": "扫描二维码登录",
|
||||
"waitingForAuth": "等待验证中...\n请在你的浏览器中完成登录。",
|
||||
@@ -32,8 +29,7 @@
|
||||
"delete": "删除",
|
||||
"shuffle": "随机播放",
|
||||
"addTo": "添加到...",
|
||||
"remove": "删除",
|
||||
"paste": "粘贴",
|
||||
"createNew": "新建",
|
||||
"connect": "连接",
|
||||
"disconnect": "断开连接",
|
||||
"play": "播放",
|
||||
@@ -46,7 +42,6 @@
|
||||
"settings": "设置",
|
||||
"mute": "静音",
|
||||
"ok": "确定",
|
||||
"loading": "加载中...",
|
||||
"reconnect": "重新连接",
|
||||
"exitConfirmTitle": "退出应用?",
|
||||
"exitConfirmMessage": "确定要退出吗?",
|
||||
@@ -57,13 +52,14 @@
|
||||
"refreshingServers": "正在刷新服务器...",
|
||||
"loadingServers": "正在加载服务器...",
|
||||
"connectingToServers": "正在连接服务器...",
|
||||
"startingOfflineMode": "正在启动离线模式..."
|
||||
"startingOfflineMode": "正在启动离线模式...",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "许可证",
|
||||
"switchProfile": "切换用户",
|
||||
"subtitleStyling": "字幕样式",
|
||||
"mpvConfig": "MPV 配置",
|
||||
"mpvConfig": "mpv.conf",
|
||||
"logs": "日志"
|
||||
},
|
||||
"update": {
|
||||
@@ -77,18 +73,18 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"supportDeveloper": "支持 Plezy",
|
||||
"supportDeveloperDescription": "通过 Liberapay 捐赠支持开发",
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"appearance": "外观",
|
||||
"videoPlayback": "视频播放",
|
||||
"videoPlaybackDescription": "配置播放行为",
|
||||
"advanced": "高级",
|
||||
"episodePosterMode": "剧集海报样式",
|
||||
"seriesPoster": "剧集海报",
|
||||
"seriesPosterDescription": "为所有剧集显示剧集海报",
|
||||
"seasonPoster": "季海报",
|
||||
"seasonPosterDescription": "为剧集显示特定季的海报",
|
||||
"episodeThumbnail": "剧集缩略图",
|
||||
"episodeThumbnailDescription": "显示16:9剧集截图缩略图",
|
||||
"episodeThumbnail": "缩略图",
|
||||
"showHeroSectionDescription": "在主屏幕上显示精选内容轮播区",
|
||||
"secondsLabel": "秒",
|
||||
"minutesLabel": "分钟",
|
||||
@@ -96,23 +92,15 @@
|
||||
"minutesShort": "m",
|
||||
"durationHint": "输入时长 (${min}-${max})",
|
||||
"systemTheme": "系统",
|
||||
"systemThemeDescription": "跟随系统设置",
|
||||
"lightTheme": "浅色",
|
||||
"darkTheme": "深色",
|
||||
"oledTheme": "OLED",
|
||||
"oledThemeDescription": "纯黑色适用于 OLED 屏幕",
|
||||
"libraryDensity": "媒体库密度",
|
||||
"compact": "紧凑",
|
||||
"compactDescription": "卡片更小,显示更多项目",
|
||||
"normal": "标准",
|
||||
"normalDescription": "默认尺寸",
|
||||
"comfortable": "舒适",
|
||||
"comfortableDescription": "卡片更大,显示更少项目",
|
||||
"viewMode": "视图模式",
|
||||
"gridView": "网格视图",
|
||||
"gridViewDescription": "以网格布局显示项目",
|
||||
"listView": "列表视图",
|
||||
"listViewDescription": "以列表布局显示项目",
|
||||
"showHeroSection": "显示主要精选区",
|
||||
"useGlobalHubs": "使用 Plex 主页布局",
|
||||
"useGlobalHubsDescription": "显示与官方 Plex 客户端相同的主页推荐。关闭时将显示按媒体库分类的推荐。",
|
||||
@@ -126,9 +114,7 @@
|
||||
"hideSpoilersDescription": "模糊未观看剧集的缩略图并隐藏其描述",
|
||||
"playerBackend": "播放器引擎",
|
||||
"exoPlayer": "ExoPlayer(推荐)",
|
||||
"exoPlayerDescription": "Android 原生播放器,硬件支持更好",
|
||||
"mpv": "MPV",
|
||||
"mpvDescription": "功能更多的高级播放器,支持 ASS 字幕",
|
||||
"mpv": "mpv",
|
||||
"hardwareDecoding": "硬件解码",
|
||||
"hardwareDecodingDescription": "如果可用,使用硬件加速",
|
||||
"bufferSize": "缓冲区大小",
|
||||
@@ -139,6 +125,7 @@
|
||||
"subtitleStylingDescription": "调整字幕外观",
|
||||
"smallSkipDuration": "短跳过时长",
|
||||
"largeSkipDuration": "长跳过时长",
|
||||
"rewindOnResume": "恢复时回退",
|
||||
"secondsUnit": "${seconds} 秒",
|
||||
"defaultSleepTimer": "默认睡眠定时器",
|
||||
"minutesUnit": "${minutes} 分钟",
|
||||
@@ -151,6 +138,11 @@
|
||||
"keyboardShortcutsDescription": "自定义键盘快捷键",
|
||||
"videoPlayerNavigation": "视频播放器导航",
|
||||
"videoPlayerNavigationDescription": "使用方向键导航视频播放器控件",
|
||||
"watchTogetherRelay": "一起看中继服务器",
|
||||
"watchTogetherRelayDescription": "设置一起看的自定义中继服务器。所有参与者必须使用相同的服务器。",
|
||||
"watchTogetherRelayHint": "https://my-relay.example.com",
|
||||
"crashReporting": "崩溃报告",
|
||||
"crashReportingDescription": "发送崩溃报告以帮助改进应用",
|
||||
"debugLogging": "调试日志",
|
||||
"debugLoggingDescription": "启用详细日志记录以便故障排除",
|
||||
"viewLogs": "查看日志",
|
||||
@@ -178,6 +170,11 @@
|
||||
"autoSkipCreditsDescription": "自动跳过片尾并播放下一集",
|
||||
"autoSkipDelay": "自动跳过延迟",
|
||||
"autoSkipDelayDescription": "自动跳过前等待 ${seconds} 秒",
|
||||
"introPattern": "片头标记模式",
|
||||
"introPatternDescription": "用于匹配章节标题中片头标记的正则表达式",
|
||||
"creditsPattern": "片尾标记模式",
|
||||
"creditsPatternDescription": "用于匹配章节标题中片尾标记的正则表达式",
|
||||
"invalidRegex": "无效的正则表达式",
|
||||
"downloads": "下载",
|
||||
"downloadLocationDescription": "选择下载内容的存储位置",
|
||||
"downloadLocationDefault": "默认(应用存储)",
|
||||
@@ -191,22 +188,49 @@
|
||||
"downloadLocationSelectError": "选择文件夹失败",
|
||||
"downloadOnWifiOnly": "仅在 WiFi 时下载",
|
||||
"downloadOnWifiOnlyDescription": "使用蜂窝数据时禁止下载",
|
||||
"autoRemoveWatchedDownloads": "自动移除已观看的下载",
|
||||
"autoRemoveWatchedDownloadsDescription": "当剧集和电影被标记为已观看时自动删除下载内容",
|
||||
"cellularDownloadBlocked": "蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。",
|
||||
"maxVolume": "最大音量",
|
||||
"maxVolumeDescription": "允许音量超过 100% 以适应安静的媒体",
|
||||
"maxVolumePercent": "${percent}%",
|
||||
"discordRichPresence": "Discord 动态状态",
|
||||
"discordRichPresenceDescription": "在 Discord 上显示您正在观看的内容",
|
||||
"trakt": "Trakt",
|
||||
"traktDescription": "将观看历史与 Trakt 同步",
|
||||
"companionRemoteServer": "配套遥控服务器",
|
||||
"companionRemoteServerDescription": "允许网络上的移动设备控制此应用",
|
||||
"autoPip": "自动画中画",
|
||||
"autoPipDescription": "在播放期间离开应用时自动进入画中画模式",
|
||||
"matchContentFrameRate": "匹配内容帧率",
|
||||
"matchContentFrameRateDescription": "调整显示刷新率以匹配视频内容,减少画面抖动并节省电量",
|
||||
"tunneledPlayback": "Tunneled Playback",
|
||||
"tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content",
|
||||
"matchRefreshRate": "匹配刷新率",
|
||||
"matchRefreshRateDescription": "全屏时切换显示刷新率以匹配视频内容",
|
||||
"matchDynamicRange": "匹配动态范围",
|
||||
"matchDynamicRangeDescription": "自动为HDR内容启用HDR,退出播放器时恢复为SDR",
|
||||
"displaySwitchDelay": "显示切换延迟",
|
||||
"tunneledPlayback": "通道化播放",
|
||||
"tunneledPlaybackDescription": "使用硬件加速视频通道。如果在 HDR 内容上看到黑屏但有声音,请禁用此选项",
|
||||
"requireProfileSelectionOnOpen": "打开应用时询问配置文件",
|
||||
"requireProfileSelectionOnOpenDescription": "每次打开应用时显示配置文件选择",
|
||||
"confirmExitOnBack": "退出前确认",
|
||||
"confirmExitOnBackDescription": "按返回键退出应用时显示确认对话框"
|
||||
"confirmExitOnBackDescription": "按返回键退出应用时显示确认对话框",
|
||||
"forceTvMode": "强制 TV 模式",
|
||||
"forceTvModeDescription": "无论自动检测结果如何都使用 TV 布局。适用于未报告 leanback 功能的 Android TV 设备。更改后将重启应用。",
|
||||
"autoHidePerformanceOverlay": "自动隐藏性能叠加层",
|
||||
"autoHidePerformanceOverlayDescription": "性能叠加层随播放控件一起淡入淡出",
|
||||
"showNavBarLabels": "显示导航栏标签",
|
||||
"showNavBarLabelsDescription": "在导航栏图标下方显示文字标签",
|
||||
"liveTvDefaultFavorites": "默认显示收藏频道",
|
||||
"liveTvDefaultFavoritesDescription": "打开直播电视时仅显示收藏频道",
|
||||
"display": "Display",
|
||||
"homeScreen": "Home Screen",
|
||||
"navigation": "Navigation",
|
||||
"content": "Content",
|
||||
"player": "Player",
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior"
|
||||
},
|
||||
"search": {
|
||||
"hint": "搜索电影、系列、音乐...",
|
||||
@@ -239,11 +263,6 @@
|
||||
"skipMarker": "跳过片头/片尾"
|
||||
}
|
||||
},
|
||||
"pinEntry": {
|
||||
"enterPin": "输入 PIN",
|
||||
"showPin": "显示 PIN",
|
||||
"hidePin": "隐藏 PIN"
|
||||
},
|
||||
"fileInfo": {
|
||||
"title": "文件信息",
|
||||
"video": "视频",
|
||||
@@ -262,6 +281,8 @@
|
||||
"colorPrimaries": "颜色原色",
|
||||
"chromaSubsampling": "色度子采样",
|
||||
"channels": "声道",
|
||||
"subtitles": "字幕",
|
||||
"overallBitrate": "总比特率",
|
||||
"path": "路径",
|
||||
"size": "大小",
|
||||
"container": "容器",
|
||||
@@ -282,7 +303,9 @@
|
||||
"deleteMultipleWarning": "这包括所有剧集及其文件。",
|
||||
"mediaDeletedSuccessfully": "媒体项已成功删除",
|
||||
"mediaFailedToDelete": "删除媒体项失败",
|
||||
"rate": "评分"
|
||||
"rate": "评分",
|
||||
"playFromBeginning": "从头播放",
|
||||
"playVersion": "播放版本..."
|
||||
},
|
||||
"accessibility": {
|
||||
"mediaCardMovie": "${title}, 电影",
|
||||
@@ -316,7 +339,6 @@
|
||||
"unlockRotation": "解锁旋转",
|
||||
"timerActive": "定时器已激活",
|
||||
"playbackWillPauseIn": "播放将在 ${duration} 后暂停",
|
||||
"sleepTimerCompleted": "睡眠定时器已完成 - 播放已暂停",
|
||||
"stillWatching": "还在看吗?",
|
||||
"pausingIn": "${seconds}秒后暂停",
|
||||
"continueWatching": "继续",
|
||||
@@ -333,34 +355,43 @@
|
||||
"muteButton": "静音",
|
||||
"unmuteButton": "取消静音",
|
||||
"settingsButton": "视频设置",
|
||||
"audioTrackButton": "音轨",
|
||||
"subtitlesButton": "字幕",
|
||||
"tracksButton": "音频和字幕",
|
||||
"chaptersButton": "章节",
|
||||
"versionsButton": "视频版本",
|
||||
"pipButton": "画中画模式",
|
||||
"aspectRatioButton": "宽高比",
|
||||
"ambientLighting": "氛围灯光",
|
||||
"ambientLightingOn": "开启氛围灯光",
|
||||
"ambientLightingOff": "关闭氛围灯光",
|
||||
"fullscreenButton": "进入全屏",
|
||||
"exitFullscreenButton": "退出全屏",
|
||||
"alwaysOnTopButton": "置顶窗口",
|
||||
"rotationLockButton": "旋转锁定",
|
||||
"lockScreen": "锁定屏幕",
|
||||
"screenLockButton": "屏幕锁定",
|
||||
"longPressToUnlock": "长按解锁",
|
||||
"timelineSlider": "视频时间轴",
|
||||
"volumeSlider": "音量调节",
|
||||
"endsAt": "${time} 结束",
|
||||
"pipActive": "正在画中画模式中播放",
|
||||
"pipFailed": "画中画启动失败",
|
||||
"pipErrors": {
|
||||
"androidVersion": "需要 Android 8.0 或更高版本",
|
||||
"iosVersion": "需要 iOS 15.0 或更高版本",
|
||||
"permissionDisabled": "画中画权限已禁用。请在设置 > 应用 > Plezy > 画中画中启用",
|
||||
"notSupported": "此设备不支持画中画模式",
|
||||
"voSwitchFailed": "无法切换画中画的视频输出",
|
||||
"failed": "画中画启动失败",
|
||||
"unknown": "发生错误:${error}"
|
||||
},
|
||||
"chapters": "章节",
|
||||
"noChaptersAvailable": "没有可用的章节",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"queue": "播放队列",
|
||||
"noQueueItems": "队列中没有项目",
|
||||
"searchSubtitles": "搜索字幕",
|
||||
"language": "语言",
|
||||
"noSubtitlesFound": "未找到字幕",
|
||||
"subtitleDownloaded": "字幕已下载",
|
||||
"subtitleDownloadFailed": "字幕下载失败",
|
||||
"searchLanguages": "搜索语言..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "管理员",
|
||||
@@ -373,6 +404,7 @@
|
||||
"markedAsUnwatched": "已标记为未观看",
|
||||
"markedAsWatchedOffline": "已标记为已观看 (将在联网时同步)",
|
||||
"markedAsUnwatchedOffline": "已标记为未观看 (将在联网时同步)",
|
||||
"autoRemovedWatchedDownload": "已自动移除: ${title}",
|
||||
"removedFromContinueWatching": "已从继续观看中移除",
|
||||
"errorLoading": "错误: ${error}",
|
||||
"fileInfoNotAvailable": "文件信息不可用",
|
||||
@@ -399,34 +431,32 @@
|
||||
"failedToCreatePlayQueueNoItems": "创建播放队列失败 - 没有项目",
|
||||
"failedPlayback": "无法${action}: ${error}",
|
||||
"switchingToCompatiblePlayer": "正在切换到兼容的播放器...",
|
||||
"logsUploaded": "Logs uploaded",
|
||||
"logsUploadFailed": "Failed to upload logs",
|
||||
"logId": "Log ID"
|
||||
"serverLimitTitle": "播放失败",
|
||||
"serverLimitBody": "服务器返回了错误 (HTTP 500)。这通常意味着服务器所有者设置了带宽或转码限制,正在拒绝你的会话。客户端无法解决此问题 — 需要服务器所有者调整其设置。",
|
||||
"logsUploaded": "日志已上传",
|
||||
"logsUploadFailed": "上传日志失败",
|
||||
"logId": "日志 ID"
|
||||
},
|
||||
"subtitlingStyling": {
|
||||
"stylingOptions": "样式选项",
|
||||
"text": "文本",
|
||||
"border": "边框",
|
||||
"background": "背景",
|
||||
"fontSize": "字号",
|
||||
"textColor": "文本颜色",
|
||||
"borderSize": "边框大小",
|
||||
"borderColor": "边框颜色",
|
||||
"backgroundOpacity": "背景不透明度",
|
||||
"backgroundColor": "背景颜色",
|
||||
"position": "Position"
|
||||
"position": "位置",
|
||||
"assOverride": "ASS 样式覆盖",
|
||||
"bold": "粗体",
|
||||
"italic": "斜体"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV 配置",
|
||||
"title": "mpv 配置",
|
||||
"description": "高级视频播放器设置",
|
||||
"properties": "属性",
|
||||
"presets": "预设",
|
||||
"noProperties": "未配置任何属性",
|
||||
"noPresets": "没有保存的预设",
|
||||
"addProperty": "添加属性",
|
||||
"editProperty": "编辑属性",
|
||||
"deleteProperty": "删除属性",
|
||||
"propertyKey": "属性键",
|
||||
"propertyKeyHint": "例如 hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "属性值",
|
||||
"propertyValueHint": "例如 auto, 256000000",
|
||||
"saveAsPreset": "保存为预设...",
|
||||
"presetName": "预设名称",
|
||||
"presetNameHint": "输入此预设的名称",
|
||||
@@ -436,8 +466,7 @@
|
||||
"presetLoaded": "预设已加载",
|
||||
"presetDeleted": "预设已删除",
|
||||
"confirmDeletePreset": "确定要删除此预设吗?",
|
||||
"confirmDeleteProperty": "确定要删除此属性吗?",
|
||||
"entriesCount": "${count} 条目"
|
||||
"configPlaceholder": "gpu-api=vulkan\nhwdec=auto\n# comment"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "确认操作"
|
||||
@@ -452,11 +481,8 @@
|
||||
"overview": "概述",
|
||||
"cast": "演员表",
|
||||
"extras": "预告片与花絮",
|
||||
"seasons": "季数",
|
||||
"studio": "制作公司",
|
||||
"rating": "年龄分级",
|
||||
"episodeCount": "${count} 集",
|
||||
"watchedProgress": "已观看 ${watched}/${total} 集",
|
||||
"movie": "电影",
|
||||
"tvShow": "电视剧",
|
||||
"minutesLeft": "剩余 ${minutes} 分钟"
|
||||
@@ -518,6 +544,7 @@
|
||||
"playlists": "播放列表"
|
||||
},
|
||||
"groupings": {
|
||||
"title": "分组",
|
||||
"all": "全部",
|
||||
"movies": "电影",
|
||||
"shows": "剧集",
|
||||
@@ -548,9 +575,7 @@
|
||||
"logs": {
|
||||
"clearLogs": "清除日志",
|
||||
"copyLogs": "复制日志",
|
||||
"uploadLogs": "Upload Logs",
|
||||
"error": "错误:",
|
||||
"stackTrace": "堆栈跟踪 (Stack Trace):"
|
||||
"uploadLogs": "上传日志"
|
||||
},
|
||||
"licenses": {
|
||||
"relatedPackages": "相关软件包",
|
||||
@@ -565,20 +590,12 @@
|
||||
},
|
||||
"liveTv": {
|
||||
"title": "电视直播",
|
||||
"channels": "频道",
|
||||
"guide": "节目指南",
|
||||
"noChannels": "没有可用的频道",
|
||||
"noDvr": "没有服务器配置了DVR",
|
||||
"tuneFailed": "无法调谐频道",
|
||||
"loading": "正在加载频道...",
|
||||
"nowPlaying": "正在播放",
|
||||
"noPrograms": "没有可用的节目数据",
|
||||
"channelNumber": "频道 ${number}",
|
||||
"live": "直播",
|
||||
"hd": "高清",
|
||||
"premiere": "新",
|
||||
"reloadGuide": "重新加载节目指南",
|
||||
"allChannels": "所有频道",
|
||||
"now": "现在",
|
||||
"today": "今天",
|
||||
"midnight": "午夜",
|
||||
@@ -588,30 +605,41 @@
|
||||
"evening": "晚上",
|
||||
"lateNight": "深夜",
|
||||
"whatsOn": "正在播出",
|
||||
"watchChannel": "观看频道"
|
||||
"watchChannel": "观看频道",
|
||||
"favorites": "收藏",
|
||||
"reorderFavorites": "重新排序收藏",
|
||||
"joinSession": "加入正在进行的会话",
|
||||
"watchFromStart": "从头观看(${minutes}分钟前开始)",
|
||||
"watchLive": "观看直播",
|
||||
"goToLive": "跳至直播"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "下载",
|
||||
"manage": "管理",
|
||||
"tvShows": "电视剧",
|
||||
"movies": "电影",
|
||||
"noDownloads": "暂无下载",
|
||||
"noDownloadsDescription": "下载的内容将在此处显示以供离线观看",
|
||||
"downloadNow": "下载",
|
||||
"deleteDownload": "删除下载",
|
||||
"retryDownload": "重试下载",
|
||||
"downloadQueued": "下载已排队",
|
||||
"episodesQueued": "${count} 集已加入下载队列",
|
||||
"downloadDeleted": "下载已删除",
|
||||
"deleteConfirm": "确定要删除 \"${title}\" 吗?下载的文件将从您的设备中删除。",
|
||||
"deletingWithProgress": "正在删除 ${title}... (${current}/${total})",
|
||||
"noDownloadsTree": "暂无下载",
|
||||
"pauseAll": "全部暂停",
|
||||
"resumeAll": "全部继续",
|
||||
"deleteAll": "全部删除"
|
||||
"collections": {
|
||||
"title": "合集",
|
||||
"collection": "合集",
|
||||
"empty": "合集为空",
|
||||
"unknownLibrarySection": "无法删除:未知的媒体库分区",
|
||||
"deleteCollection": "删除合集",
|
||||
"deleteConfirm": "确定要删除\"${title}\"吗?此操作无法撤销。",
|
||||
"deleted": "已删除合集",
|
||||
"deleteFailed": "删除合集失败",
|
||||
"deleteFailedWithError": "删除合集失败:${error}",
|
||||
"failedToLoadItems": "加载合集项目失败:${error}",
|
||||
"selectCollection": "选择合集",
|
||||
"collectionName": "合集名称",
|
||||
"enterCollectionName": "输入合集名称",
|
||||
"addedToCollection": "已添加到合集",
|
||||
"errorAddingToCollection": "添加到合集失败",
|
||||
"created": "已创建合集",
|
||||
"removeFromCollection": "从合集移除",
|
||||
"removeFromCollectionConfirm": "将“${title}”从此合集移除?",
|
||||
"removedFromCollection": "已从合集移除",
|
||||
"removeFromCollectionFailed": "从合集移除失败",
|
||||
"removeFromCollectionError": "从合集移除时出错:${error}",
|
||||
"searchCollections": "搜索合集..."
|
||||
},
|
||||
"playlists": {
|
||||
"title": "播放列表",
|
||||
"playlist": "播放列表",
|
||||
"noPlaylists": "未找到播放列表",
|
||||
"create": "创建播放列表",
|
||||
"playlistName": "播放列表名称",
|
||||
@@ -629,38 +657,12 @@
|
||||
"itemAdded": "已添加到播放列表",
|
||||
"itemRemoved": "已从播放列表中移除",
|
||||
"selectPlaylist": "选择播放列表",
|
||||
"createNewPlaylist": "创建新播放列表",
|
||||
"errorCreating": "创建播放列表失败",
|
||||
"errorDeleting": "删除播放列表失败",
|
||||
"errorLoading": "加载播放列表失败",
|
||||
"errorAdding": "添加到播放列表失败",
|
||||
"errorReordering": "重新排序播放列表项目失败",
|
||||
"errorRemoving": "从播放列表中移除失败",
|
||||
"playlist": "播放列表"
|
||||
},
|
||||
"collections": {
|
||||
"title": "合集",
|
||||
"collection": "合集",
|
||||
"empty": "合集为空",
|
||||
"unknownLibrarySection": "无法删除:未知的媒体库分区",
|
||||
"deleteCollection": "删除合集",
|
||||
"deleteConfirm": "确定要删除\"${title}\"吗?此操作无法撤销。",
|
||||
"deleted": "已删除合集",
|
||||
"deleteFailed": "删除合集失败",
|
||||
"deleteFailedWithError": "删除合集失败:${error}",
|
||||
"failedToLoadItems": "加载合集项目失败:${error}",
|
||||
"selectCollection": "选择合集",
|
||||
"createNewCollection": "创建新合集",
|
||||
"collectionName": "合集名称",
|
||||
"enterCollectionName": "输入合集名称",
|
||||
"addedToCollection": "已添加到合集",
|
||||
"errorAddingToCollection": "添加到合集失败",
|
||||
"created": "已创建合集",
|
||||
"removeFromCollection": "从合集移除",
|
||||
"removeFromCollectionConfirm": "将“${title}”从此合集移除?",
|
||||
"removedFromCollection": "已从合集移除",
|
||||
"removeFromCollectionFailed": "从合集移除失败",
|
||||
"removeFromCollectionError": "从合集移除时出错:${error}"
|
||||
"errorRemoving": "从播放列表中移除失败"
|
||||
},
|
||||
"watchTogether": {
|
||||
"title": "一起看",
|
||||
@@ -697,18 +699,76 @@
|
||||
"leave": "离开",
|
||||
"syncing": "同步中...",
|
||||
"joinWatchSession": "加入观看会话",
|
||||
"enterCodeHint": "输入8位代码",
|
||||
"enterCodeHint": "输入5位代码",
|
||||
"pasteFromClipboard": "从剪贴板粘贴",
|
||||
"pleaseEnterCode": "请输入会话代码",
|
||||
"codeMustBe8Chars": "会话代码必须是8个字符",
|
||||
"codeMustBe5Chars": "会话代码必须是5个字符",
|
||||
"joinInstructions": "输入主持人分享的会话代码以加入他们的观看会话。",
|
||||
"failedToCreate": "创建会话失败",
|
||||
"failedToJoin": "加入会话失败",
|
||||
"sessionCodeCopied": "会话代码已复制到剪贴板",
|
||||
"relayUnreachable": "无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。",
|
||||
"reconnectingToHost": "正在重新连接到主持人...",
|
||||
"currentPlayback": "当前播放",
|
||||
"joinCurrentPlayback": "加入当前播放",
|
||||
"joinCurrentPlaybackDescription": "回到房主当前正在观看的内容",
|
||||
"failedToOpenCurrentPlayback": "无法打开当前播放",
|
||||
"participantJoined": "${name} 加入了",
|
||||
"participantLeft": "${name} 离开了"
|
||||
"participantLeft": "${name} 离开了",
|
||||
"participantPaused": "${name} 暂停了",
|
||||
"participantResumed": "${name} 继续播放了",
|
||||
"participantSeeked": "${name} 跳转了",
|
||||
"participantBuffering": "${name} 正在缓冲",
|
||||
"waitingForParticipants": "等待其他人加载...",
|
||||
"recentRooms": "最近的房间",
|
||||
"renameRoom": "重命名房间",
|
||||
"removeRoom": "移除"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "下载",
|
||||
"manage": "管理",
|
||||
"tvShows": "电视剧",
|
||||
"movies": "电影",
|
||||
"noDownloads": "暂无下载",
|
||||
"noDownloadsDescription": "下载的内容将在此处显示以供离线观看",
|
||||
"downloadNow": "下载",
|
||||
"deleteDownload": "删除下载",
|
||||
"retryDownload": "重试下载",
|
||||
"downloadQueued": "下载已排队",
|
||||
"serverErrorBitrate": "服务器错误 — 文件可能超出远程流媒体比特率限制",
|
||||
"episodesQueued": "${count} 集已加入下载队列",
|
||||
"downloadDeleted": "下载已删除",
|
||||
"deleteConfirm": "确定要删除 \"${title}\" 吗?下载的文件将从您的设备中删除。",
|
||||
"deletingWithProgress": "正在删除 ${title}... (${current}/${total})",
|
||||
"noDownloadsTree": "暂无下载",
|
||||
"pauseAll": "全部暂停",
|
||||
"resumeAll": "全部继续",
|
||||
"deleteAll": "全部删除",
|
||||
"selectVersion": "选择版本",
|
||||
"allEpisodes": "所有剧集",
|
||||
"unwatchedOnly": "仅未观看",
|
||||
"nextNUnwatched": "接下来 ${count} 集未观看",
|
||||
"customAmount": "自定义数量...",
|
||||
"howManyEpisodes": "下载几集?",
|
||||
"itemsQueued": "${count} 个项目已加入下载队列",
|
||||
"keepSynced": "保持同步",
|
||||
"downloadOnce": "下载一次",
|
||||
"keepNUnwatched": "保留${count}个未观看",
|
||||
"editSyncRule": "编辑同步规则",
|
||||
"removeSyncRule": "删除同步规则",
|
||||
"removeSyncRuleConfirm": "停止同步“${title}”?已下载的剧集将被保留。",
|
||||
"syncRuleCreated": "同步规则已创建 — 保留${count}个未观看的剧集",
|
||||
"syncRuleUpdated": "同步规则已更新",
|
||||
"syncRuleRemoved": "同步规则已删除",
|
||||
"syncedNewEpisodes": "已为${title}同步${count}个新剧集",
|
||||
"activeSyncRules": "同步规则",
|
||||
"noSyncRules": "没有同步规则",
|
||||
"manageSyncRule": "管理同步",
|
||||
"editEpisodeCount": "剧集数量",
|
||||
"editSyncFilter": "同步筛选",
|
||||
"syncAllItems": "同步所有项目",
|
||||
"syncUnwatchedItems": "同步未观看项目",
|
||||
"syncRuleListCreated": "同步规则已创建"
|
||||
},
|
||||
"shaders": {
|
||||
"title": "着色器",
|
||||
@@ -716,58 +776,46 @@
|
||||
"nvscalerDescription": "NVIDIA 图像缩放,使视频更清晰",
|
||||
"qualityFast": "快速",
|
||||
"qualityHQ": "高质量",
|
||||
"mode": "模式"
|
||||
"mode": "模式",
|
||||
"importShader": "导入着色器",
|
||||
"customShaderDescription": "自定义 GLSL 着色器",
|
||||
"shaderImported": "着色器已导入",
|
||||
"shaderImportFailed": "导入着色器失败",
|
||||
"deleteShader": "删除着色器",
|
||||
"deleteShaderConfirm": "删除\"${name}\"?"
|
||||
},
|
||||
"companionRemote": {
|
||||
"title": "Companion Remote",
|
||||
"connectToDevice": "连接到设备",
|
||||
"hostRemoteSession": "创建远程会话",
|
||||
"controlThisDevice": "使用手机控制此设备",
|
||||
"remoteControl": "远程控制",
|
||||
"controlDesktop": "控制桌面设备",
|
||||
"title": "伴侣遥控",
|
||||
"connectedTo": "已连接到 ${name}",
|
||||
"session": {
|
||||
"creatingSession": "正在创建远程会话...",
|
||||
"failedToCreate": "创建远程会话失败:",
|
||||
"noSession": "没有可用的会话",
|
||||
"scanQrCode": "扫描 QR 码",
|
||||
"orEnterManually": "或手动输入",
|
||||
"startingServer": "正在启动远程服务器...",
|
||||
"failedToCreate": "启动远程服务器失败:",
|
||||
"hostAddress": "主机地址",
|
||||
"sessionId": "会话 ID",
|
||||
"pin": "PIN",
|
||||
"connected": "已连接",
|
||||
"waitingForConnection": "等待连接中...",
|
||||
"serverRunning": "远程服务器已启动",
|
||||
"serverStopped": "远程服务器已停止",
|
||||
"serverRunningDescription": "网络上的移动设备可以发现并连接到此应用",
|
||||
"serverStoppedDescription": "启动服务器以允许移动设备连接",
|
||||
"usePhoneToControl": "使用移动设备控制此应用",
|
||||
"copiedToClipboard": "${label}已复制到剪贴板",
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"newSession": "新建会话",
|
||||
"startServer": "启动服务器",
|
||||
"stopServer": "停止服务器",
|
||||
"minimize": "最小化"
|
||||
},
|
||||
"pairing": {
|
||||
"scan": "扫描",
|
||||
"manual": "手动",
|
||||
"pairWithDesktop": "与桌面配对",
|
||||
"enterSessionDetails": "输入桌面设备上显示的会话信息",
|
||||
"discoveryDescription": "网络上使用相同Plex账户运行Plezy的设备将自动显示",
|
||||
"hostAddressHint": "192.168.1.100:48632",
|
||||
"sessionIdHint": "输入8位会话 ID",
|
||||
"pinHint": "输入6位 PIN",
|
||||
"connecting": "连接中...",
|
||||
"tips": "提示",
|
||||
"tipDesktop": "在桌面上打开 Plezy,并从设置或菜单中启用 Companion Remote",
|
||||
"tipScan": "使用扫描选项卡扫描桌面上的 QR 码以快速配对",
|
||||
"tipWifi": "请确保两台设备连接到同一个 WiFi 网络",
|
||||
"cameraPermissionRequired": "扫描 QR 码需要相机权限。\n请在设备设置中授予相机访问权限。",
|
||||
"cameraError": "无法启动相机:${error}",
|
||||
"scanInstruction": "将相机对准桌面上显示的 QR 码",
|
||||
"invalidQrCode": "无效的 QR 码格式",
|
||||
"connecting": "正在连接...",
|
||||
"searchingForDevices": "正在搜索设备...",
|
||||
"noDevicesFound": "未在网络上找到设备",
|
||||
"noDevicesHint": "请确保桌面上已打开Plezy,且两台设备在同一WiFi网络上",
|
||||
"availableDevices": "可用设备",
|
||||
"manualConnection": "手动连接",
|
||||
"cryptoInitFailed": "无法初始化安全连接。请确保已登录Plex账户。",
|
||||
"validationHostRequired": "请输入主机地址",
|
||||
"validationHostFormat": "格式必须为 IP:端口(例如 192.168.1.100:48632)",
|
||||
"validationSessionIdRequired": "请输入会话 ID",
|
||||
"validationSessionIdLength": "会话 ID 必须为8个字符",
|
||||
"validationPinRequired": "请输入 PIN",
|
||||
"validationPinLength": "PIN 必须为6位数字",
|
||||
"connectionTimedOut": "连接超时。请检查会话 ID 和 PIN。",
|
||||
"sessionNotFound": "找不到会话。请检查您的凭据。",
|
||||
"validationHostFormat": "格式必须为IP:端口(例如 192.168.1.100:48632)",
|
||||
"connectionTimedOut": "连接超时。请确保两台设备在同一网络上。",
|
||||
"sessionNotFound": "未找到设备。请确保Plezy正在主机上运行。",
|
||||
"authFailed": "认证失败。请确保两台设备使用相同的Plex账户。",
|
||||
"failedToConnect": "连接失败:${error}"
|
||||
},
|
||||
"remote": {
|
||||
@@ -775,8 +823,6 @@
|
||||
"reconnecting": "重新连接中...",
|
||||
"attemptOf": "第 ${current} 次尝试,共 5 次",
|
||||
"retryNow": "立即重试",
|
||||
"connectionError": "连接错误",
|
||||
"notConnected": "未连接",
|
||||
"tabRemote": "遥控",
|
||||
"tabPlay": "播放",
|
||||
"tabMore": "更多",
|
||||
@@ -819,13 +865,13 @@
|
||||
"useExternalPlayer": "使用外部播放器",
|
||||
"useExternalPlayerDescription": "在外部应用中打开视频,而不是使用内置播放器",
|
||||
"selectPlayer": "选择播放器",
|
||||
"customPlayers": "自定义播放器",
|
||||
"systemDefault": "系统默认",
|
||||
"addCustomPlayer": "添加自定义播放器",
|
||||
"playerName": "播放器名称",
|
||||
"playerCommand": "命令",
|
||||
"playerPackage": "包名",
|
||||
"playerUrlScheme": "URL 方案",
|
||||
"customPlayer": "自定义播放器",
|
||||
"off": "关闭",
|
||||
"launchFailed": "无法打开外部播放器",
|
||||
"appNotInstalled": "${name} 未安装",
|
||||
@@ -847,8 +893,12 @@
|
||||
"summary": "简介",
|
||||
"poster": "海报",
|
||||
"background": "背景",
|
||||
"logo": "标志",
|
||||
"squareArt": "方形图片",
|
||||
"selectPoster": "选择海报",
|
||||
"selectBackground": "选择背景",
|
||||
"selectLogo": "选择标志",
|
||||
"selectSquareArt": "选择方形图片",
|
||||
"fromUrl": "从 URL",
|
||||
"uploadFile": "上传文件",
|
||||
"enterImageUrl": "输入图片 URL",
|
||||
@@ -890,6 +940,39 @@
|
||||
"subtitleMode": "自动选择字幕模式",
|
||||
"manuallySelected": "手动选择",
|
||||
"shownWithForeignAudio": "外语音频时显示",
|
||||
"alwaysEnabled": "始终启用"
|
||||
"alwaysEnabled": "始终启用",
|
||||
"tags": "标签",
|
||||
"addTag": "添加标签",
|
||||
"genre": "类型",
|
||||
"director": "导演",
|
||||
"writer": "编剧",
|
||||
"producer": "制片人",
|
||||
"country": "国家",
|
||||
"collection": "合集",
|
||||
"label": "标记",
|
||||
"style": "风格",
|
||||
"mood": "氛围"
|
||||
},
|
||||
"serverTasks": {
|
||||
"title": "服务器任务",
|
||||
"failedToLoad": "加载任务失败",
|
||||
"noTasks": "没有正在运行的任务"
|
||||
},
|
||||
"trakt": {
|
||||
"title": "Trakt",
|
||||
"connected": "已连接",
|
||||
"connectedAs": "已以 @${username} 身份连接",
|
||||
"disconnectConfirm": "断开 Trakt 账户?",
|
||||
"disconnectConfirmBody": "Plezy 将停止向 Trakt 发送播放事件。您随时可以重新连接。",
|
||||
"scrobble": "实时 Scrobble",
|
||||
"scrobbleDescription": "在播放时向 Trakt 发送播放、暂停和停止事件。",
|
||||
"watchedSync": "同步已观看状态",
|
||||
"watchedSyncDescription": "在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。",
|
||||
"deviceCodeTitle": "在 Trakt 上激活 Plezy",
|
||||
"deviceCodeBody": "请访问 ${url} 并输入此代码:",
|
||||
"openTraktActivate": "打开 Trakt 进行激活",
|
||||
"waitingForAuthorization": "等待授权…",
|
||||
"codeCopied": "代码已复制",
|
||||
"connectFailed": "无法连接到 Trakt。请重试。"
|
||||
}
|
||||
}
|
||||
|
||||
+561
-58
@@ -1,22 +1,29 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' show AppExitResponse;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:io' show Platform, ProcessInfo;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'screens/main_screen.dart';
|
||||
import 'screens/auth_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
import 'services/macos_window_service.dart';
|
||||
import 'services/native_window_service.dart';
|
||||
import 'services/fullscreen_state_manager.dart';
|
||||
import 'services/settings_service.dart';
|
||||
import 'utils/platform_detector.dart';
|
||||
import 'services/discord_rpc_service.dart';
|
||||
import 'services/gamepad_service.dart';
|
||||
import 'services/trakt/trakt_scrobble_service.dart';
|
||||
import 'services/trakt/trakt_sync_service.dart';
|
||||
import 'providers/trakt_account_provider.dart';
|
||||
import 'providers/user_profile_provider.dart';
|
||||
import 'providers/multi_server_provider.dart';
|
||||
import 'providers/server_state_provider.dart';
|
||||
import 'providers/theme_provider.dart';
|
||||
import 'providers/settings_provider.dart';
|
||||
import 'providers/hidden_libraries_provider.dart';
|
||||
@@ -27,6 +34,7 @@ import 'providers/offline_mode_provider.dart';
|
||||
import 'providers/offline_watch_provider.dart';
|
||||
import 'providers/companion_remote_provider.dart';
|
||||
import 'providers/shader_provider.dart';
|
||||
import 'utils/snackbar_helper.dart';
|
||||
import 'watch_together/watch_together.dart';
|
||||
import 'services/multi_server_manager.dart';
|
||||
import 'services/offline_watch_sync_service.dart';
|
||||
@@ -40,14 +48,23 @@ import 'services/download_storage_service.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'services/plex_api_cache.dart';
|
||||
import 'database/app_database.dart';
|
||||
import 'screens/video_player_screen.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
import 'utils/orientation_helper.dart';
|
||||
import 'utils/language_codes.dart';
|
||||
import 'utils/global_key_utils.dart';
|
||||
import 'utils/watch_state_notifier.dart';
|
||||
import 'i18n/strings.g.dart';
|
||||
import 'focus/input_mode_tracker.dart';
|
||||
import 'focus/key_event_utils.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'utils/navigation_transitions.dart';
|
||||
import 'utils/log_redaction_manager.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
const bool _enableSentry = bool.fromEnvironment('ENABLE_SENTRY', defaultValue: false);
|
||||
const String gitCommit = String.fromEnvironment('GIT_COMMIT');
|
||||
const String _sentryEnvironment = String.fromEnvironment('SENTRY_ENVIRONMENT');
|
||||
const String _plexTokenDefine = String.fromEnvironment('PLEX_TOKEN');
|
||||
|
||||
// Workaround for Flutter bug #177992: iPadOS 26.1+ misinterprets fake touch events
|
||||
// at (0,0) as barrier taps, causing modals to dismiss immediately.
|
||||
@@ -66,10 +83,34 @@ void _absorbZeroOffsetPointerEvent(PointerEvent event) {
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
_installZeroOffsetPointerGuard(); // Workaround for iPadOS 26.1+ modal dismissal bug
|
||||
|
||||
if (_enableSentry) {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
await SentryFlutter.init((options) {
|
||||
options.dsn = 'https://6a1a6ef8c72140099b2798973c1bfb2f@bugs.plezy.app/1';
|
||||
options.release = gitCommit.isNotEmpty
|
||||
? 'plezy@${gitCommit.substring(0, 7)}'
|
||||
: 'plezy@${packageInfo.version}+${packageInfo.buildNumber}';
|
||||
if (_sentryEnvironment.isNotEmpty) options.environment = _sentryEnvironment;
|
||||
options.tracesSampleRate = 0;
|
||||
options.attachStacktrace = true;
|
||||
options.enableAutoSessionTracking = false;
|
||||
options.recordHttpBreadcrumbs = false;
|
||||
options.captureNativeFailedRequests = false;
|
||||
options.beforeSend = _beforeSend;
|
||||
options.beforeBreadcrumb = _beforeBreadcrumb;
|
||||
}, appRunner: _bootstrapApp);
|
||||
return;
|
||||
}
|
||||
|
||||
await _bootstrapApp();
|
||||
}
|
||||
|
||||
Future<void> _bootstrapApp() async {
|
||||
// Initialize settings first to get saved locale
|
||||
final settings = await SettingsService.getInstance();
|
||||
final savedLocale = settings.getAppLocale();
|
||||
@@ -80,9 +121,14 @@ void main() async {
|
||||
// Needed for formatting dates in different locales
|
||||
await initializeDateFormatting(savedLocale.languageCode, null);
|
||||
|
||||
// Configure image cache for large libraries
|
||||
PaintingBinding.instance.imageCache.maximumSize = 2000; // default 1000
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 300 << 20; // 300MB
|
||||
// Configure image cache — keep budget modest to leave headroom for Skia decode buffers
|
||||
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||
PaintingBinding.instance.imageCache.maximumSize = 1000;
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 150 << 20; // 150MB
|
||||
} else {
|
||||
PaintingBinding.instance.imageCache.maximumSize = 800;
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20; // 100MB
|
||||
}
|
||||
|
||||
// Initialize services in parallel where possible
|
||||
final futures = <Future<void>>[];
|
||||
@@ -94,7 +140,7 @@ void main() async {
|
||||
|
||||
// Initialize TV detection and PiP service for Android
|
||||
if (Platform.isAndroid) {
|
||||
futures.add(TvDetectionService.getInstance());
|
||||
futures.add(TvDetectionService.getInstance(forceTv: settings.getForceTvMode()));
|
||||
// Initialize PiP service to listen for PiP state changes
|
||||
PipService();
|
||||
}
|
||||
@@ -102,39 +148,157 @@ void main() async {
|
||||
// Configure macOS window with custom titlebar (depends on window manager)
|
||||
futures.add(MacOSWindowService.setupCustomTitlebar());
|
||||
|
||||
// Hook Windows native fullscreen callback (no-op elsewhere).
|
||||
NativeWindowService.initialize();
|
||||
|
||||
// Initialize storage service
|
||||
futures.add(StorageService.getInstance());
|
||||
|
||||
// Initialize language codes for track selection
|
||||
futures.add(LanguageCodes.initialize());
|
||||
|
||||
// Wait for all parallel services to complete
|
||||
await Future.wait(futures);
|
||||
|
||||
// Seed Plex token from dart-define (used by screenshot automation)
|
||||
if (_plexTokenDefine.isNotEmpty) {
|
||||
final storage = await StorageService.getInstance();
|
||||
await storage.savePlexToken(_plexTokenDefine);
|
||||
}
|
||||
|
||||
// Initialize logger level based on debug setting
|
||||
final debugEnabled = settings.getEnableDebugLogging();
|
||||
setLoggerLevel(debugEnabled);
|
||||
|
||||
// Log app version and git commit at startup
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final commitSuffix = gitCommit.isNotEmpty ? ' (${gitCommit.substring(0, 7)})' : '';
|
||||
String renderer = '';
|
||||
if (Platform.isAndroid) {
|
||||
renderer = ' [${await const MethodChannel('com.plezy/theme').invokeMethod<String>('getRenderer')}]';
|
||||
}
|
||||
appLogger.i('Plezy v${packageInfo.version}+${packageInfo.buildNumber}$commitSuffix$renderer');
|
||||
|
||||
// Initialize download storage service with settings
|
||||
await DownloadStorageService.instance.initialize(settings);
|
||||
|
||||
// Start global fullscreen state monitoring
|
||||
FullscreenStateManager().startMonitoring();
|
||||
|
||||
// Initialize gamepad service for desktop platforms
|
||||
// Initialize gamepad service (all platforms — universal_gamepad auto-registers
|
||||
// and intercepts input events, so we must listen to re-dispatch them)
|
||||
GamepadService.instance.start();
|
||||
|
||||
// Desktop-only services
|
||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
GamepadService.instance.start();
|
||||
DiscordRPCService.instance.initialize();
|
||||
}
|
||||
|
||||
// Trakt scrobble service (all platforms)
|
||||
await TraktScrobbleService.instance.initialize();
|
||||
|
||||
// DTD service is available for MCP tooling connection if needed
|
||||
|
||||
// Register bundled shader licenses
|
||||
_registerShaderLicenses();
|
||||
|
||||
// In release mode, show a colored placeholder instead of a blank/white screen
|
||||
// when a widget build() throws an unhandled exception.
|
||||
ErrorWidget.builder = (FlutterErrorDetails details) {
|
||||
if (kDebugMode) return ErrorWidget(details.exception);
|
||||
return const ColoredBox(color: Color(0xFF000000));
|
||||
};
|
||||
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
Breadcrumb? _beforeBreadcrumb(Breadcrumb? breadcrumb, Hint _) {
|
||||
if (breadcrumb == null) return null;
|
||||
|
||||
final message = breadcrumb.message;
|
||||
final data = breadcrumb.data;
|
||||
if (message == null && (data == null || data.isEmpty)) return breadcrumb;
|
||||
|
||||
if (message != null) breadcrumb.message = LogRedactionManager.redact(message);
|
||||
if (data != null) breadcrumb.data = data.map((k, v) => MapEntry(k, v is String ? LogRedactionManager.redact(v) : v));
|
||||
return breadcrumb;
|
||||
}
|
||||
|
||||
FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) {
|
||||
// Drop event if user opted out of crash reporting
|
||||
final instance = SettingsService.instanceOrNull;
|
||||
if (instance != null && !instance.getCrashReporting()) return null;
|
||||
|
||||
// Drop unactionable errors
|
||||
var exceptions = event.exceptions;
|
||||
if (exceptions != null) {
|
||||
bool shouldDrop(SentryException e) {
|
||||
final v = e.value;
|
||||
// Windows file-lock errors from cache manager cleanup
|
||||
if (e.type == 'FileSystemException' && v != null && v.contains('plexImageCache') && v.contains('errno = 32')) {
|
||||
return true;
|
||||
}
|
||||
// Linux without DBus/NetworkManager
|
||||
if (e.type == 'DBusServiceUnknownException' || (v != null && v.contains('system_bus_socket'))) {
|
||||
return true;
|
||||
}
|
||||
// Device out of disk space
|
||||
if (v != null &&
|
||||
(v.contains('SQLITE_FULL') ||
|
||||
v.contains('No space left on device') ||
|
||||
v.contains('errno = 112') ||
|
||||
v.contains('database or disk is full'))) {
|
||||
return true;
|
||||
}
|
||||
// Native HTTP errors from CFNetwork (server errors, not actionable)
|
||||
if (e.type == 'HTTPClientError') return true;
|
||||
// Discord RPC errors when Discord is not running
|
||||
if (e.type == 'DiscordStateException') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exceptions.any(shouldDrop)) return null;
|
||||
|
||||
// Scrub Plex tokens and server URLs from exception messages
|
||||
for (final e in exceptions) {
|
||||
final value = e.value;
|
||||
if (value != null) {
|
||||
e.value = LogRedactionManager.redact(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich TimeoutException with operation name + duration as tags/fingerprint.
|
||||
// value format: "TimeoutException after 0:00:05.000000: <operation> timed out"
|
||||
if (exceptions != null) {
|
||||
final timeoutException = exceptions.where((e) => e.type == 'TimeoutException').firstOrNull;
|
||||
if (timeoutException != null) {
|
||||
final value = timeoutException.value ?? '';
|
||||
final colonIdx = value.indexOf(': ');
|
||||
final message = colonIdx >= 0 ? value.substring(colonIdx + 2) : value;
|
||||
final operation = message.endsWith(' timed out')
|
||||
? message.substring(0, message.length - ' timed out'.length)
|
||||
: null;
|
||||
final durationMatch = RegExp(r'after (\d+:\d{2}:\d{2}\.\d+)').firstMatch(value);
|
||||
|
||||
final tags = event.tags ??= {};
|
||||
if (operation != null) tags['timeout.operation'] = operation;
|
||||
if (durationMatch != null) tags['timeout.duration'] = durationMatch.group(1)!;
|
||||
event.fingerprint = ['TimeoutException', ?operation];
|
||||
}
|
||||
}
|
||||
|
||||
// Scrub breadcrumb messages and data
|
||||
final breadcrumbs = event.breadcrumbs;
|
||||
if (breadcrumbs != null) {
|
||||
for (final b in breadcrumbs) {
|
||||
final message = b.message;
|
||||
final data = b.data;
|
||||
if (message != null) b.message = LogRedactionManager.redact(message);
|
||||
if (data != null) b.data = data.map((k, v) => MapEntry(k, v is String ? LogRedactionManager.redact(v) : v));
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void _registerShaderLicenses() {
|
||||
LicenseRegistry.addLicense(() async* {
|
||||
yield const LicenseEntryWithLineBreaks(
|
||||
@@ -190,6 +354,7 @@ void _registerShaderLicenses() {
|
||||
|
||||
// Global RouteObserver for tracking navigation
|
||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
class MainApp extends StatefulWidget {
|
||||
const MainApp({super.key});
|
||||
@@ -205,12 +370,37 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
late final AppDatabase _appDatabase;
|
||||
late final DownloadManagerService _downloadManager;
|
||||
late final OfflineWatchSyncService _offlineWatchSyncService;
|
||||
late final AppLifecycleListener _appLifecycleListener;
|
||||
StreamSubscription<WatchStateEvent>? _watchStateSubscription;
|
||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
||||
Timer? _syncDebounce;
|
||||
final Set<String> _pendingSyncKeys = <String>{};
|
||||
bool _isAutoDeleteRunning = false;
|
||||
bool _lastConnectivityWasWifi = false;
|
||||
|
||||
/// Last time server health probes ran from a resume event (cooldown for desktop)
|
||||
DateTime _lastResumeProbe = DateTime(0);
|
||||
|
||||
/// Periodic memory check timer for desktop platforms
|
||||
Timer? _memoryCheckTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// On desktop, periodically check RSS and evict image cache if too high
|
||||
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||
_memoryCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
final rss = ProcessInfo.currentRss;
|
||||
if (rss > 1536 * 1024 * 1024) {
|
||||
// 1.5GB
|
||||
appLogger.w('RSS high ($rss bytes), evicting image caches');
|
||||
_evictImageCaches();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_serverManager = MultiServerManager();
|
||||
_aggregationService = DataAggregationService(_serverManager);
|
||||
_appDatabase = AppDatabase();
|
||||
@@ -219,34 +409,168 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
PlexApiCache.initialize(_appDatabase);
|
||||
|
||||
_downloadManager = DownloadManagerService(database: _appDatabase, storageService: DownloadStorageService.instance);
|
||||
_downloadManager.setClientResolver(_serverManager.getClient);
|
||||
_downloadManager.recoveryFuture = _downloadManager.recoverInterruptedDownloads();
|
||||
|
||||
_offlineWatchSyncService = OfflineWatchSyncService(database: _appDatabase, serverManager: _serverManager);
|
||||
|
||||
// Trakt sync service (subscribes to WatchStateNotifier, requires serverManager
|
||||
// to resolve PlexClients for GUID lookups).
|
||||
TraktSyncService.instance.initialize(serverManager: _serverManager);
|
||||
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onExitRequested: () async {
|
||||
await _appDatabase.close();
|
||||
return AppExitResponse.exit;
|
||||
},
|
||||
);
|
||||
|
||||
// Start in-app review session tracking
|
||||
InAppReviewService.instance.startSession();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_syncDebounce?.cancel();
|
||||
_watchStateSubscription?.cancel();
|
||||
_connectivitySubscription?.cancel();
|
||||
_memoryCheckTimer?.cancel();
|
||||
_appLifecycleListener.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didHaveMemoryPressure() {
|
||||
super.didHaveMemoryPressure();
|
||||
appLogger.w('System memory pressure, evicting image caches');
|
||||
_evictImageCaches();
|
||||
}
|
||||
|
||||
void _evictImageCaches() {
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
PaintingBinding.instance.imageCache.clearLiveImages();
|
||||
}
|
||||
|
||||
/// Fires [_autoDeleteAndSync] on each WiFi/Ethernet reconnect so rules run
|
||||
/// as soon as the device is back online. Rapid flapping is bounded by the
|
||||
/// executor's cooldown.
|
||||
void _startConnectivitySyncTrigger(DownloadProvider downloadProvider) {
|
||||
Future<void> setup() async {
|
||||
try {
|
||||
final initial = await Connectivity().checkConnectivity();
|
||||
_lastConnectivityWasWifi = _hasWifiOrEthernet(initial);
|
||||
} catch (e) {
|
||||
appLogger.w('Initial connectivity read failed, defaulting to false: $e');
|
||||
_lastConnectivityWasWifi = false;
|
||||
}
|
||||
|
||||
try {
|
||||
_connectivitySubscription = Connectivity().onConnectivityChanged.listen((results) {
|
||||
final hasWifi = _hasWifiOrEthernet(results);
|
||||
final transitioned = hasWifi && !_lastConnectivityWasWifi;
|
||||
_lastConnectivityWasWifi = hasWifi;
|
||||
if (transitioned) {
|
||||
appLogger.d('Connectivity moved onto WiFi/Ethernet — triggering sync pass');
|
||||
_autoDeleteAndSync(downloadProvider);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
appLogger.w('Could not subscribe to connectivity changes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
setup();
|
||||
}
|
||||
|
||||
static bool _hasWifiOrEthernet(List<ConnectivityResult> results) =>
|
||||
results.contains(ConnectivityResult.wifi) || results.contains(ConnectivityResult.ethernet);
|
||||
|
||||
/// Run auto-delete (if enabled) and then a sync-rule pass.
|
||||
///
|
||||
/// When [targetKeys] is non-null, only those rules are re-evaluated
|
||||
/// (cooldown doesn't apply — targeted runs are always "we know this
|
||||
/// changed"). When null, every rule runs via the executor, with [force]
|
||||
/// gating the cooldown: `true` for user-initiated drains, `false` for
|
||||
/// background probes like a connectivity reconnect.
|
||||
Future<void> _autoDeleteAndSync(
|
||||
DownloadProvider downloadProvider, {
|
||||
List<String>? targetKeys,
|
||||
bool force = false,
|
||||
}) async {
|
||||
if (_isAutoDeleteRunning) return;
|
||||
_isAutoDeleteRunning = true;
|
||||
try {
|
||||
await downloadProvider.refreshMetadataFromCache();
|
||||
final activeKey = VideoPlayerScreenState.activeRatingKey;
|
||||
final settings = SettingsService.instanceOrNull;
|
||||
if (settings != null && settings.getAutoRemoveWatchedDownloads()) {
|
||||
final deleted = await downloadProvider.autoDeleteWatchedDownloads(activeRatingKey: activeKey);
|
||||
if (deleted.isNotEmpty) {
|
||||
final msg = deleted.length == 1
|
||||
? t.messages.autoRemovedWatchedDownload(title: deleted.first)
|
||||
: t.messages.autoRemovedWatchedDownload(title: '${deleted.length} items');
|
||||
showMainSnackBar(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetKeys != null) {
|
||||
for (final key in targetKeys) {
|
||||
if (!downloadProvider.hasSyncRule(key)) continue;
|
||||
final result = await downloadProvider.executeSyncRuleFor(key, _serverManager);
|
||||
if (result != null && result.queuedCount > 0) {
|
||||
final title = result.title ?? 'Unknown';
|
||||
showMainSnackBar(t.downloads.syncedNewEpisodes(count: '1', title: '$title (${result.queuedCount})'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final synced = await downloadProvider.executeSyncRules(_serverManager, force: force);
|
||||
if (synced.isNotEmpty) {
|
||||
showMainSnackBar(t.downloads.syncedNewEpisodes(count: synced.length.toString(), title: synced.first));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_isAutoDeleteRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
// App came back to foreground - trigger sync check and start new session
|
||||
_offlineWatchSyncService.onAppResumed();
|
||||
TraktSyncService.instance.flushQueue();
|
||||
InAppReviewService.instance.startSession();
|
||||
// Re-probe servers — mobile OS may have dropped TCP connections during doze/sleep
|
||||
_serverManager.checkServerHealth();
|
||||
_serverManager.reconnectOfflineServers();
|
||||
// Re-probe servers — mobile OS may have dropped TCP connections during doze/sleep.
|
||||
// On desktop, resumed fires on every window focus (alt-tab), so apply a cooldown
|
||||
// to avoid piling up network probes from rapid alt-tabbing.
|
||||
final now = DateTime.now();
|
||||
final cooldown = (Platform.isIOS || Platform.isAndroid)
|
||||
? const Duration(seconds: 10)
|
||||
: const Duration(minutes: 2);
|
||||
if (now.difference(_lastResumeProbe) >= cooldown) {
|
||||
_lastResumeProbe = now;
|
||||
// Await health check before reconnecting so stale "online" servers
|
||||
// get marked offline and included in the reconnection sweep.
|
||||
unawaited(() async {
|
||||
await _serverManager.checkServerHealth();
|
||||
await _serverManager.reconnectOfflineServers();
|
||||
}());
|
||||
}
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
// App went to background or is closing - end session
|
||||
// Database is session-scoped and must survive suspend/resume.
|
||||
// Closing here would kill the Drift isolate channel while services
|
||||
// (sync, downloads, cache) still hold references to the executor.
|
||||
// SQLite WAL mode handles process death; desktop uses onExitRequested.
|
||||
InAppReviewService.instance.endSession();
|
||||
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||
if (ProcessInfo.currentRss > 1024 * 1024 * 1024) {
|
||||
// 1GB
|
||||
_evictImageCaches();
|
||||
}
|
||||
}
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.hidden:
|
||||
// Transitional states - don't trigger session events
|
||||
@@ -259,7 +583,6 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => MultiServerProvider(_serverManager, _aggregationService)),
|
||||
ChangeNotifierProvider(create: (context) => ServerStateProvider()),
|
||||
// Offline mode provider - depends on MultiServerProvider
|
||||
ChangeNotifierProxyProvider<MultiServerProvider, OfflineModeProvider>(
|
||||
create: (_) {
|
||||
@@ -274,18 +597,44 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
// Download provider
|
||||
ChangeNotifierProvider(create: (context) => DownloadProvider(downloadManager: _downloadManager)),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => DownloadProvider(downloadManager: _downloadManager, database: _appDatabase),
|
||||
),
|
||||
// Offline watch sync service
|
||||
ChangeNotifierProvider<OfflineWatchSyncService>(
|
||||
create: (context) {
|
||||
final offlineModeProvider = context.read<OfflineModeProvider>();
|
||||
final downloadProvider = context.read<DownloadProvider>();
|
||||
|
||||
// Wire up callback to refresh download provider after watch state sync
|
||||
_offlineWatchSyncService.onWatchStatesRefreshed = () {
|
||||
downloadProvider.refreshMetadataFromCache();
|
||||
// Offline-sync drain replays a batch of queued watch actions without
|
||||
// per-item data, so we can't target rules — force a full pass.
|
||||
_offlineWatchSyncService.onWatchStatesRefreshed = () async {
|
||||
await _autoDeleteAndSync(downloadProvider, force: true);
|
||||
};
|
||||
|
||||
// In-session watch events carry the episode's parent chain, so we
|
||||
// only re-evaluate rules that actually cover the watched item —
|
||||
// leaves unrelated collection/playlist rules alone. Debounced so
|
||||
// binge-watching coalesces into one pass.
|
||||
_watchStateSubscription = WatchStateNotifier().stream.listen((event) {
|
||||
if (event.changeType != WatchStateChangeType.watched) return;
|
||||
if (VideoPlayerScreenState.activeRatingKey == event.ratingKey) return;
|
||||
|
||||
_pendingSyncKeys.add(event.globalKey);
|
||||
for (final parentKey in event.parentChain) {
|
||||
_pendingSyncKeys.add(buildGlobalKey(event.serverId, parentKey));
|
||||
}
|
||||
|
||||
_syncDebounce?.cancel();
|
||||
_syncDebounce = Timer(const Duration(seconds: 5), () {
|
||||
final keys = _pendingSyncKeys.toList();
|
||||
_pendingSyncKeys.clear();
|
||||
_autoDeleteAndSync(downloadProvider, targetKeys: keys);
|
||||
});
|
||||
});
|
||||
|
||||
_startConnectivitySyncTrigger(downloadProvider);
|
||||
|
||||
_offlineWatchSyncService.startConnectivityMonitoring(offlineModeProvider);
|
||||
return _offlineWatchSyncService;
|
||||
},
|
||||
@@ -295,20 +644,17 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
create: (context) => OfflineWatchProvider(
|
||||
syncService: _offlineWatchSyncService,
|
||||
downloadProvider: context.read<DownloadProvider>(),
|
||||
apiCache: PlexApiCache.instance,
|
||||
),
|
||||
update: (_, syncService, downloadProvider, previous) {
|
||||
return previous ??
|
||||
OfflineWatchProvider(
|
||||
syncService: syncService,
|
||||
downloadProvider: downloadProvider,
|
||||
apiCache: PlexApiCache.instance,
|
||||
);
|
||||
return previous ?? OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
|
||||
},
|
||||
),
|
||||
// Existing providers
|
||||
ChangeNotifierProvider(create: (context) => UserProfileProvider()),
|
||||
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||
// Trakt account — depends on UserProfileProvider for per-profile token scoping.
|
||||
// Hydrated and rebound in `_TraktBootstrap` below.
|
||||
ChangeNotifierProvider(create: (context) => TraktAccountProvider()),
|
||||
ChangeNotifierProvider(create: (context) => SettingsProvider(), lazy: true),
|
||||
ChangeNotifierProvider(create: (context) => HiddenLibrariesProvider(), lazy: true),
|
||||
ChangeNotifierProvider(create: (context) => LibrariesProvider()),
|
||||
@@ -320,15 +666,30 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
child: Consumer<ThemeProvider>(
|
||||
builder: (context, themeProvider, child) {
|
||||
return TranslationProvider(
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
title: t.app.title,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: themeProvider.lightTheme,
|
||||
darkTheme: themeProvider.darkTheme,
|
||||
themeMode: themeProvider.materialThemeMode,
|
||||
navigatorObservers: [routeObserver, BackKeySuppressorObserver()],
|
||||
home: const OrientationAwareSetup(),
|
||||
child: _TraktProfileBootstrap(
|
||||
child: Listener(
|
||||
onPointerDown: (event) {
|
||||
if ((event.buttons & kBackMouseButton) != 0) {
|
||||
rootNavigatorKey.currentState?.maybePop();
|
||||
}
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: InputModeTracker(
|
||||
child: MaterialApp(
|
||||
title: t.app.title,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: themeProvider.lightTheme,
|
||||
darkTheme: themeProvider.darkTheme,
|
||||
themeMode: themeProvider.materialThemeMode,
|
||||
navigatorKey: rootNavigatorKey,
|
||||
navigatorObservers: [routeObserver, BackKeySuppressorObserver()],
|
||||
home: const OrientationAwareSetup(),
|
||||
builder: (context, child) => ScaffoldMessenger(
|
||||
key: rootScaffoldMessengerKey,
|
||||
child: Scaffold(backgroundColor: Colors.transparent, body: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -338,6 +699,61 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hydrates the [TraktAccountProvider] with the active Plex profile's session
|
||||
/// and rebinds the scrobble + sync services whenever the user switches.
|
||||
///
|
||||
/// Lives high in the widget tree (above MaterialApp) so the listener survives
|
||||
/// route changes.
|
||||
class _TraktProfileBootstrap extends StatefulWidget {
|
||||
final Widget child;
|
||||
const _TraktProfileBootstrap({required this.child});
|
||||
|
||||
@override
|
||||
State<_TraktProfileBootstrap> createState() => _TraktProfileBootstrapState();
|
||||
}
|
||||
|
||||
class _TraktProfileBootstrapState extends State<_TraktProfileBootstrap> {
|
||||
UserProfileProvider? _profile;
|
||||
TraktAccountProvider? _trakt;
|
||||
String? _lastUuid;
|
||||
bool _hydrated = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final profile = context.read<UserProfileProvider>();
|
||||
final trakt = context.read<TraktAccountProvider>();
|
||||
|
||||
if (!identical(_profile, profile)) {
|
||||
_profile?.removeListener(_onProfileChanged);
|
||||
_profile = profile;
|
||||
_profile!.addListener(_onProfileChanged);
|
||||
}
|
||||
_trakt = trakt;
|
||||
|
||||
if (!_hydrated) {
|
||||
_hydrated = true;
|
||||
_onProfileChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void _onProfileChanged() {
|
||||
final uuid = _profile?.currentUser?.uuid;
|
||||
if (uuid == _lastUuid) return;
|
||||
_lastUuid = uuid;
|
||||
_trakt?.onActiveProfileChanged(uuid);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_profile?.removeListener(_onProfileChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
class OrientationAwareSetup extends StatefulWidget {
|
||||
const OrientationAwareSetup({super.key});
|
||||
|
||||
@@ -372,6 +788,9 @@ class SetupScreen extends StatefulWidget {
|
||||
class _SetupScreenState extends State<SetupScreen> {
|
||||
String _statusMessage = '';
|
||||
|
||||
// Per-server connection status: serverId -> (name, connected?)
|
||||
final Map<String, (String name, bool? connected)> _serverStatus = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -390,11 +809,20 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
|
||||
// Check network connectivity early to fast-path airplane mode.
|
||||
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
|
||||
final connectivityResult = await Connectivity().checkConnectivity().timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () => [ConnectivityResult.other],
|
||||
);
|
||||
final hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
|
||||
bool hasNetwork;
|
||||
Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup'));
|
||||
try {
|
||||
final connectivityResult = await Connectivity().checkConnectivity().timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () => [ConnectivityResult.other],
|
||||
);
|
||||
hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
|
||||
} catch (e) {
|
||||
// connectivity_plus throws DBusServiceUnknownException on Linux without NetworkManager
|
||||
hasNetwork = true;
|
||||
}
|
||||
|
||||
Sentry.addBreadcrumb(Breadcrumb(message: 'Network check done: hasNetwork=$hasNetwork', category: 'setup'));
|
||||
|
||||
if (hasNetwork) {
|
||||
_setStatus(t.common.refreshingServers);
|
||||
@@ -435,8 +863,18 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'setup'));
|
||||
_setStatus(t.common.connectingToServers);
|
||||
|
||||
// Populate per-server status for splash display
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
for (final server in servers) {
|
||||
_serverStatus[server.clientIdentifier] = (server.name, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await ServerConnectionOrchestrator.connectAndInitialize(
|
||||
servers: servers,
|
||||
@@ -444,11 +882,21 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
librariesProvider: context.read<LibrariesProvider>(),
|
||||
syncService: context.read<OfflineWatchSyncService>(),
|
||||
clientIdentifier: storage.getClientIdentifier(),
|
||||
onServerStatus: (serverId, success) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
final existing = _serverStatus[serverId];
|
||||
if (existing != null) {
|
||||
_serverStatus[serverId] = (existing.$1, success);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (result.hasConnections) {
|
||||
if (result.hasConnections && result.firstClient != null) {
|
||||
// Resume any downloads that were interrupted by app kill
|
||||
final downloadProvider = context.read<DownloadProvider>();
|
||||
downloadProvider.ensureInitialized().then((_) {
|
||||
@@ -474,30 +922,85 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStatusText(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
_statusMessage,
|
||||
key: ValueKey(_statusMessage),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildServerStatusList(BuildContext context) {
|
||||
if (_serverStatus.isEmpty) return const SizedBox.shrink();
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final dimColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5);
|
||||
const coralColor = Color(0xFFE5A00D);
|
||||
const successColor = Color(0xFF4CAF50);
|
||||
const failColor = Color(0xFFEF5350);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _serverStatus.entries.map((entry) {
|
||||
final (name, connected) = entry.value;
|
||||
final Widget statusIcon;
|
||||
if (connected == null) {
|
||||
statusIcon = const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1.5, color: coralColor),
|
||||
);
|
||||
} else if (connected) {
|
||||
statusIcon = const Icon(Icons.check_circle, size: 14, color: successColor);
|
||||
} else {
|
||||
statusIcon = const Icon(Icons.cancel, size: 14, color: failColor);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
statusIcon,
|
||||
const SizedBox(width: 8),
|
||||
Text(name, style: textTheme.bodySmall?.copyWith(color: dimColor)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const coralColor = Color(0xFFE5A00D);
|
||||
return ColoredBox(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Icon dead-center, matching Android 12+ splash position.
|
||||
// 192dp accounts for the 16% inset in ic_launcher.xml.
|
||||
Center(child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288)),
|
||||
// Status text below center, independent of icon position.
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: MediaQuery.of(context).size.height * 0.5 - 140,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
_statusMessage,
|
||||
key: ValueKey(_statusMessage),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
|
||||
),
|
||||
bottom: MediaQuery.of(context).size.height * 0.5 - 170,
|
||||
child: _buildStatusText(context),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: MediaQuery.of(context).size.height * 0.5 + 180,
|
||||
child: Center(
|
||||
child: _serverStatus.isEmpty
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: coralColor),
|
||||
)
|
||||
: _buildServerStatusList(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -40,6 +40,28 @@ mixin GridFocusNodeMixin<T extends StatefulWidget> on State<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evict focus nodes far from [centerIndex], keeping at most [keepCount] around it.
|
||||
void evictDistantFocusNodes(int centerIndex, {int keepCount = 200}) {
|
||||
if (gridItemFocusNodes.length <= keepCount) return;
|
||||
|
||||
final halfKeep = keepCount ~/ 2;
|
||||
final keepStart = centerIndex - halfKeep;
|
||||
final keepEnd = centerIndex + halfKeep;
|
||||
|
||||
final keysToRemove = <int>[];
|
||||
for (final key in gridItemFocusNodes.keys) {
|
||||
if (key < keepStart || key > keepEnd) {
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
for (final key in keysToRemove) {
|
||||
final node = gridItemFocusNodes.remove(key);
|
||||
if (node != null && !node.hasFocus) {
|
||||
node.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispose all grid-item focus nodes.
|
||||
void disposeGridFocusNodes() {
|
||||
for (final node in gridItemFocusNodes.values) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Provides a safe [setState] wrapper for async callbacks.
|
||||
mixin MountedSetStateMixin<T extends StatefulWidget> on State<T> {
|
||||
void setStateIfMounted(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
setState(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../services/plex_client.dart';
|
||||
import '../utils/global_key_utils.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
|
||||
/// Shared helpers for screens bound to a single [PlexMetadata] item/server.
|
||||
mixin ServerBoundMediaMixin<T extends StatefulWidget> on State<T> {
|
||||
PlexMetadata get serverBoundMetadata;
|
||||
|
||||
bool get isServerBoundOffline => false;
|
||||
|
||||
String? get serverBoundServerId => serverBoundMetadata.serverId;
|
||||
|
||||
String toServerBoundGlobalKey(String ratingKey, {String? serverId}) =>
|
||||
buildGlobalKey(serverId ?? serverBoundServerId ?? '', ratingKey);
|
||||
|
||||
PlexClient? getServerBoundClient(BuildContext context) =>
|
||||
context.getClientForMetadataOrNull(serverBoundMetadata, isOffline: isServerBoundOffline);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/gamepad_service.dart';
|
||||
import '../screens/main_screen.dart';
|
||||
import '../widgets/focusable_tab_chip.dart';
|
||||
|
||||
/// Mixin that provides common tab navigation infrastructure.
|
||||
///
|
||||
@@ -12,7 +13,7 @@ import '../screens/main_screen.dart';
|
||||
/// - Tab bar back navigation to sidebar
|
||||
///
|
||||
/// Subclasses must provide [tabChipFocusNodes] — one [FocusNode] per tab.
|
||||
mixin TabNavigationMixin<T extends StatefulWidget> on State<T>, SingleTickerProviderStateMixin<T> {
|
||||
mixin TabNavigationMixin<T extends StatefulWidget> on State<T>, TickerProviderStateMixin<T> {
|
||||
late TabController tabController;
|
||||
|
||||
/// When true, suppress auto-focus in tabs (used when navigating via tab bar).
|
||||
@@ -83,4 +84,51 @@ mixin TabNavigationMixin<T extends StatefulWidget> on State<T>, SingleTickerProv
|
||||
void onTabBarBack() {
|
||||
MainScreenFocusScope.of(context)?.focusSidebar();
|
||||
}
|
||||
|
||||
/// Shared tab chip builder — eliminates duplication between screens.
|
||||
Widget buildTabChip(
|
||||
String label,
|
||||
int index, {
|
||||
required VoidCallback onSelectWhenActive,
|
||||
required VoidCallback onNavigateDown,
|
||||
VoidCallback? onNavigateRightFromLast,
|
||||
}) {
|
||||
final isSelected = tabController.index == index;
|
||||
return FocusableTabChip(
|
||||
label: label,
|
||||
isSelected: isSelected,
|
||||
focusNode: getTabChipFocusNode(index),
|
||||
onSelect: () {
|
||||
if (isSelected) {
|
||||
onSelectWhenActive();
|
||||
} else {
|
||||
setState(() {
|
||||
tabController.index = index;
|
||||
});
|
||||
}
|
||||
},
|
||||
onNavigateLeft: index > 0
|
||||
? () {
|
||||
final newIndex = index - 1;
|
||||
setState(() {
|
||||
suppressAutoFocus = true;
|
||||
tabController.index = newIndex;
|
||||
});
|
||||
getTabChipFocusNode(newIndex).requestFocus();
|
||||
}
|
||||
: onTabBarBack,
|
||||
onNavigateRight: index < tabCount - 1
|
||||
? () {
|
||||
final newIndex = index + 1;
|
||||
setState(() {
|
||||
suppressAutoFocus = true;
|
||||
tabController.index = newIndex;
|
||||
});
|
||||
getTabChipFocusNode(newIndex).requestFocus();
|
||||
}
|
||||
: onNavigateRightFromLast,
|
||||
onNavigateDown: onNavigateDown,
|
||||
onBack: onTabBarBack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,6 @@ class RemoteDevice {
|
||||
|
||||
@JsonSerializable()
|
||||
class RemoteSession {
|
||||
final String sessionId;
|
||||
final String pin;
|
||||
@JsonKey(unknownEnumValue: RemoteSessionRole.remote)
|
||||
final RemoteSessionRole role;
|
||||
@JsonKey(unknownEnumValue: RemoteSessionStatus.disconnected)
|
||||
@@ -67,8 +65,6 @@ class RemoteSession {
|
||||
final String? errorMessage;
|
||||
|
||||
RemoteSession({
|
||||
required this.sessionId,
|
||||
required this.pin,
|
||||
required this.role,
|
||||
this.status = RemoteSessionStatus.disconnected,
|
||||
this.connectedDevice,
|
||||
@@ -85,8 +81,6 @@ class RemoteSession {
|
||||
Map<String, dynamic> toJson() => _$RemoteSessionToJson(this);
|
||||
|
||||
RemoteSession copyWith({
|
||||
String? sessionId,
|
||||
String? pin,
|
||||
RemoteSessionRole? role,
|
||||
RemoteSessionStatus? status,
|
||||
RemoteDevice? connectedDevice,
|
||||
@@ -96,8 +90,6 @@ class RemoteSession {
|
||||
bool clearErrorMessage = false,
|
||||
}) {
|
||||
return RemoteSession(
|
||||
sessionId: sessionId ?? this.sessionId,
|
||||
pin: pin ?? this.pin,
|
||||
role: role ?? this.role,
|
||||
status: status ?? this.status,
|
||||
connectedDevice: clearConnectedDevice ? null : (connectedDevice ?? this.connectedDevice),
|
||||
|
||||
@@ -10,47 +10,61 @@ RemoteDevice _$RemoteDeviceFromJson(Map<String, dynamic> json) => RemoteDevice(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
platform: json['platform'] as String,
|
||||
connectedAt: json['connectedAt'] == null ? null : DateTime.parse(json['connectedAt'] as String),
|
||||
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map((k, e) => MapEntry(k, e as bool)),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'platform': instance.platform,
|
||||
'connectedAt': instance.connectedAt.toIso8601String(),
|
||||
'capabilities': instance.capabilities,
|
||||
};
|
||||
|
||||
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) => RemoteSession(
|
||||
sessionId: json['sessionId'] as String,
|
||||
pin: json['pin'] as String,
|
||||
role: $enumDecode(_$RemoteSessionRoleEnumMap, json['role'], unknownValue: RemoteSessionRole.remote),
|
||||
status:
|
||||
$enumDecodeNullable(
|
||||
_$RemoteSessionStatusEnumMap,
|
||||
json['status'],
|
||||
unknownValue: RemoteSessionStatus.disconnected,
|
||||
) ??
|
||||
RemoteSessionStatus.disconnected,
|
||||
connectedDevice: json['connectedDevice'] == null
|
||||
connectedAt: json['connectedAt'] == null
|
||||
? null
|
||||
: RemoteDevice.fromJson(json['connectedDevice'] as Map<String, dynamic>),
|
||||
createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String),
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
: DateTime.parse(json['connectedAt'] as String),
|
||||
capabilities: (json['capabilities'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as bool),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) => <String, dynamic>{
|
||||
'sessionId': instance.sessionId,
|
||||
'pin': instance.pin,
|
||||
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
|
||||
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
|
||||
'connectedDevice': instance.connectedDevice,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'errorMessage': instance.errorMessage,
|
||||
};
|
||||
Map<String, dynamic> _$RemoteDeviceToJson(RemoteDevice instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'platform': instance.platform,
|
||||
'connectedAt': instance.connectedAt.toIso8601String(),
|
||||
'capabilities': instance.capabilities,
|
||||
};
|
||||
|
||||
const _$RemoteSessionRoleEnumMap = {RemoteSessionRole.host: 'host', RemoteSessionRole.remote: 'remote'};
|
||||
RemoteSession _$RemoteSessionFromJson(Map<String, dynamic> json) =>
|
||||
RemoteSession(
|
||||
role: $enumDecode(
|
||||
_$RemoteSessionRoleEnumMap,
|
||||
json['role'],
|
||||
unknownValue: RemoteSessionRole.remote,
|
||||
),
|
||||
status:
|
||||
$enumDecodeNullable(
|
||||
_$RemoteSessionStatusEnumMap,
|
||||
json['status'],
|
||||
unknownValue: RemoteSessionStatus.disconnected,
|
||||
) ??
|
||||
RemoteSessionStatus.disconnected,
|
||||
connectedDevice: json['connectedDevice'] == null
|
||||
? null
|
||||
: RemoteDevice.fromJson(
|
||||
json['connectedDevice'] as Map<String, dynamic>,
|
||||
),
|
||||
createdAt: json['createdAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['createdAt'] as String),
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RemoteSessionToJson(RemoteSession instance) =>
|
||||
<String, dynamic>{
|
||||
'role': _$RemoteSessionRoleEnumMap[instance.role]!,
|
||||
'status': _$RemoteSessionStatusEnumMap[instance.status]!,
|
||||
'connectedDevice': instance.connectedDevice,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'errorMessage': instance.errorMessage,
|
||||
};
|
||||
|
||||
const _$RemoteSessionRoleEnumMap = {
|
||||
RemoteSessionRole.host: 'host',
|
||||
RemoteSessionRole.remote: 'remote',
|
||||
};
|
||||
|
||||
const _$RemoteSessionStatusEnumMap = {
|
||||
RemoteSessionStatus.disconnected: 'disconnected',
|
||||
|
||||
@@ -41,13 +41,6 @@ class DownloadProgress {
|
||||
String get downloadedFormatted => ByteFormatter.formatBytes(downloadedBytes);
|
||||
String get totalFormatted => ByteFormatter.formatBytes(totalBytes);
|
||||
|
||||
Duration? get estimatedTimeRemaining {
|
||||
if (speed <= 0 || totalBytes <= 0) return null;
|
||||
final remainingBytes = totalBytes - downloadedBytes;
|
||||
if (remainingBytes <= 0) return Duration.zero;
|
||||
return Duration(seconds: (remainingBytes / speed).round());
|
||||
}
|
||||
|
||||
/// Check if this progress update includes artwork paths
|
||||
bool get hasArtworkPaths => thumbPath != null;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../data/hid_key_labels.dart';
|
||||
|
||||
/// Modifier keys that can be combined with a primary key to form a hotkey.
|
||||
///
|
||||
/// Each value holds the physical keys that correspond to it (e.g. shift maps
|
||||
@@ -40,7 +42,7 @@ String physicalKeyLabel(PhysicalKeyboardKey key) {
|
||||
final macLabel = _macKeyLabels[key.usbHidUsage];
|
||||
if (macLabel != null) return macLabel;
|
||||
}
|
||||
return _knownKeyLabels[key.usbHidUsage] ?? key.debugName ?? 'Unknown';
|
||||
return hidKeyLabels[key.usbHidUsage] ?? 'Key 0x${key.usbHidUsage.toRadixString(16).padLeft(8, '0')}';
|
||||
}
|
||||
|
||||
/// macOS-specific overrides for keys that have standard symbols.
|
||||
@@ -69,89 +71,3 @@ const _macKeyLabels = <int, String>{
|
||||
0x000700e7: '\u2318', // metaRight (Command) → ⌘
|
||||
0x00000012: 'fn', // fn
|
||||
};
|
||||
|
||||
const _knownKeyLabels = <int, String>{
|
||||
0x00070004: 'A', // keyA
|
||||
0x00070005: 'B', // keyB
|
||||
0x00070006: 'C', // keyC
|
||||
0x00070007: 'D', // keyD
|
||||
0x00070008: 'E', // keyE
|
||||
0x00070009: 'F', // keyF
|
||||
0x0007000a: 'G', // keyG
|
||||
0x0007000b: 'H', // keyH
|
||||
0x0007000c: 'I', // keyI
|
||||
0x0007000d: 'J', // keyJ
|
||||
0x0007000e: 'K', // keyK
|
||||
0x0007000f: 'L', // keyL
|
||||
0x00070010: 'M', // keyM
|
||||
0x00070011: 'N', // keyN
|
||||
0x00070012: 'O', // keyO
|
||||
0x00070013: 'P', // keyP
|
||||
0x00070014: 'Q', // keyQ
|
||||
0x00070015: 'R', // keyR
|
||||
0x00070016: 'S', // keyS
|
||||
0x00070017: 'T', // keyT
|
||||
0x00070018: 'U', // keyU
|
||||
0x00070019: 'V', // keyV
|
||||
0x0007001a: 'W', // keyW
|
||||
0x0007001b: 'X', // keyX
|
||||
0x0007001c: 'Y', // keyY
|
||||
0x0007001d: 'Z', // keyZ
|
||||
0x0007001e: '1', // digit1
|
||||
0x0007001f: '2', // digit2
|
||||
0x00070020: '3', // digit3
|
||||
0x00070021: '4', // digit4
|
||||
0x00070022: '5', // digit5
|
||||
0x00070023: '6', // digit6
|
||||
0x00070024: '7', // digit7
|
||||
0x00070025: '8', // digit8
|
||||
0x00070026: '9', // digit9
|
||||
0x00070027: '0', // digit0
|
||||
0x00070028: 'Enter', // enter
|
||||
0x00070029: 'Escape', // escape
|
||||
0x0007002a: 'Backspace', // backspace
|
||||
0x0007002b: 'Tab', // tab
|
||||
0x0007002c: 'Space', // space
|
||||
0x0007002d: '=', // equal
|
||||
0x0007002e: '-', // minus
|
||||
0x0007002f: '[', // bracketLeft
|
||||
0x00070030: ']', // bracketRight
|
||||
0x00070031: r'\', // backslash
|
||||
0x00070033: ';', // semicolon
|
||||
0x00070034: "'", // quote
|
||||
0x00070035: '`', // backquote
|
||||
0x00070036: ',', // comma
|
||||
0x00070037: '.', // period
|
||||
0x00070038: '/', // slash
|
||||
0x00070039: 'CapsLock', // capsLock
|
||||
0x0007003a: 'F1',
|
||||
0x0007003b: 'F2',
|
||||
0x0007003c: 'F3',
|
||||
0x0007003d: 'F4',
|
||||
0x0007003e: 'F5',
|
||||
0x0007003f: 'F6',
|
||||
0x00070040: 'F7',
|
||||
0x00070041: 'F8',
|
||||
0x00070042: 'F9',
|
||||
0x00070043: 'F10',
|
||||
0x00070044: 'F11',
|
||||
0x00070045: 'F12',
|
||||
0x0007004a: 'Home', // home
|
||||
0x0007004b: 'Page Up', // pageUp
|
||||
0x0007004c: 'Delete', // delete
|
||||
0x0007004d: 'End', // end
|
||||
0x0007004e: 'Page Down', // pageDown
|
||||
0x0007004f: 'Arrow Right', // arrowRight
|
||||
0x00070050: 'Arrow Left', // arrowLeft
|
||||
0x00070051: 'Arrow Down', // arrowDown
|
||||
0x00070052: 'Arrow Up', // arrowUp
|
||||
0x000700e0: 'Ctrl', // controlLeft
|
||||
0x000700e1: 'Shift', // shiftLeft
|
||||
0x000700e2: 'Alt', // altLeft
|
||||
0x000700e3: 'Meta', // metaLeft
|
||||
0x000700e4: 'Ctrl', // controlRight
|
||||
0x000700e5: 'Shift', // shiftRight
|
||||
0x000700e6: 'Alt', // altRight
|
||||
0x000700e7: 'Meta', // metaRight
|
||||
0x00000012: 'Fn', // fn
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/// Represents the seekable capture buffer for a live TV transcode session.
|
||||
///
|
||||
/// Extracted from the `TranscodeSession` element in the tune response:
|
||||
/// - [startedAt] = `timeStamp` (epoch seconds, the buffer origin)
|
||||
/// - [seekStartSeconds] = `minOffsetAvailable` (earliest seekable offset)
|
||||
/// - [seekEndSeconds] = `maxOffsetAvailable` (latest seekable offset / live edge)
|
||||
class CaptureBuffer {
|
||||
final double startedAt;
|
||||
final double seekStartSeconds;
|
||||
final double seekEndSeconds;
|
||||
|
||||
const CaptureBuffer({required this.startedAt, required this.seekStartSeconds, required this.seekEndSeconds});
|
||||
|
||||
/// Absolute epoch second of the earliest seekable point.
|
||||
int get seekableStartEpoch => (startedAt + seekStartSeconds).round();
|
||||
|
||||
/// Absolute epoch second of the latest seekable point (≈ live edge).
|
||||
int get seekableEndEpoch => (startedAt + seekEndSeconds).round();
|
||||
|
||||
/// Duration of the seekable range in seconds.
|
||||
int get seekableDurationSeconds => (seekEndSeconds - seekStartSeconds).round();
|
||||
|
||||
/// Parse from a TranscodeSession JSON map. Returns null if required fields are missing.
|
||||
/// Values may be num or String depending on whether the server returned JSON or XML.
|
||||
static CaptureBuffer? fromTranscodeSession(Map<String, dynamic> session) {
|
||||
final timeStamp = _parseDouble(session['timeStamp']);
|
||||
final minOffset = _parseDouble(session['minOffsetAvailable']);
|
||||
final maxOffset = _parseDouble(session['maxOffsetAvailable']);
|
||||
if (timeStamp == null || minOffset == null || maxOffset == null) return null;
|
||||
return CaptureBuffer(startedAt: timeStamp, seekStartSeconds: minOffset, seekEndSeconds: maxOffset);
|
||||
}
|
||||
|
||||
static double? _parseDouble(dynamic value) {
|
||||
if (value is num) return value.toDouble();
|
||||
if (value is String) return double.tryParse(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'CaptureBuffer(startedAt: $startedAt, seek: $seekStartSeconds..$seekEndSeconds, '
|
||||
'range: $seekableStartEpoch..$seekableEndEpoch)';
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import '../utils/json_utils.dart';
|
||||
|
||||
/// Represents a Live TV channel from the EPG
|
||||
class LiveTvChannel {
|
||||
final String key;
|
||||
@@ -34,17 +36,27 @@ class LiveTvChannel {
|
||||
|
||||
factory LiveTvChannel.fromJson(Map<String, dynamic> json) {
|
||||
return LiveTvChannel(
|
||||
key: json['key'] as String? ?? json['ratingKey'] as String? ?? json['identifier'] as String? ?? json['channelIdentifier'] as String? ?? '',
|
||||
identifier: json['identifier'] as String? ?? json['channelIdentifier'] as String?,
|
||||
key:
|
||||
json['key'] as String? ??
|
||||
json['ratingKey'] as String? ??
|
||||
json['identifier'] as String? ??
|
||||
json['id'] as String? ??
|
||||
json['channelIdentifier'] as String? ??
|
||||
'',
|
||||
identifier: json['identifier'] as String? ?? json['id'] as String? ?? json['channelIdentifier'] as String?,
|
||||
callSign: json['callSign'] as String?,
|
||||
title: json['title'] as String? ?? json['callSign'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
art: json['art'] as String?,
|
||||
number: json['number'] as String? ?? json['channelNumber'] as String? ?? json['channelVcn']?.toString(),
|
||||
hd: json['hd'] == true || json['hd'] == 1,
|
||||
number:
|
||||
json['number'] as String? ??
|
||||
json['channelNumber'] as String? ??
|
||||
json['channelVcn']?.toString() ??
|
||||
json['vcn']?.toString(),
|
||||
hd: flexibleBool(json['hd']),
|
||||
lineup: json['lineup'] as String?,
|
||||
slug: json['slug'] as String?,
|
||||
drm: json['drm'] == true || json['drm'] == 1,
|
||||
drm: flexibleBool(json['drm']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,3 +81,44 @@ class LiveTvChannel {
|
||||
/// Display name: prefer callSign, fallback to title
|
||||
String get displayName => callSign ?? title ?? 'Channel $number';
|
||||
}
|
||||
|
||||
/// A channel entry in the Plex cloud favorites list.
|
||||
/// Stored at `https://epg.provider.plex.tv/settings/favoriteChannels`.
|
||||
class FavoriteChannel {
|
||||
final String source;
|
||||
final String id;
|
||||
final String? title;
|
||||
final String? thumb;
|
||||
final String? vcn;
|
||||
|
||||
FavoriteChannel({required this.source, required this.id, this.title, this.thumb, this.vcn});
|
||||
|
||||
factory FavoriteChannel.fromJson(Map<String, dynamic> json) {
|
||||
return FavoriteChannel(
|
||||
source: json['source'] as String? ?? '',
|
||||
id: json['id'] as String? ?? json['key'] as String? ?? '',
|
||||
title: json['title'] as String?,
|
||||
thumb: json['thumb'] as String?,
|
||||
vcn: json['vcn'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'source': source,
|
||||
'id': id,
|
||||
if (title != null) 'title': title,
|
||||
if (thumb != null) 'thumb': thumb,
|
||||
if (vcn != null) 'vcn': vcn,
|
||||
};
|
||||
|
||||
/// Create from a [LiveTvChannel] and a source URI.
|
||||
factory FavoriteChannel.fromLiveTvChannel(LiveTvChannel channel, String source) {
|
||||
return FavoriteChannel(
|
||||
source: source,
|
||||
id: channel.key,
|
||||
title: channel.title ?? channel.callSign,
|
||||
thumb: channel.thumb,
|
||||
vcn: channel.number,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '../utils/json_utils.dart';
|
||||
|
||||
/// Represents a Plex Live TV DVR device (e.g., HDHomeRun tuner, IPTV provider)
|
||||
class LiveTvDvr {
|
||||
final String key;
|
||||
@@ -74,7 +76,7 @@ class ChannelMapping {
|
||||
return ChannelMapping(
|
||||
channelKey: json['channelKey'] as String?,
|
||||
deviceIdentifier: json['deviceIdentifier'] as String?,
|
||||
enabled: json['enabled'] == true || json['enabled'] == 1 || json['enabled'] == '1',
|
||||
enabled: flexibleBool(json['enabled']),
|
||||
lineupIdentifier: json['lineupIdentifier'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '../utils/json_utils.dart';
|
||||
|
||||
/// Represents an EPG program entry (what's on a channel at a given time)
|
||||
class LiveTvProgram {
|
||||
final String? key;
|
||||
@@ -42,11 +44,27 @@ class LiveTvProgram {
|
||||
this.premiere,
|
||||
});
|
||||
|
||||
factory LiveTvProgram.fromJson(Map<String, dynamic> json) {
|
||||
// Grid endpoint nests timing/channel info inside Media[0] and Channel[0]
|
||||
final media = (json['Media'] as List?)?.firstOrNull as Map<String, dynamic>?;
|
||||
factory LiveTvProgram.fromJson(Map<String, dynamic> json, {Map<String, dynamic>? mediaOverride}) {
|
||||
// Grid endpoint nests timing/channel info inside Media[] and Channel[].
|
||||
// When mediaOverride is supplied, the caller is pinning this parse to a
|
||||
// specific airing (one Media entry); treat it as authoritative for
|
||||
// begin/end/channel fields.
|
||||
final hasOverride = mediaOverride != null;
|
||||
final media = mediaOverride ?? (json['Media'] as List?)?.firstOrNull as Map<String, dynamic>?;
|
||||
final channel = (json['Channel'] as List?)?.firstOrNull as Map<String, dynamic>?;
|
||||
|
||||
int? pickInt(String key) {
|
||||
final fromMedia = (media?[key] as num?)?.toInt();
|
||||
final fromJson = (json[key] as num?)?.toInt();
|
||||
return hasOverride ? (fromMedia ?? fromJson) : (fromJson ?? fromMedia);
|
||||
}
|
||||
|
||||
String? pickString(String key) {
|
||||
final fromMedia = media?[key]?.toString();
|
||||
final fromJson = json[key] as String?;
|
||||
return hasOverride ? (fromMedia ?? fromJson) : (fromJson ?? fromMedia);
|
||||
}
|
||||
|
||||
return LiveTvProgram(
|
||||
key: json['key'] as String?,
|
||||
ratingKey: json['ratingKey'] as String?,
|
||||
@@ -55,19 +73,18 @@ class LiveTvProgram {
|
||||
summary: json['summary'] as String?,
|
||||
type: json['type'] as String?,
|
||||
year: (json['year'] as num?)?.toInt(),
|
||||
beginsAt: (json['beginsAt'] as num?)?.toInt() ?? (media?['beginsAt'] as num?)?.toInt(),
|
||||
endsAt: (json['endsAt'] as num?)?.toInt() ?? (media?['endsAt'] as num?)?.toInt(),
|
||||
beginsAt: pickInt('beginsAt'),
|
||||
endsAt: pickInt('endsAt'),
|
||||
grandparentTitle: json['grandparentTitle'] as String?,
|
||||
parentTitle: json['parentTitle'] as String?,
|
||||
index: (json['index'] as num?)?.toInt(),
|
||||
parentIndex: (json['parentIndex'] as num?)?.toInt(),
|
||||
thumb: json['thumb'] as String? ?? json['grandparentThumb'] as String?,
|
||||
art: json['art'] as String?,
|
||||
channelIdentifier:
|
||||
json['channelIdentifier'] as String? ?? media?['channelIdentifier']?.toString() ?? channel?['id']?.toString(),
|
||||
channelCallSign: json['channelCallSign'] as String? ?? media?['channelCallSign'] as String?,
|
||||
live: json['live'] == true || json['live'] == 1 || json['live'] == '1',
|
||||
premiere: json['premiere'] == true || json['premiere'] == 1 || json['premiere'] == '1',
|
||||
channelIdentifier: pickString('channelIdentifier') ?? channel?['id']?.toString(),
|
||||
channelCallSign: pickString('channelCallSign'),
|
||||
live: flexibleBool(json['live']),
|
||||
premiere: flexibleBool(json['premiere']),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,18 @@
|
||||
/// Represents a single MPV configuration entry
|
||||
class MpvConfigEntry {
|
||||
final String key;
|
||||
final String value;
|
||||
final bool isEnabled;
|
||||
|
||||
const MpvConfigEntry({required this.key, required this.value, this.isEnabled = true});
|
||||
|
||||
factory MpvConfigEntry.fromJson(Map<String, dynamic> json) {
|
||||
return MpvConfigEntry(
|
||||
key: json['key'] as String,
|
||||
value: json['value'] as String,
|
||||
isEnabled: json['isEnabled'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {'key': key, 'value': value, 'isEnabled': isEnabled};
|
||||
|
||||
MpvConfigEntry copyWith({String? key, String? value, bool? isEnabled}) {
|
||||
return MpvConfigEntry(key: key ?? this.key, value: value ?? this.value, isEnabled: isEnabled ?? this.isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a saved preset of MPV configurations
|
||||
class MpvPreset {
|
||||
final String name;
|
||||
final List<MpvConfigEntry> entries;
|
||||
final String text;
|
||||
final DateTime createdAt;
|
||||
|
||||
const MpvPreset({required this.name, required this.entries, required this.createdAt});
|
||||
const MpvPreset({required this.name, required this.text, required this.createdAt});
|
||||
|
||||
factory MpvPreset.fromJson(Map<String, dynamic> json) {
|
||||
return MpvPreset(
|
||||
name: json['name'] as String,
|
||||
entries: (json['entries'] as List).map((e) => MpvConfigEntry.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
text: json['text'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'entries': entries.map((e) => e.toJson()).toList(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
Map<String, dynamic> toJson() => {'name': name, 'text': text, 'createdAt': createdAt.toIso8601String()};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user